Référence de l’API Python

Note

Cette page est générée automatiquement à partir des docstrings du code source (sphinx.ext.autodoc) – elle reflète toujours le code réel, jamais en retard sur une doc écrite à la main. Pour une vue d’ensemble plus narrative, voir le Architecture technique.

Volontairement limitée au cœur métier (modèles, services, stockage, API web, sécurité, import/export) – la GUI Tkinter (fletchscore.gui) n’est pas documentée ici : elle ne contient que de l’affichage et des appels aux fonctions ci-dessous (voir Architecture technique), et son import a besoin d’un affichage réel non garanti sur tous les environnements qui construisent cette doc.

fletchscore.models – modèles de données

Entités persistées en base (voir Modèle de données pour le schéma complet et les relations entre elles).

Entité Compétition – regroupe une ou plusieurs Épreuves.

class fletchscore.models.competition.Competition(id: str, nom: str, date_debut: datetime.date, date_fin: datetime.date, lieu: str = '', statut: fletchscore.models.enums.StatutCompetition = <StatutCompetition.OUVERTE: 'ouverte'>, categories_veteran_actives: bool = False, code_club: str | None = None)

Bases: object

categories_veteran_actives: bool

Active ou non les divisions Veteran/Senior pour cette compétition – le règlement les laisse optionnelles, voir fletchscore.models.competiteur.categorie_age.

code_club: str | None

