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.
Nonepour 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 uneProcurationvalidé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:
StrEnumDivisions 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:
objectToutes 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:
ExceptionErreur 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:
objectUne 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_competiteurpour le détail de ce qui est conservé/supprimé). Le nom devientCompé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_activesest 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_globalles 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 (voirsaisir_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.rstpromettait déjà (“expiration automatique à la clôture”). Réversible viarouvrir_competition; les accès révoqués ici ne le sont pas automatiquement à la réouverture (une révocation reste un choix explicite, voirrevoquer_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 quecreer_epreuve_depuis_template.Chaque épreuve générée prend
date_debutcomme date par défaut – un modèle de compétition ne porte aucune date (voir docstring deCompetitionTemplate). Si les épreuves ne tombent pas toutes le même jour, l’organisateur ajuste ensuite individuellement viamodifier_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 quecreer_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 quedemander_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_federalfourni) 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
Tokenpersisté 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_anneespar 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 == "", voiranonymiser_competiteur, #37) – leurs données identifiantes ont déjà disparu, rien de plus à purger.date_referenceest un paramètre explicite (jamaisdate.today()en interne) pour rester testable de façon déterministe – même principe quecompetiteur.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_clubn’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_federaln’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 (voirstorage.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_cibleomis), ou pour quelqu’un d’autre via uneProcurationdéjà validée par l’organisateur (id_federal_ciblefourni). Mêmes bornes que la saisie organisateur (saisir_score_final), statutPROPOSE: n’apparaît dans aucun classement tant qu’un organisateur ne l’a pas validé (valider_score_propose), voirscoring.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_federalvé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 (voirproposer_score) – laissé àNonepour 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_proposepassent 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_competitionne 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_epreuvesur 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àVALIDEsans 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,Nonesinon (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_courtpuis comparaison du HMAC du secret présenté – jamais une comparaison directe (le secret n’est jamais stocké en clair).hmac.compare_digestplutô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
Noneaussi 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 –
Nones’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 (voirsupprimer_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 :
_SCHEMAcrée déjà tout dans son état le plus récent) d’une base préexistante créée avant ce mécanisme (scoresdéjà là mais passchema_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_federalcorrespond) 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_competitionviaepreuve_a_des_scoressur 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_epreuveviaepreuve_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 quesupprimer_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_inscriptionviaget_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_clubn’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_federaln’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.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:
HTTPServerServeur 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.1si ç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=0laisse l’OS choisir un port libre (consulter ensuiteserveur.server_port).https=Trueenveloppe le socket dans TLS avec un certificat auto-signé, généré au besoin (voircertificat_https). Le socket est déjà lié (server_bind/server_activate, faits parHTTPServer.__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 quepage_competition(voir docstring en tête de module).rotation_secondes: durée d’affichage d’une catégorie, réglable par écran via?rotation=Ndans l’URL (voirdo_GET) – utile si un grand écran doit rester plus longtemps sur chaque catégorie qu’un téléphone en support.Noneou une valeur invalide (pas un entier positif) retombe surAFFICHAGE_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 defletchtime/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 deservices.rassembler_donnees_personnelles). Le lien de téléchargement pointe vers/mes-donnees/export.json, qui réutilise les mêmes données (voirservices.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 quepage_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_definis’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
ImportErrorsicryptographyn’est pas installé – à l’appelant de le gérer proprement (voirapi/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 queexporter_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.
epreuvesdoit être la liste retournée parservices.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_csvpour 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.entreesassocie 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.entreesassocie chaque compétiteur à son score final (au plus un par inscription – voir models/score.py) ouNones’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 quetailleretourne 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.