Club organisateur – optionnel (issue #48), distinct du club de chaque compétiteur. None pour une compétition inter-clubs ou fédérale sans club organisateur unique identifié.

Entité Épreuve – une session de tir avec un barème donné.

class fletchscore.models.epreuve.Epreuve(id: str, competition_id: str, nom: str, date: datetime.date, bareme_id: str)

Bases: object

Entité EpreuveTemplate – modèle d’épreuve réutilisable.

Une Épreuve a trois données : nom, date, barème. La date est propre à chaque compétition (jamais réutilisable), mais nom + barème forment souvent un même “type d’épreuve” répété d’une compétition à l’autre (ex. “IFAA Indoor” avec le barème ifaa-indoor) – ce modèle capture uniquement cette partie réutilisable.

class fletchscore.models.epreuve_template.EpreuveTemplate(id: str, nom: str, bareme_id: str)

Bases: object

Entité Barème – définit la structure de score d’un type de round.

class fletchscore.models.bareme.Bareme(id: str, nom: str, nb_series: int, volees_par_serie: int, fleches_par_volee: int, valeurs_zones: list[int], departage_par_x: bool = False)

Bases: object

departage_par_x: bool

True si le round utilise un compteur de flèches en zone X comme critère de départage (ex. IFAA Indoor Round) – jamais compté dans le score brut, voir docs/cahier-des-charges/regles-metier.rst.

valeurs_zones: list[int]

Valeurs de zones possibles, de la plus haute à la plus basse (ex. [5, 4, 3, 2, 1] pour l’IFAA Indoor Round).

Référentiel Style de tir.

Fermé par défaut, pré-rempli avec les 12 codes IFAA (voir STYLES_IFAA) – voir docs/cahier-des-charges/regles-metier.rst. Une compétition/club peut ajouter des variantes FFTL locales via storage, mais la base pré-remplie ne doit jamais être perdue à l’import (voir seed_referentiel_styles dans storage/db.py).

class fletchscore.models.style.Style(code: str, libelle: str, libelle_en: str = '')

Bases: object

Référentiel Club.

class fletchscore.models.club.Club(code_club: str, nom: str, ville: str = '')

Bases: object

Entité Compétiteur et calcul de la division d’âge officielle.

class fletchscore.models.competiteur.Competiteur(id_federal: str, nom: str, prenom: str, code_club: str, sexe: fletchscore.models.enums.Sexe, date_naissance: datetime.date, code_style: str, licence_valide_jusqu_au: datetime.date | None = None)

Bases: object

code_categorie(date_reference: date, *, categories_veteran_actives: bool = False) str

Code combiné sexe + division d’âge + style, ex. AMBB-R (Adulte Homme Barebow-Recurve) – voir la nomenclature officielle dans docs/cahier-des-charges/regles-metier.rst.

licence_valide(date_reference: date) bool

True si aucune date de validité n’est renseignée (pas de contrôle possible) ou si elle n’est pas encore expirée.

fletchscore.models.competiteur.categorie_age(date_naissance: date, date_reference: date, *, categories_veteran_actives: bool = False) DivisionAge

Détermine la division d’âge IFAA/FFTL à une date donnée.

L’âge est calculé à la date de référence (l’épreuve), jamais figé sur la fiche du compétiteur – voir docs/cahier-des-charges/modele-donnees.rst.

Veteran (55+) et Senior (65+) sont explicitement “optionnelles, non contraignantes” dans le règlement IFAA – une compétition peut choisir de ne pas les distinguer et de tout regrouper sous Adult, d’où le paramètre categories_veteran_actives (résout un point ouvert du cahier des charges : ce réglage vit sur l’entité Competition).

Entité Inscription – lien Compétiteur <-> Épreuve.

class fletchscore.models.inscription.Inscription(id: str, id_federal: str, epreuve_id: str)

Bases: object

Entité DemandeRattachement – attribution de token a posteriori.

Objet transitoire : un compétiteur qui n’a pas reçu de token à l’inscription se retrouve dans une liste des inscrits, demande un rattachement, et un organisateur valide après vérification visuelle de l’identité – le token n’est généré qu’à ce moment-là, jamais avant. Voir docs/cahier-des-charges/securite.rst §7.3.

class fletchscore.models.demande_rattachement.DemandeRattachement(id: str, id_federal: str, competition_id: str, statut: fletchscore.models.enums.StatutDemandeRattachement = <StatutDemandeRattachement.EN_ATTENTE: 'en_attente'>, horodatage: datetime.datetime | None = None)

Bases: object

Entité Score – le score final d’une Inscription à son épreuve.

Simplifié à un total + un compteur de X (pas une saisie flèche par flèche ni volée par volée) : décision prise après un premier jalon de saisie détaillée, jugée trop lourde face à l’usage réel – les scores sont déjà totalisés à la main sur la feuille de match pendant le tir, le rôle de FletchScore est d’enregistrer ce résultat et de classer, pas de rejouer le calcul flèche par flèche. Voir docs/architecture.md.

Une seule ligne par Inscription (contrainte UNIQUE en base, voir storage/db.py) – pas une liste de volées.

class fletchscore.models.score.Score(id: str, inscription_id: str, total: int, nombre_x: int = 0, statut: fletchscore.models.enums.StatutScore = <StatutScore.PROPOSE: 'propose'>, propose_par_id_federal: str | None = None)

Bases: object

nombre_x: int

Nombre de flèches en zone X sur l’ensemble de l’épreuve – critère de départage uniquement, jamais ajouté au total (voir Bareme.departage_par_x).

propose_par_id_federal: str | None

Qui a réellement soumis la proposition – absent (None) pour une saisie organisateur, égal à l’id fédéral de l’inscription pour une auto-proposition, ou l’id d’un mandataire agissant via une Procuration validée (voir models/procuration.py). Ne jamais confondre avec l’identité DU SCORE (portée par l’inscription) – c’est la traçabilité de QUI a tapé les chiffres, pour que l’organisateur puisse juger la fiabilité d’une proposition avant de la valider.

total: int

Score final tel que totalisé sur la feuille de match.

Entité Procuration – autorise un compétiteur (le mandataire) à proposer un score au nom d’un autre (le mandant), pour une compétition donnée.

Cas d’usage réel : sur un pas de tir, une seule personne note souvent les scores de tout le groupe plutôt que chacun sorte son téléphone. Demandé par l’utilisateur – toujours soumis à validation humaine de l’organisateur avant de produire le moindre effet (même principe que DemandeRattachement) : jamais de proposition au nom d’autrui possible tant qu’une Procuration n’est pas VALIDEE.

class fletchscore.models.procuration.Procuration(id: str, competition_id: str, id_federal_mandataire: str, id_federal_mandant: str, statut: fletchscore.models.enums.StatutProcuration = <StatutProcuration.EN_ATTENTE: 'en_attente'>, demandee_le: datetime.datetime | None = None)

Bases: object

id_federal_mandant: str

Celui pour qui les scores seront proposés.

id_federal_mandataire: str

Celui qui va proposer des scores – typiquement le scoreur du groupe.

Entité Token – accès compétiteur à une Compétition.

Voir docs/cahier-des-charges/securite.rst : un token par couple (Compétiteur, Compétition), usage unique pour toute la durée de la compétition. Seul le hash (HMAC) est stocké – jamais le token en clair, voir fletchscore.storage pour la génération.

class fletchscore.models.token.Token(id_federal: str, competition_id: str, code_court: str, hash_token: str, statut: fletchscore.models.enums.StatutToken = <StatutToken.EMIS: 'emis'>, cree_le: datetime.datetime | None = None, expire_le: datetime.datetime | None = None)

Bases: object

code_court: str

6-8 caractères alphanumériques sans caractères ambigus (0/O, 1/I) – saisie manuelle en secours si le QR code n’est pas scannable.

hash_token: str

HMAC du token complet – jamais l’identifiant brut stocké en clair.

Entité Message – envoyé par l’organisateur, à un compétiteur précis ou à tous ceux d’une compétition.

Demande de l’utilisateur, sans équivalent dans le cahier des charges initial – voir docs/roadmap.md pour le cadrage retenu (historique persistant, pas de suivi lu/non lu par compétiteur).

class fletchscore.models.message.Message(id: str, competition_id: str, contenu: str, id_federal: str | None = None, envoye_le: datetime.datetime | None = None)

Bases: object

id_federal: str | None

None = message envoyé à tous les compétiteurs de la compétition ; sinon, un id fédéral précis – message ciblé, visible seulement par cette personne (une fois son identité confirmée par cookie de session signé, voir api/competiteur.py).

Enums partagés entre les entités du modèle de données FletchScore.

Values are stored as plain strings in SQLite (see storage/db.py) – StrEnum (Python 3.11+) keeps comparisons and serialization simple without a custom adapter.

class fletchscore.models.enums.DivisionAge(*values)

Bases: StrEnum

Divisions d’âge officielles IFAA/FFTL.

VETERAN et SENIOR sont explicitement “optionnelles, non contraignantes” dans le règlement – voir fletchscore.models.competiteur.categorie_age(), qui n’y bascule que si la compétition les active.

class fletchscore.models.enums.Sexe(*values)

Bases: StrEnum

class fletchscore.models.enums.StatutCompetition(*values)

Bases: StrEnum

class fletchscore.models.enums.StatutDemandeRattachement(*values)

Bases: StrEnum

class fletchscore.models.enums.StatutProcuration(*values)

Bases: StrEnum

class fletchscore.models.enums.StatutScore(*values)

Bases: StrEnum

class fletchscore.models.enums.StatutToken(*values)

Bases: StrEnum

fletchscore.services / fletchscore.storage – cas d’usage et stockage

La couche service fait le lien entre le stockage (SQLite) et l’interface (GUI ou vue web) – volontairement séparée des widgets pour rester testable sans affichage Tkinter.

Couche service – cas d’usage de l’organisateur.

Fait le lien entre le stockage (storage/), les règles (scoring/) et l’interface (gui/). Volontairement séparée des widgets : la GUI ne contient que de l’affichage et des appels à ces fonctions, ce qui rend tout le comportement testable sans affichage Tkinter (voir CLAUDE.md – la GUI réelle n’est pas vérifiable dans l’environnement de dev).

Génère les identifiants (uuid4) plutôt que de les demander à l’appelant : la GUI n’a pas à s’en préoccuper.

fletchscore.services.DELAI_INACTIVITE_ANNEES_PAR_DEFAUT = 3

Durée retenue pour la purge RGPD par inactivité (issue #40, décidée avec l’utilisateur le 2026-08-15). Aucun texte – RGPD ou CNIL – ne fixe de chiffre précis pour les structures sportives (la page CNIL dédiée renvoie explicitement à une justification propre à chaque structure) : ce délai reprend par analogie le seul chiffre que la CNIL documente réellement (3 ans depuis le dernier contact, doctrine fichiers clients/prospects) plutôt que d’en inventer un sans source. Voir docs/cahier-des-charges/securite.rst pour la décision documentée.

class fletchscore.services.DonneesPersonnellesCompetiteur(competiteur: Competiteur, club_nom: str, style_libelle: str, inscriptions: list[LigneDonneesPersonnelles], procurations_comme_mandataire: list[Procuration], procurations_comme_mandant: list[Procuration])

Bases: object

Toutes les données personnelles détenues par FletchScore sur un compétiteur – droit d’accès RGPD (article 15) et support à la portabilité (article 20, issue #38).

exception fletchscore.services.ErreurMetier

Bases: Exception

Erreur attendue, à afficher telle quelle à l’organisateur.

Distincte d’une exception technique (sqlite3.Error, etc.) : le message est rédigé pour être lu par un bénévole, pas par un développeur.

class fletchscore.services.LigneDonneesPersonnelles(competition_nom: str, epreuve_nom: str, epreuve_date: date, score_total: int | None, score_nombre_x: int | None, score_statut: StatutScore | None)

Bases: object

Une inscription du compétiteur avec le score associé s’il existe – une ligne du récapitulatif RGPD (issue #38).

class fletchscore.services.ResumeAccueil(nb_competitions: 'int', nb_competiteurs: 'int', nb_epreuves: 'int', derniere_epreuve: 'tuple[Competition, Epreuve] | None')

Bases: object

derniere_epreuve: tuple[Competition, Epreuve] | None

La compétition et l’épreuve les plus récentes par date (pas un horodatage de dernière action – rien dans le modèle ne trace “quand” une compétition ou un score a été saisi, seulement les dates métier des épreuves elles-mêmes). C’est le meilleur indicateur disponible de “ce qui se passe en ce moment” sans ajouter un champ d’horodatage à plusieurs tables juste pour cet écran.

fletchscore.services.annuler_inscription(conn: Connection, inscription_id: str) None

Annule une inscription – issue #46. Refusée dès qu’un score a déjà été saisi : il faut d’abord le traiter (mécanisme existant côté saisie) avant de pouvoir annuler l’inscription elle-même.

fletchscore.services.anonymiser_competiteur(conn: Connection, id_federal: str) None

Anonymise un compétiteur – droit à l’effacement RGPD (issue #37).

Choix délibéré : anonymisation plutôt que suppression complète, pour ne pas fausser un classement déjà publié en faisant “remonter” les rangs suivants (voir docstring de db.anonymiser_competiteur pour le détail de ce qui est conservé/supprimé). Le nom devient Compétiteur/{id_federal} – reste techniquement rattaché à l’identifiant fédéral (conservé comme clé, voir discussion issue #37) plutôt qu’un texte totalement générique, pour qu’un organisateur retrouve facilement quelle ligne correspond à quelle demande d’effacement s’il doit s’y référer plus tard.

fletchscore.services.classement_epreuve(conn: Connection, epreuve_id: str) dict[str, list[LigneClassement]]

Classement live d’une épreuve, groupé par catégorie.

Le paramètre categories_veteran_actives est lu sur la Compétition parente – l’organisateur l’a choisi une fois à la création, la GUI n’a pas à le repasser à chaque affichage.

fletchscore.services.classement_global_competition(conn: Connection, competition_id: str) tuple[list[Epreuve], dict[str, list[LigneClassementGlobal]]]

Classement cumulé sur toutes les épreuves d’une compétition – un total par épreuve, plus un total global qui sert de critère de tri (voir scoring.classement_global pour le pourquoi de l’absence de départage au X ici).

Retourne aussi la liste des épreuves (dans l’ordre où classement_global les a utilisées) – nécessaire à l’appelant pour savoir quelle colonne correspond à quelle épreuve à l’export.

fletchscore.services.cloturer_competition(conn: Connection, competition_id: str) Competition

Clôture une compétition – issue #50. Fait passer le statut à CLOTUREE, ce qui active les garde-fous déjà en place mais jusqu’ici jamais déclenchés (creer_epreuve, modifier_epreuve, supprimer_epreuve) et bloque désormais aussi toute nouvelle saisie/proposition de score (voir saisir_score_final).

Révoque au passage tous les accès compétiteurs actifs de cette compétition – fait enfin correspondre le comportement réel à ce que docs/cahier-des-charges/securite.rst promettait déjà (“expiration automatique à la clôture”). Réversible via rouvrir_competition ; les accès révoqués ici ne le sont pas automatiquement à la réouverture (une révocation reste un choix explicite, voir revoquer_acces).

fletchscore.services.creer_club(conn: Connection, code_club: str, nom: str, ville: str = '') Club

Ajoute un club manuellement – mêmes règles que l’import CSV (voir io/import_csv.py) : code et nom obligatoires, code déjà pris refusé plutôt qu’écrasé silencieusement.

fletchscore.services.creer_competiteur(conn: Connection, id_federal: str, nom: str, prenom: str, code_club: str, sexe: Sexe, date_naissance: date, code_style: str, licence_valide_jusqu_au: date | None = None) Competiteur

Ajoute un compétiteur manuellement – mêmes règles que l’import CSV : club et style doivent déjà exister dans leur référentiel (jamais créés à la volée), id fédéral déjà pris refusé plutôt qu’écrasé silencieusement (voir io/import_csv.py, même principe).

fletchscore.services.creer_competition_depuis_template(conn: Connection, template_id: str, nom: str, date_debut: date, date_fin: date, *, lieu: str = '', categories_veteran_actives: bool = False, code_club: str | None = None) tuple[Competition, list[Epreuve]]

Crée une compétition puis génère en une fois toutes ses épreuves à partir du modèle – délègue à creer_competition()/creer_epreuve() pour ne pas dupliquer leurs validations, même principe que creer_epreuve_depuis_template.

Chaque épreuve générée prend date_debut comme date par défaut – un modèle de compétition ne porte aucune date (voir docstring de CompetitionTemplate). Si les épreuves ne tombent pas toutes le même jour, l’organisateur ajuste ensuite individuellement via modifier_epreuve(), déjà existant – pas la peine d’un mécanisme de dates par épreuve dans le modèle rien que pour ce cas.

fletchscore.services.creer_epreuve_depuis_template(conn: Connection, competition_id: str, template_id: str, date_epreuve: date) Epreuve

Crée une épreuve à partir d’un modèle – seule la date reste à saisir, nom et barème sont repris du modèle. Passe par creer_epreuve() pour ne pas dupliquer ses validations (compétition ouverte, date dans les bornes de la compétition…).

fletchscore.services.creer_template_competition(conn: Connection, nom: str, epreuves: list[tuple[str, str]]) CompetitionTemplate

Crée un modèle de compétition réutilisable – un bundle de plusieurs épreuves (nom, bareme_id), toujours sans date (même principe qu’EpreuveTemplate, voir son docstring).

epreuves : liste de (nom, bareme_id) dans l’ordre souhaité – au moins une, un modèle vide n’aurait rien à générer.

fletchscore.services.creer_template_depuis_competition(conn: Connection, competition_id: str, nom_template: str | None = None) CompetitionTemplate

Enregistre les épreuves d’une compétition existante comme modèle réutilisable – reprend le nom de la compétition par défaut (personnalisable via nom_template, même principe que creer_template_depuis_epreuve).

fletchscore.services.creer_template_depuis_epreuve(conn: Connection, epreuve_id: str, nom_template: str | None = None) EpreuveTemplate

Enregistre une épreuve existante comme modèle réutilisable – reprend son nom par défaut (personnalisable via nom_template, utile si on veut un nom de modèle différent du nom de l’épreuve d’origine, ex. “IFAA Indoor – samedi” -> modèle “IFAA Indoor”).

fletchscore.services.creer_template_epreuve(conn: Connection, nom: str, bareme_id: str) EpreuveTemplate

Crée un modèle d’épreuve réutilisable (nom + barème), indépendant de toute compétition – voir EpreuveTemplate.

fletchscore.services.demander_procuration(conn: Connection, id_federal_mandataire: str, id_federal_mandant: str, competition_id: str) Procuration

Demande le droit de proposer des scores au nom d’un autre compétiteur pour une compétition – sans effet tant qu’un organisateur ne l’a pas validée (voir valider_procuration), même principe que demander_rattachement.

fletchscore.services.demander_rattachement(conn: Connection, id_federal: str, competition_id: str) DemandeRattachement

Enregistre une demande de rattachement – ne génère jamais de token à ce stade, seulement une entrée en file d’attente (voir docs/cahier-des-charges/securite.rst : le token n’est émis qu’après validation humaine de l’organisateur, voir valider_rattachement).

Refuse une nouvelle demande si un accès valide existe déjà, ou si une demande est déjà en attente pour ce (compétiteur, compétition) – sans ce garde-fou, valider une demande redondante émettrait un second token pour la même personne, sans jamais révoquer le premier : deux codes valides simultanés pour un seul compétiteur, confusion pour l’organisateur qui reverrait une demande déjà traitée en pratique.

fletchscore.services.donnees_personnelles_en_dict(donnees: DonneesPersonnellesCompetiteur) dict

Représentation JSON-sérialisable de DonneesPersonnellesCompetiteur – portabilité RGPD (article 20, issue #38). Format volontairement simple (dict de types JSON natifs) : l’article 20 n’impose qu’un format structuré lisible par machine, rien de plus sophistiqué n’est nécessaire pour ce volume de données.

fletchscore.services.envoyer_message(conn: Connection, competition_id: str, contenu: str, id_federal: str | None = None) Message

Envoie un message – à un compétiteur précis (id_federal fourni) ou à tous ceux de la compétition (id_federal=None).

Pas de suivi lu/non lu : envoyer suffit, l’organisateur n’a pas besoin de savoir qui l’a vu (choix explicite, voir docs/roadmap.md).

fletchscore.services.generer_token(conn: Connection, id_federal: str, competition_id: str, *, expire_le: datetime | None = None) tuple[Token, str]

Génère un nouveau token d’accès pour ce compétiteur à cette compétition.

Retourne le Token persisté ET le secret en clair – ce dernier n’est jamais stocké tel quel (seul son HMAC l’est, voir _hash_token), donc c’est la seule fois où l’appelant peut le récupérer pour l’encoder dans un QR code ou l’afficher.

fletchscore.services.libelle_competiteur(competiteur: Competiteur) str

Libellé d’affichage d’un compétiteur, id fédéral inclus pour distinguer deux personnes du même nom.

fletchscore.services.libelle_epreuve(competition: Competition, epreuve: Epreuve) str

Libellé d’affichage d’une épreuve, avec sa compétition – partagé par les écrans qui doivent choisir une épreuve parmi toutes celles de toutes les compétitions (saisie, classement).

fletchscore.services.lister_competiteurs_inactifs(conn: Connection, date_reference: date, delai_annees: int = 3) list[tuple[Competiteur, date]]

Compétiteurs éligibles à une purge RGPD pour inactivité (issue #40) – ceux dont la dernière inscription remonte à plus de delai_annees par rapport à date_reference, triés du plus ancien au plus récent.

Ne concerne que les compétiteurs ayant déjà concouru au moins une fois : sans inscription du tout, un compétiteur est déjà supprimable sans attendre (voir supprimer_competiteur, #43). Exclut aussi ceux déjà anonymisés (prenom == "", voir anonymiser_competiteur, #37) – leurs données identifiantes ont déjà disparu, rien de plus à purger.

date_reference est un paramètre explicite (jamais date.today() en interne) pour rester testable de façon déterministe – même principe que competiteur.categorie_age().

fletchscore.services.lister_competiteurs_non_inscrits(conn: Connection, epreuve_id: str) list[Competiteur]

Compétiteurs de la base qui ne sont pas encore inscrits à cette épreuve – pour alimenter un sélecteur GUI sans proposer deux fois la même personne.

fletchscore.services.lister_demandes_en_attente(conn: Connection, competition_id: str) list[tuple[Competiteur, DemandeRattachement]]

Associe chaque demande en attente à son compétiteur – pour affichage direct dans la GUI (nom, prénom), pas juste un id_federal brut à recouper manuellement.

fletchscore.services.lister_epreuves_toutes(conn: Connection) list[tuple[Competition, Epreuve]]

Toutes les épreuves, toutes compétitions confondues, triées par date décroissante – pour un sélecteur GUI qui n’a pas besoin de naviguer compétition par compétition pour retrouver l’épreuve du jour.

fletchscore.services.lister_mandants_pour(conn: Connection, id_federal_mandataire: str, competition_id: str) list[Competiteur]

Compétiteurs pour lesquels ce mandataire a une procuration validée sur cette compétition – pour lui proposer, côté web, pour qui il peut soumettre un score.

fletchscore.services.lister_messages_envoyes(conn: Connection, competition_id: str) list[Message]

Historique complet des messages envoyés pour cette compétition, tous destinataires confondus – pour l’écran organisateur.

fletchscore.services.lister_messages_pour(conn: Connection, competition_id: str, id_federal: str) list[Message]

Messages visibles par ce compétiteur : les siens + ceux adressés à tous, du plus récent au plus ancien (voir db.list_messages_for).

fletchscore.services.lister_procurations_en_attente(conn: Connection, competition_id: str) list[tuple[Competiteur, Competiteur, Procuration]]

Associe chaque demande en attente à ses deux compétiteurs (mandataire, mandant) – pour affichage direct dans la GUI.

fletchscore.services.lister_procurations_validees(conn: Connection, competition_id: str) list[tuple[Competiteur, Competiteur, Procuration]]

Associe chaque procuration validée de cette compétition à ses deux compétiteurs – pour l’écran “révoquer une procuration” de la GUI, sur le modèle de lister_tokens_actifs.

fletchscore.services.lister_propositions_en_attente(conn: Connection, epreuve_id: str) list[tuple[Competiteur, Score]]

Associe chaque score proposé (pas encore validé/rejeté) de cette épreuve à son compétiteur – pour affichage direct dans la GUI.

fletchscore.services.lister_tokens_actifs(conn: Connection, competition_id: str) list[tuple[Competiteur, Token]]

Associe chaque token non révoqué de cette compétition à son compétiteur – pour l’écran “révoquer un accès” de la GUI. Les tokens révoqués ne sont pas cachés en base (traçabilité), mais n’ont pas leur place dans une liste “accès actifs”.

fletchscore.services.modifier_club(conn: Connection, code_club: str, nom: str, ville: str = '') Club

Corrige un club existant – code_club n’est volontairement pas modifiable (voir storage.update_club) : c’est la clé référencée par les fiches compétiteur, la changer casserait ces références.

fletchscore.services.modifier_competiteur(conn: Connection, id_federal: str, nom: str, prenom: str, code_club: str, sexe: Sexe, date_naissance: date, code_style: str, licence_valide_jusqu_au: date | None = None) Competiteur

Corrige un compétiteur existant – mêmes règles que creer_competiteur(). id_federal n’est volontairement pas modifiable (voir storage.update_competiteur) : c’est l’identifiant fédéral, la clé de tout le reste (inscriptions, tokens…).

fletchscore.services.modifier_competition(conn: Connection, competition_id: str, nom: str, date_debut: date, date_fin: date, *, lieu: str = '', categories_veteran_actives: bool = False, code_club: str | None = None) Competition

Corrige une compétition existante – mêmes règles que creer_competition(), plus une vérification propre à la modification : si la compétition a déjà des épreuves, retrécir les dates ne doit pas en laisser une hors des nouvelles bornes (le statut n’est volontairement pas modifiable ici – clôturer une compétition est une action distincte, pas un simple champ à corriger).

fletchscore.services.modifier_epreuve(conn: Connection, epreuve_id: str, nom: str, date_epreuve: date, bareme_id: str) Epreuve

Corrige une épreuve existante – mêmes règles que creer_epreuve(). Le barème ne peut plus être changé une fois une volée saisie (voir storage.epreuve_a_des_scores) : les numéros de série/volée déjà enregistrés ne correspondraient plus forcément au nouveau barème (nombre de séries, de volées, de flèches différent).

fletchscore.services.parser_date(texte: str, nom_champ: str) date

Convertit un champ de saisie AAAA-MM-JJ en date.

Utilitaire partagé par les écrans GUI (qui ne manipulent que du texte saisi) – vit ici plutôt que dans un module gui/ pour rester testable sans customtkinter.

fletchscore.services.proposer_score(conn: Connection, id_federal_proposant: str, epreuve_id: str, total: int, *, nombre_x: int = 0, id_federal_cible: str | None = None) Score

Propose un score final pour une épreuve – pour soi-même par défaut (id_federal_cible omis), ou pour quelqu’un d’autre via une Procuration déjà validée par l’organisateur (id_federal_cible fourni). Mêmes bornes que la saisie organisateur (saisir_score_final), statut PROPOSE : n’apparaît dans aucun classement tant qu’un organisateur ne l’a pas validé (valider_score_propose), voir scoring.total_scores.

Le proposant réel est toujours enregistré (Score.propose_par_id_federal), même en cas d’auto-proposition – pour que l’organisateur puisse juger la fiabilité d’une proposition avant de la valider, plutôt que de voir un total sans savoir qui l’a réellement soumis (voir models/score.py).

Refuse d’écraser un score déjà validé – une nouvelle proposition ne doit jamais rouvrir silencieusement un score officiel déjà entériné ; seul l’organisateur peut le corriger (écran Saisie). Re-proposer avant validation, en revanche, remplace la proposition précédente sans problème (une correction avant revue, cas normal).

fletchscore.services.rassembler_donnees_personnelles(conn: Connection, id_federal: str) DonneesPersonnellesCompetiteur

Rassemble l’ensemble des données personnelles détenues sur ce compétiteur – droit d’accès RGPD (issue #38). Contrairement à la plupart des pages de la vue compétiteur (scopées à la compétition de la session en cours, ex. lister_messages_pour), volontairement pas limité à une seule compétition : l’article 15 porte sur l’ensemble des données détenues, pas seulement celles de l’événement en cours – le compétiteur est déjà authentifié (id_federal vérifié par le cookie de session), donc rassembler ses données d’autres compétitions n’expose rien à un tiers.

fletchscore.services.resumer_accueil(conn: Connection) ResumeAccueil

Chiffres clés pour l’écran d’accueil – une seule fonction, testée une fois, plutôt que de laisser la GUI recompter elle-même.

fletchscore.services.revoquer_acces(conn: Connection, id_federal: str, competition_id: str) None

Révoque le token d’un compétiteur pour une compétition – ne lève pas d’erreur si aucun token n’existait déjà (l’effet recherché, “cette personne n’a plus accès”, est atteint dans les deux cas).

fletchscore.services.revoquer_procuration(conn: Connection, procuration_id: str) None

Révoque une procuration déjà validée – le mandataire ne peut plus proposer de score pour ce mandant à partir de maintenant (les scores déjà proposés ne sont pas affectés rétroactivement).

fletchscore.services.rouvrir_competition(conn: Connection, competition_id: str) Competition

Rouvre une compétition clôturée par erreur – symétrique de cloturer_competition (issue #50).

fletchscore.services.saisir_score_final(conn: Connection, inscription_id: str, total: int, *, nombre_x: int = 0, statut: StatutScore = StatutScore.VALIDE, propose_par_id_federal: str | None = None) Score

Enregistre (ou corrige) le score final d’une inscription, tel que totalisé sur la feuille de match – pas une saisie flèche par flèche ni volée par volée (voir models/score.py pour le pourquoi).

Le total est borné par bareme.score_max : au-delà, c’est un signal de saisie erronée (faute de frappe), pas une valeur à corriger silencieusement.

Le statut par défaut est VALIDE : une saisie faite par l’organisateur lui-même n’a pas à repasser par une file de validation (voir docs/cahier-des-charges/securite.rst §7.2).

propose_par_id_federal : qui a réellement soumis ce score, pour une proposition en ligne (voir proposer_score) – laissé à None pour une saisie organisateur, sans lien avec une soumission en ligne.

Refuse toute écriture sur une compétition clôturée (issue #50) – même garde-fou que creer_epreuve/modifier_epreuve/ supprimer_epreuve, point de passage unique pour toute saisie de score (organisateur, proposer_score, valider_score_propose, rejeter_score_propose passent tous par ici).

fletchscore.services.signer_identite_competiteur(id_federal: str, competition_id: str) str

Signe un id fédéral + une compétition pour un cookie de session côté vue compétiteur – empêche un navigateur de se faire passer pour quelqu’un d’autre juste en modifiant son cookie à la main.

Utilisé après confirmation d’un code d’accès (voir api/competiteur.py) pour que le navigateur “se souvienne” de qui il est le temps de la session, sans qu’un cookie en clair suffise à usurper l’identité d’un autre compétiteur – même principe HMAC que les tokens (_hash_token), même clé serveur. La compétition est signée avec l’id fédéral (pas seulement l’id) : “Mes messages” a besoin de savoir pour quelle compétition, un compétiteur pouvant en principe avoir un accès à plusieurs.

fletchscore.services.supprimer_competiteur(conn: Connection, id_federal: str) None

Supprime purement et simplement une fiche compétiteur – réservé à un compétiteur qui n’a jamais concouru (issue #43), contrairement à anonymiser_competiteur (#37) qui s’applique à un compétiteur déjà engagé. Refusé dès la moindre inscription, dans n’importe quelle épreuve : le seul chemin pour quelqu’un déjà classé reste l’anonymisation, pour ne pas revenir sur la décision prise à ce sujet (risque de fausser un classement déjà publié).

fletchscore.services.supprimer_competition(conn: Connection, competition_id: str) None

Supprime une compétition vide – issue #45. Refusée dès qu’un score existe dans n’importe laquelle de ses épreuves, même un seul – pas de cascade forcée sur des données notées. En l’absence de score, cascade complète sur épreuves, inscriptions et accès (voir db.supprimer_competition). Contrairement à supprimer_epreuve (#44), le statut de la compétition n’est pas vérifié ici – modifier_competition ne bloque déjà pas sur une compétition clôturée (clôturer est une action à part, pas un champ comme un autre), pas de raison d’introduire une règle plus stricte à la suppression.

fletchscore.services.supprimer_epreuve(conn: Connection, epreuve_id: str) None

Supprime une épreuve vide – issue #44. Contrairement à la compétition (#45), les inscriptions sans score sont supprimées avec (rien d’irréversible à perdre) plutôt que de bloquer aussi sur elles – seule la présence d’un score, même un seul, refuse toute la suppression. Même règle que modifier_epreuve sur une compétition clôturée : pas plus supprimable que modifiable.

fletchscore.services.valider_rattachement(conn: Connection, demande_id: str) tuple[Token, str]

Valide une demande après vérification visuelle de l’identité par l’organisateur – génère et attribue le token à ce moment précis, jamais avant (voir docs/cahier-des-charges/securite.rst).

fletchscore.services.valider_score_propose(conn: Connection, inscription_id: str) Score

Valide un score proposé par le compétiteur – le fait passer de PROPOSE à VALIDE sans en changer les valeurs. Devient à ce moment précis LE score officiel de cette inscription, comptabilisé dans le classement.

fletchscore.services.verifier_code_court(conn: Connection, code_court: str) Token | None

Vérifie un accès à partir du seul code court, saisi à la main – volontairement plus faible que verifier_token() : ne demande pas le secret complet, seulement le code à 6 caractères communiqué de vive voix ou par écrit.

Acceptable dans le contexte actuel (v0.2, wifi de club, aucune écriture de score en jeu – juste “confirmer que je suis bien identifié”) mais pas suffisant le jour où une vraie donnée sensible transitera par ce chemin (proposition de score, v0.3) : un code à 6 caractères depuis un alphabet de 32 (~30 bits) reste devinable par force brute si l’enjeu grandit – à revoir à ce moment-là plutôt que d’y ajouter des rustines a posteriori.

fletchscore.services.verifier_identite_signee(valeur: str) tuple[str, str] | None

Vérifie une valeur de cookie produite par signer_identite_competiteur – retourne (id_federal, competition_id) si la signature est valide, None sinon (cookie absent, altéré, ou fabriqué de toutes pièces).

fletchscore.services.verifier_token(conn: Connection, code_court: str, secret_token: str) Token | None

Vérifie un token présenté par un compétiteur : recherche par code_court puis comparaison du HMAC du secret présenté – jamais une comparaison directe (le secret n’est jamais stocké en clair). hmac.compare_digest plutôt que == : une comparaison naïve fuiterait un minuscule signal temporel exploitable (attaque par canal auxiliaire), même si le risque réel reste faible sur un wifi de club – pas de raison de s’en priver, ça ne coûte rien.

Retourne None aussi bien si le token n’existe pas, si le secret ne correspond pas, que s’il est expiré/révoqué – volontairement le même signal dans les trois cas, pour ne pas révéler à un attaquant lequel des trois a échoué.

Stockage SQLite local – source de vérité unique de FletchScore.

Un seul fichier, pas de serveur distant, pas d’écriture concurrente à gérer (poste organisateur unique en v0.1 – voir docs/cahier-des-charges/architecture.rst). Les dates sont stockées en ISO 8601 (TEXT), les listes (valeurs de zones, valeurs de flèches) en JSON (TEXT) – SQLite n’a pas de type liste natif.

fletchscore.storage.db.anonymiser_competiteur(conn: Connection, id_federal: str, nom_anonyme: str) None

Anonymise un compétiteur – droit à l’effacement RGPD (issue #37).

Nom/prénom remplacés par nom_anonyme (prénom vidé) ; scores et inscriptions volontairement conservés, pour ne pas fausser les classements déjà publiés (le rang d’un tiers ne doit pas se retrouver décalé par la suppression d’un autre compétiteur). Tokens, procurations (comme mandataire et comme mandant) et demandes de rattachement supprimés – l’accès de ce compétiteur doit cesser, aucune raison de le garder après une demande d’effacement. Messages qui lui étaient adressés supprimés aussi (jamais les messages diffusés à tous, id_federal IS NULL, sans lien avec lui).

Transaction unique (tout ou rien) – un état à moitié anonymisé (ex. nom effacé mais token encore valide) serait pire que l’état de départ.

fletchscore.storage.db.connect(path: str) Connection

Ouvre (ou crée) le fichier SQLite local et active les clés étrangères – désactivées par défaut par SQLite, ce qui laisserait passer silencieusement une inscription vers une épreuve inexistante.

fletchscore.storage.db.date_derniere_activite_competiteur(conn: Connection, id_federal: str) date | None

Date de l’épreuve la plus récente à laquelle ce compétiteur a été inscrit, toutes compétitions confondues – None s’il n’a jamais été inscrit nulle part. Utilisé pour la purge RGPD par inactivité (issue #40) : un compétiteur jamais inscrit est déjà supprimable sans attendre (voir supprimer_competiteur, #43), pas besoin d’un délai d’inactivité pour lui.

fletchscore.storage.db.epreuve_a_des_scores(conn: Connection, epreuve_id: str) bool

True si au moins un score a été saisi pour une inscription de cette épreuve – utilisé pour interdire un changement de barème une fois la saisie commencée (le score déjà entré a été validé contre le score_max de l’ancien barème, pas forcément cohérent avec un nouveau).

fletchscore.storage.db.importer_donnees_competition(conn: Connection, *, competition: Competition, epreuves: list[Epreuve], clubs_a_creer: list[Club], competiteurs_a_creer: list[Competiteur], baremes_a_creer: list[Bareme], inscriptions: list[Inscription], scores: list[Score]) None

Écrit en une seule transaction tout ce qu’il faut pour restaurer une compétition complète (issue #7) – voir io.sauvegarde_competition.importer_competition, qui résout en amont quels clubs/compétiteurs/barèmes doivent réellement être créés (ceux déjà présents sur cette base sont réutilisés, jamais passés ici). Compétition/épreuves/inscriptions/scores toujours neufs.

Ordre d’insertion contraint par les clés étrangères : clubs avant compétiteurs (competiteurs.code_club), barèmes avant épreuves (epreuves.bareme_id), compétition avant épreuves (epreuves.competition_id), compétiteurs+épreuves avant inscriptions, inscriptions avant scores. Rollback complet si un seul insert échoue – une compétition à moitié restaurée serait pire qu’un échec net.

fletchscore.storage.db.init_schema(conn: Connection) None

Crée le schéma s’il n’existe pas encore, puis applique les migrations en attente (voir MIGRATIONS) – idempotent, peut être rappelée à chaque démarrage sans risque, que la base soit neuve, déjà à jour, ou dans un état intermédiaire laissé par une version antérieure du code.

Distingue une base neuve (aucune migration à rejouer : _SCHEMA crée déjà tout dans son état le plus récent) d’une base préexistante créée avant ce mécanisme (scores déjà là mais pas schema_version – part de la version 0, migrations rejouées). Sans cette distinction, une base neuve se verrait inutilement rejouer des migrations déjà satisfaites par _SCHEMA.

fletchscore.storage.db.list_competitions(conn: Connection) list[Competition]

Triées par date de début décroissante – la plus récente (ou à venir) en premier, la plus utile à retrouver pour un organisateur.

fletchscore.storage.db.list_inscriptions_by_competiteur(conn: Connection, id_federal: str) list[Inscription]

Toutes épreuves confondues – utilisé pour savoir si un compétiteur a déjà concouru (issue #43 : condition de suppression).

fletchscore.storage.db.list_messages_by_competition(conn: Connection, competition_id: str) list[Message]

Tous les messages d’une compétition, tous destinataires confondus – pour l’écran organisateur (historique de ce qui a été envoyé).

fletchscore.storage.db.list_messages_for(conn: Connection, competition_id: str, id_federal: str) list[Message]

Messages visibles par ce compétiteur pour cette compétition – ceux qui lui sont adressés (id_federal correspond) et ceux adressés à tous (id_federal IS NULL). Triés du plus récent au plus ancien.

fletchscore.storage.db.list_procurations_by_competiteur(conn: Connection, id_federal: str) list[Procuration]

Toutes les procurations où ce compétiteur apparaît, comme mandataire ou comme mandant – tout statut, toute compétition. Utilisé pour le droit d’accès RGPD (issue #38), qui porte sur l’ensemble des données détenues, pas une seule compétition.

fletchscore.storage.db.ouvrir_base(chemin: str = 'fletchscore.db') Connection

Ouvre la base locale, crée le schéma et charge les référentiels.

Point d’entrée unique du démarrage : la GUI comme les scripts passent par ici plutôt que d’enchaîner connect/init_schema/seed à la main. Les deux seed_* sont idempotents – les appeler à chaque démarrage ne duplique rien et rattrape une base créée par une version antérieure qui n’aurait pas encore tel barème.

fletchscore.storage.db.revoquer_tokens_by_competition(conn: Connection, competition_id: str) None

Révoque d’un coup tous les tokens actifs d’une compétition – utilisé à sa clôture (services.cloturer_competition, issue #50).

fletchscore.storage.db.seed_baremes_preconfigures(conn: Connection) None

Insère les barèmes préconfigurés (Flint Indoor, IFAA Indoor) s’ils n’existent pas déjà – idempotent.

fletchscore.storage.db.seed_referentiel_styles(conn: Connection) None

Insère les 12 styles IFAA s’ils n’existent pas déjà – idempotent, à appeler à chaque démarrage sans risque de doublon.

fletchscore.storage.db.supprimer_competiteur(conn: Connection, id_federal: str) None

Supprime purement et simplement la fiche compétiteur – issue #43, réservée à un compétiteur sans aucune inscription (vérifié en amont par services.supprimer_competiteur, jamais ici – cette fonction fait juste l’écriture). Contrairement à anonymiser_competiteur (#37), qui garde la ligne pour ne pas fausser un classement déjà publié, il n’y a ici justement aucun classement concerné : rien à perdre en supprimant réellement.

Tokens, procurations (mandataire et mandant), demandes de rattachement et messages qui lui étaient adressés supprimés avec – un accès sans fiche compétiteur derrière n’a plus de sens. Même transaction unique (tout ou rien) que anonymiser_competiteur.

fletchscore.storage.db.supprimer_competition(conn: Connection, competition_id: str) None

Supprime une compétition et tout ce qui en dépend sans avoir de score – issue #45, réservée à une compétition sans aucun score (vérifié en amont par services.supprimer_competition via epreuve_a_des_scores sur chacune de ses épreuves, jamais ici).

Cascade complète : épreuves, leurs inscriptions (aucune n’a de score si on arrive ici, même logique que supprimer_epreuve), et l’état d’accès propre à cette compétition – tokens, procurations, demandes de rattachement, messages – sans objet une fois la compétition partie. Transaction unique, même pattern que les autres suppressions de ce lot.

fletchscore.storage.db.supprimer_epreuve(conn: Connection, epreuve_id: str) None

Supprime une épreuve et ses inscriptions – issue #44, réservée à une épreuve sans aucun score (vérifié en amont par services.supprimer_epreuve via epreuve_a_des_scores, jamais ici). Cascade sur les inscriptions : si on arrive ici, aucune n’a de score, rien d’irréversible à perdre en les supprimant avec. Transaction unique, même pattern que supprimer_competiteur.

fletchscore.storage.db.supprimer_inscription(conn: Connection, inscription_id: str) None

Annule une inscription sans score – issue #46, réservée à une inscription sans score (vérifié en amont par services.annuler_inscription via get_score_by_inscription, jamais ici). Une seule instruction – pas de transaction dédiée nécessaire, contrairement aux suppressions en cascade du même lot (#43/#44/#45).

fletchscore.storage.db.update_club(conn: Connection, club: Club) None

code_club n’est pas modifiable via cette fonction – c’est l’identifiant référencé par competiteurs.code_club, le changer demanderait de mettre à jour toutes les fiches compétiteur qui le référencent. Seuls nom et ville sont corrigibles.

fletchscore.storage.db.update_competiteur(conn: Connection, competiteur: Competiteur) None

id_federal n’est pas modifiable via cette fonction – c’est l’identifiant fédéral, la clé de tout le reste (inscriptions, tokens…). Tous les autres champs sont corrigibles.

fletchscore.storage.db.upsert_score(conn: Connection, score: Score) None

Insère ou remplace le score de cette inscription – l’organisateur corrige un score déjà saisi plutôt que d’en créer un nouveau en doublon (au plus un Score par Inscription, contrainte UNIQUE).

fletchscore.api.competiteur – vue web compétiteur

Serveur HTTP local (http.server, thread séparé) qui sert le classement en lecture seule, le formulaire de proposition de score, et la procuration – voir Sécurité & vue compétiteur pour le détail du flux de validation et des tokens.

Vue compétiteur – serveur HTTP, majoritairement en lecture seule.

Lecture seule pour l’essentiel (classement live), plus quelques écritures à faible enjeu, jamais de score : la demande de rattachement (« je pense être telle personne » – aucun effet avant validation humaine de l’organisateur, voir services.valider_rattachement) et la confirmation d’un code déjà attribué. Pas de token requis pour consulter le classement – seulement pour les fonctionnalités qui identifient le compétiteur (messages ciblés, voir page_mes_messages).

Le serveur tourne dans un thread séparé pendant que la GUI continue – il utilise donc systématiquement sa propre connexion SQLite en lecture seule (jamais celle de la GUI, qui appartient à un autre thread) via l’URI file:...?mode=ro, une connexion neuve par requête – y compris pour les écritures, qui passent par services.py, donc par sa propre gestion de connexion à chaque appel.

Pas de rendu JS : une simple balise <meta http-equiv="refresh"> recharge la page à intervalle régulier ; la demande de rattachement passe par un formulaire HTML natif (POST), sans JavaScript non plus.

Style visuel et bascule langue/thème repris de theme.css (src/fletchscore/web/), le système de conception partagé avec FletchTime – servi tel quel, jamais dupliqué dans le code Python. Préférence de langue/thème mémorisée par cookie plutôt que par JavaScript : cohérent avec le choix “pas de JS” déjà fait pour cette page, et survit naturellement au rechargement automatique périodique (un cookie persiste, un état JS en mémoire ne survivrait pas à un rechargement complet de page).

class fletchscore.api.competiteur.GestionnaireRequetesCompetiteur(request, client_address, server)

Bases: BaseHTTPRequestHandler

log_message(format: str, *args) None

Log an arbitrary message.

This is used by all other logging functions. Override it if you have specific logging wishes.

The first argument, FORMAT, is a format string for the message to be logged. If the format string contains any % escapes requiring parameters, they should be specified as subsequent arguments (it’s just like printf!).

The client ip and current date/time are prefixed to every message.

Unicode control characters are replaced with escaped hex before writing the output to stderr.

class fletchscore.api.competiteur.ServeurCompetiteur(adresse: tuple[str, int], chemin_base: str)

Bases: HTTPServer

Serveur HTTP – porte le chemin de la base plutôt qu’une connexion ouverte, pour que chaque requête ouvre la sienne (voir le docstring du module).

fletchscore.api.competiteur.adresse_ip_locale() str

Meilleure estimation de l’IP de la machine sur le réseau local – celle à donner aux compétiteurs pour ouvrir la page dans leur navigateur.

Astuce classique : une connexion UDP vers une IP externe n’envoie aucun paquet réseau réel (UDP est sans connexion) – ça ne fait qu’interroger la table de routage locale pour savoir quelle interface serait utilisée, ce qui donne l’IP locale correcte même sans accès internet réel. Repli sur 127.0.0.1 si ça échoue (aucune interface réseau disponible).

fletchscore.api.competiteur.creer_serveur(chemin_base: str, port: int = 0, https: bool = False) ServeurCompetiteur

Crée le serveur sans le démarrer – port=0 laisse l’OS choisir un port libre (consulter ensuite serveur.server_port).

https=True enveloppe le socket dans TLS avec un certificat auto-signé, généré au besoin (voir certificat_https). Le socket est déjà lié (server_bind/server_activate, faits par HTTPServer.__init__) avant d’être enveloppé – enveloppement après coup, pas de configuration TLS spéciale au niveau de la classe du serveur elle-même.

fletchscore.api.competiteur.page_accueil(conn: Connection, lang: str = 'fr', theme: str = 'dark', identite: tuple[str, str] | None = None) str

Page d’accueil de la vue compétiteur – message de bienvenue, bannière du dernier message reçu si une session est identifiée (voir identite, posée après confirmation d’un code), liste des compétitions/épreuves (avec un lien de demande d’accès par compétition), et une section pour confirmer un code déjà reçu.

fletchscore.api.competiteur.page_affichage_public(conn: Connection, competition_id: str, lang: str = 'fr', rotation_secondes: int | None = None) str

Écran d’affichage public (téléphone en support, ou grand écran, laissé ouvert sans surveillance pour des spectateurs) – voir issue #21. Distinct de page_competition (usage compétiteur identifié, inchangé) : mêmes données de classement (services.classement_global_competition, factorisées dans _tableau_classement_global), mais sans lien de retour ni aucune fonctionnalité liée à un compétiteur précis, et sans token requis – même politique d’accès public que page_competition (voir docstring en tête de module).

rotation_secondes : durée d’affichage d’une catégorie, réglable par écran via ?rotation=N dans l’URL (voir do_GET) – utile si un grand écran doit rester plus longtemps sur chaque catégorie qu’un téléphone en support. None ou une valeur invalide (pas un entier positif) retombe sur AFFICHAGE_ROTATION_SECONDES, pas d’erreur – un lien mal formé ne doit jamais casser un écran public laissé sans surveillance.

Une seule catégorie affichée à la fois (pas tout le classement empilé) quand il y en a plusieurs : évite un écran interminable à faire défiler quand il y a beaucoup de catégories et de compétiteurs. La catégorie active est dérivée de l’horloge murale (time.time() // AFFICHAGE_ROTATION_SECONDES), pas d’un compteur local propre à chaque page – même principe que le diaporama de fletchtime/web/display.html (currentSlideshowStep()) : deux écrans (ex. un téléphone en support et un grand écran) qui chargent à des instants différents affichent quand même la même catégorie au même moment, sans le moindre JS ni synchronisation explicite entre eux – juste le rechargement automatique déjà en place (RAFRAICHISSEMENT_SECONDES) qui, de temps en temps, retombe sur une catégorie différente.

fletchscore.api.competiteur.page_aide(lang: str = 'fr', theme: str = 'dark') str

Page d’aide pour le compétiteur – pas de contenu technique (installation, config…), contrairement au manuel FletchTime : le public ici n’installe jamais rien, il ouvre juste un lien reçu de l’organisateur. Rendue côté serveur comme le reste de la vue compétiteur (pas de JS – voir docstring du module), contrairement à manual.html côté FletchTime qui est une page statique avec i18n JS.

fletchscore.api.competiteur.page_mes_donnees(conn: Connection, id_federal: str, lang: str = 'fr', theme: str = 'dark') str

Droit d’accès RGPD (issue #38) – récapitulatif de toutes les données personnelles détenues sur ce compétiteur, toutes compétitions confondues (contrairement à page_mes_messages, scopée à la compétition de la session – voir la docstring de services.rassembler_donnees_personnelles). Le lien de téléchargement pointe vers /mes-donnees/export.json, qui réutilise les mêmes données (voir services.donnees_personnelles_en_dict).

fletchscore.api.competiteur.page_procuration(conn: Connection, competition_id: str, lang: str = 'fr', theme: str = 'dark', recherche: str = '', identite: tuple[str, str] | None = None) str

Recherche + formulaire de demande de procuration – réservée à un compétiteur déjà identifié pour cette compétition (voir _lire_identite) : impossible de savoir pour quel mandataire demander sans ça. Pas de rechargement automatique, même raison que page_rattachement.

fletchscore.api.competiteur.page_rattachement(conn: Connection, competition_id: str, lang: str = 'fr', theme: str = 'dark', recherche: str = '', identite: tuple[str, str] | None = None) str

Recherche + formulaire de demande de rattachement – pas de rechargement automatique ici (contrairement aux pages de classement) : un compétiteur en train de chercher son nom ou de remplir le champ ne doit pas se faire couper par un rafraîchissement intempestif.

Sécurité

Mot de passe organisateur – protège l’accès au poste organisateur.

Optionnel : si config/auth.toml n’existe pas, aucun mot de passe n’est demandé (comportement historique, rien ne casse pour qui ne veut pas de ce réglage). Fichier local, jamais versionné (voir .gitignore) – il contient un vrai secret, même haché.

Hachage via hashlib.pbkdf2_hmac (stdlib) plutôt que bcrypt/argon2 : aucune dépendance compilée à faire fonctionner sur Pydroid 3 (voir CLAUDE.md), et PBKDF2-SHA256 avec un nombre d’itérations suffisant reste un choix raisonnable pour un mot de passe local protégeant un poste déjà physiquement contrôlé – pas un service exposé sur internet.

fletchscore.auth.definir_mot_de_passe(mot_de_passe: str, chemin: Path | str = PosixPath('config/auth.toml')) None

Définit (ou remplace) le mot de passe organisateur.

fletchscore.auth.mot_de_passe_defini(chemin: Path | str = PosixPath('config/auth.toml')) bool

True si un mot de passe organisateur a été configuré – False signifie “pas de protection”, pas une erreur.

fletchscore.auth.supprimer_mot_de_passe(chemin: Path | str = PosixPath('config/auth.toml')) None

Désactive la protection – ne lève pas d’erreur si aucun mot de passe n’était configuré (l’effet recherché est déjà atteint).

fletchscore.auth.verifier_mot_de_passe(mot_de_passe: str, chemin: Path | str = PosixPath('config/auth.toml')) bool

Vérifie le mot de passe présenté – False si aucun mot de passe n’est configuré (rien à comparer) autant que si celui présenté est incorrect : à l’appelant de distinguer les deux cas via mot_de_passe_defini s’il en a besoin.

Clé secrète serveur – signe les tokens compétiteur (HMAC).

Stockée dans un fichier séparé de la base SQLite (config/cle_secrete.txt, gitignoré comme le reste de config/) plutôt que dans la base elle-même : quelqu’un qui ne récupère que le fichier .db (une sauvegarde égarée, une copie du dossier du club…) ne peut pas reconstituer un token valide sans cette clé, qui vit ailleurs.

fletchscore.securite.obtenir_cle_secrete(chemin: Path | str = PosixPath('config/cle_secrete.txt')) bytes

Charge la clé existante, ou en génère une nouvelle (32 octets aléatoires, secrets.token_bytes – cryptographiquement sûr) au tout premier appel sur une installation donnée.

Certificat auto-signé pour le serveur HTTPS local (v0.3).

⚠️ Non exécuté dans l’environnement de développement utilisé ici : cryptography n’est pas installable ici (pas d’accès réseau), même situation que fpdf2/qrcode – voir CLAUDE.md.

Certificat et clé privée générés une seule fois (au premier lancement en HTTPS), puis réutilisés – jamais versionnés (voir .gitignore). Auto-signé : le navigateur du compétiteur affichera un avertissement “connexion non sécurisée” à accepter manuellement une fois – normal et attendu pour un certificat qui ne provient pas d’une autorité reconnue, voir docs/guide-utilisateur/depannage.rst.

fletchscore.certificat_https.generer_certificat(chemin_cert: Path | str = PosixPath('config/certificat_https.pem'), chemin_cle: Path | str = PosixPath('config/certificat_https_cle.pem')) None

Génère un certificat auto-signé (RSA 2048, SHA-256) et sa clé privée, écrits en PEM. Lève ImportError si cryptography n’est pas installé – à l’appelant de le gérer proprement (voir api/competiteur.creer_serveur).

fletchscore.certificat_https.obtenir_certificat(chemin_cert: Path | str = PosixPath('config/certificat_https.pem'), chemin_cle: Path | str = PosixPath('config/certificat_https_cle.pem')) tuple[Path, Path]

Retourne (chemin_cert, chemin_cle) – génère le certificat au besoin s’il n’existe pas encore.

Limitation de débit – protège les points d’entrée sensibles de la vue compétiteur (v0.3), en particulier POST /code : sans limite, un code à 6 caractères (~30 bits, voir services.verifier_code_court) deviendrait devinable par force brute en l’essayant en boucle.

Fenêtre glissante en mémoire, pas en base – volontairement : un redémarrage du serveur remet les compteurs à zéro, ce qui est acceptable (le serveur tourne le temps d’une compétition, pas en permanence), et évite d’écrire à chaque requête dans la base SQLite pour un simple compteur éphémère.

Import / export

Import et export des référentiels clubs.csv et competiteurs.csv.

Règle centrale pour l’import (voir docs/cahier-des-charges/modele-donnees.rst §6.1) : si un code_club ou code_style référencé n’existe pas dans son référentiel, la ligne est REJETÉE avec un message explicite – jamais de création automatique silencieuse, pour éviter les doublons du type “ALFP” / “Archers Libres FP”.

Les fonctions d’export utilisent exactement le même format de colonnes que l’import correspondant – un fichier exporté ici se réimporte tel quel (round-trip garanti), utile pour sauvegarder, partager avec un autre club, ou corriger dans un tableur puis réimporter.

Toutes les fonctions acceptent soit un chemin de fichier (str), soit un objet texte déjà ouvert (io.StringIO en test, un fichier uploadé…) – ça évite de dépendre du système de fichiers réel dans les tests unitaires.

class fletchscore.io.import_csv.ErreurImport(numero_ligne: 'int', message: 'str')

Bases: object

numero_ligne: int

Numéro de ligne dans le fichier source, en-tête comprise (la première ligne de données est donc la ligne 2) – pour que le message corresponde à ce que l’organisateur voit s’il ouvre le fichier dans un tableur.

class fletchscore.io.import_csv.RapportImport(lignes_traitees: 'int' = 0, importees: 'int' = 0, ignorees: 'int' = 0, erreurs: 'list[ErreurImport]' = <factory>)

Bases: object

ignorees: int

Lignes valides mais déjà présentes en base (ex. club déjà importé lors d’une session précédente) – pas une erreur, juste un import idempotent.

fletchscore.io.import_csv.exporter_clubs_csv(clubs: list[Club], destination: str | TextIO) None

Exporte les clubs au même format que celui attendu par import_clubs – un fichier exporté ici se réimporte tel quel, ailleurs ou après correction dans un tableur.

fletchscore.io.import_csv.exporter_competiteurs_csv(competiteurs: list[Competiteur], destination: str | TextIO) None

Exporte les compétiteurs au même format que celui attendu par import_competiteurs – même principe que exporter_clubs_csv.

fletchscore.io.import_csv.formater_rapport(rapport: RapportImport) str

Résumé lisible d’un RapportImport, pensé pour être affiché tel quel à l’organisateur (GUI ou sortie CLI) – vit ici plutôt que dans un module gui/ pour rester testable sans customtkinter.

fletchscore.io.import_csv.import_clubs(conn: Connection, source: str | TextIO) RapportImport

Importe clubs.csv – colonnes attendues : code_club, nom, ville (ville optionnelle).

fletchscore.io.import_csv.import_competiteurs(conn: Connection, source: str | TextIO) RapportImport

Importe competiteurs.csv – colonnes attendues : id_federal, nom, prenom, code_club, sexe, date_naissance, code_style (licence_valide_jusqu_au optionnelle).

code_club et code_style doivent déjà exister dans leurs référentiels respectifs – une ligne qui en référence un absent est rejetée, pas corrigée automatiquement (voir docstring du module).

Export CSV du classement (résultats complets ou podiums).

Format brut de secours – voir docs/cahier-des-charges/modele-donnees.rst §6.2. Fonctions pures sur un classement déjà calculé (voir services.classement_epreuve), sans dépendance au stockage.

fletchscore.io.export.csv.exporter_classement_csv(classement: dict[str, list[LigneClassement]], destination: str | TextIO) None

Écrit le classement complet, une ligne par compétiteur classé, catégories triées alphabétiquement puis par rang – ordre stable et reproductible, pas l’ordre d’insertion du dict.

fletchscore.io.export.csv.exporter_classement_global_csv(epreuves: list[Epreuve], classement: dict[str, list[LigneClassementGlobal]], destination: str | TextIO) None

Écrit le classement cumulé d’une compétition – une colonne par épreuve (nom + date, pour rester unique même si deux épreuves portent le même nom), plus une colonne total.

epreuves doit être la liste retournée par services.classement_global_competition() – l’ordre des colonnes suit son ordre.

Export Excel du classement – une feuille, groupée par catégorie.

Format destiné à la fédération (voir docs/cahier-des-charges/modele-donnees.rst §6.2) – le format exact imposé ou non par la FFTL reste un point ouvert (voir docs/roadmap.md) ; en attendant, ce tableau reprend les mêmes colonnes que l’export CSV.

fletchscore.io.export.excel.exporter_classement_excel(classement: dict[str, list[LigneClassement]], destination: str | BinaryIO, titre_feuille: str = 'Classement') None

Écrit le classement dans un classeur Excel (.xlsx), une section par catégorie (triées alphabétiquement) sur une seule feuille.

fletchscore.io.export.excel.exporter_classement_global_excel(epreuves: list[Epreuve], classement: dict[str, list[LigneClassementGlobal]], destination: str | BinaryIO, titre_feuille: str = 'Classement') None

Écrit le classement cumulé d’une compétition – une colonne par épreuve, plus une colonne total (voir io.export.csv.exporter_classement_global_csv pour le détail du format, même principe ici en Excel).

Export PDF du classement – tableau par catégorie, une page continue.

Utilise fpdf2 (voir pyproject.toml) – choisi pour rester pur Python (pas de dépendance C, plus sûr sur Pydroid/Android) et pour la simplicité de l’API sur un besoin volontairement simple : un tableau, pas une mise en page élaborée.

⚠️ Non exécuté dans l’environnement de développement : fpdf2 n’est pas installable ici (pas d’accès réseau). Le code est écrit avec soin à partir de l’API connue de fpdf2, mais n’a pas pu être vérifié par un vrai test – à confirmer par la CI ou en le lançant côté utilisateur (voir CLAUDE.md).

fletchscore.io.export.pdf.exporter_classement_global_pdf(epreuves: list[Epreuve], classement: dict[str, list[LigneClassementGlobal]], destination: str | BinaryIO, titre: str = 'Classement') None

Écrit le classement cumulé d’une compétition en PDF – une colonne par épreuve, plus une colonne total.

Page en paysage plutôt qu’en portrait (contrairement à exporter_classement_pdf) : le nombre de colonnes dépend du nombre d’épreuves, le paysage laisse plus de place avant que ça ne devienne illisible. Au-delà d’une poignée d’épreuves, les colonnes se resserrent (largeur plancher 20mm) – pas de retour à la ligne ni de rotation de texte, une compétition avec beaucoup d’épreuves restera plus lisible en CSV/Excel.

fletchscore.io.export.pdf.exporter_classement_pdf(classement: dict[str, list[LigneClassement]], destination: str | BinaryIO, titre: str = 'Classement') None

Écrit le classement en PDF, un tableau par catégorie (triées alphabétiquement), dans l’ordre de rang déjà calculé par scoring.classement_par_categorie.

Scoring et référentiels

Classement par catégorie et départage au X.

Fonctions pures : reçoivent des objets déjà chargés (Competiteur, Score), sans dépendance au stockage ni à la GUI – voir docs/cahier-des-charges/architecture.rst.

class fletchscore.scoring.classement.LigneClassement(competiteur: 'Competiteur', code_categorie: 'str', total: 'int', nombre_x: 'int', rang: 'int' = 0)

Bases: object

rang: int

Rang au sein de sa catégorie – calculé par classement_par_categorie(), pas par l’appelant. Deux compétiteurs à égalité totale (et à égalité de X si le barème en tient compte) partagent le même rang ; le rang suivant saute en conséquence (1, 2, 2, 4 – convention sportive standard). Une égalité qui subsiste à ce stade doit être départagée sous supervision de l’organisateur, voir docs/cahier-des-charges/regles-metier.rst §4.3 – ce module n’invente pas de critère supplémentaire.

class fletchscore.scoring.classement.LigneClassementGlobal(competiteur: 'Competiteur', code_categorie: 'str', totaux_par_epreuve: 'dict[str, int]', total_global: 'int', nombre_x_global: 'int', rang: 'int' = 0)

Bases: object

rang: int

Rang au sein de sa catégorie, sur le total global uniquement – voir classement_global().

totaux_par_epreuve: dict[str, int]

Total de chaque épreuve, indexé par epreuve_id – 0 si le compétiteur n’y était pas inscrit ou n’y a pas de score validé.

fletchscore.scoring.classement.classement_global(date_reference: date, epreuve_ids: list[str], entrees: list[tuple[Competiteur, dict[str, Score | None]]], *, categories_veteran_actives: bool = False) dict[str, list[LigneClassementGlobal]]

Classement cumulé sur plusieurs épreuves d’une même compétition – un total par épreuve, plus un total global qui sert seul de critère de tri.

Volontairement pas de départage au X ici : les épreuves d’une même compétition peuvent utiliser des barèmes différents (certains avec zone X, d’autres non), un critère uniforme n’aurait pas de sens garanti – contrairement à classement_par_categorie(), qui connaît le barème d’une seule épreuve et peut s’y fier.

entrees associe chaque compétiteur à un dict {epreuve_id: Score ou None} – une entrée manquante pour une épreuve compte pour 0, pas une erreur (un compétiteur peut ne pas être inscrit à toutes les épreuves de la compétition).

fletchscore.scoring.classement.classement_par_categorie(bareme: Bareme, date_reference: date, entrees: list[tuple[Competiteur, Score | None]], *, categories_veteran_actives: bool = False) dict[str, list[LigneClassement]]

Construit le classement, groupé par code de catégorie combiné (ex. AMBB-R), trié par total décroissant puis, si le barème utilise un départage au X (bareme.departage_par_x), par nombre de X décroissant.

entrees associe chaque compétiteur à son score final (au plus un par inscription – voir models/score.py) ou None s’il n’a pas encore été saisi.

fletchscore.scoring.classement.podium_par_categorie(classement: dict[str, list[LigneClassement]], taille: int = 3) dict[str, list[LigneClassement]]

Extrait le podium (par défaut top 3) de chaque catégorie d’un classement déjà calculé.

Filtre sur le rang (ligne.rang <= taille), pas sur la position dans la liste : si deux personnes sont ex-aequo au rang 1, les DEUX sont sur le podium, comme au rang 2 il n’y en aura donc aucune – cohérent avec la convention 1, 2, 2, 4 déjà utilisée pour l’attribution des rangs. Une catégorie avec moins de compétiteurs que taille retourne simplement tout le monde.

fletchscore.scoring.classement.total_scores(score: Score | None) tuple[int, int]

Total de points et nombre de X, en ne comptant QUE si le score est validé – une proposition en attente ne doit jamais influencer un classement officiel (voir docs/cahier-des-charges/securite.rst §7.2). Un compétiteur sans score saisi (None) compte pour 0.

Référentiel Style – lecture et extension locale.

La base des 12 codes IFAA est pré-remplie via fletchscore.storage.db.seed_referentiel_styles (idempotent, appelée au démarrage). Ce module ne gère que ce qui vient éventuellement s’ajouter par-dessus : une variante FFTL locale non couverte par le règlement IFAA – point ouvert du cahier des charges, non tranché à ce stade (voir docs/roadmap.md).

fletchscore.referentiels.styles.ajouter_variante_style(conn: Connection, code: str, libelle: str, libelle_en: str = '') None

Ajoute une variante de style non couverte par les 12 codes IFAA.

Refuse explicitement d’écraser un code déjà existant (IFAA ou variante précédemment ajoutée) – une variante locale doit avoir un code qui lui est propre, jamais réutiliser un code IFAA existant pour éviter toute ambiguïté dans les classements par catégorie.

fletchscore.referentiels.styles.styles_disponibles(conn: Connection) list[Style]

Styles IFAA + variantes locales ajoutées par le club, triés par code.