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 de l’architecture, voir Architecture technique.

fletchtime.engine – moteur de séquencement

Pur Python, aucune dépendance – voir Guide développeur pour comment y ajouter un nouveau mode de tir.

Core data model shared by every shooting mode and by the match engine.

Deliberately dependency-free (stdlib only) so this package can be tested and reused without pulling in FastAPI/websockets: the engine only produces MatchState snapshots, it says nothing about how they are transported.

class fletchtime.engine.models.Phase(*values)[source]

Bases : StrEnum

Visual/safety phase of the current step.

  • WAIT – before the match starts, or between matches. No countdown.

  • RED – preparation time (archers approach / take position).

  • GREEN – main shooting time.

  • ORANGE – warning period near the end of shooting time.

  • PAUSE – end of a volée: archers retrieve arrows, no countdown. The engine waits here indefinitely until the DOS manually starts the next volée (MatchEngine.next()).

  • EMERGENCY – danger signal, clock frozen, must be explicitly resumed.

  • FINISHED – sequence exhausted, nothing left to shoot.

class fletchtime.engine.models.MatchState(phase=Phase.WAIT, time_left=0.0, current_turn='', end_number=0, total_ends=0, unit_number=1, arrow_in_end=0, total_arrows_in_end=0, distance_label='', target_image='', target_image_2='', finished=False, orange_threshold=None)[source]

Bases : object

Immutable snapshot of where the match currently stands.

This is the object a transport layer (FastAPI/WebSocket) would serialize and push to display screens – see docs/architecture.md.

Paramètres:
  • phase (Phase)

  • time_left (float)

  • current_turn (str)

  • end_number (int)

  • total_ends (int)

  • unit_number (int)

  • arrow_in_end (int)

  • total_arrows_in_end (int)

  • distance_label (str)

  • target_image (str)

  • target_image_2 (str)

  • finished (bool)

  • orange_threshold (float | None)

A Step is one timed segment of a competition (e.g. « red light, 10s, end 3 of 12, turn A-B, distance 18m »). A shooting mode’s job is only to produce an ordered list of Step objects up front; the engine then plays that list back, handling ticking, manual advance and emergency stop.

This keeps modes simple, declarative, and trivial to unit test: you can assert on the exact list of steps a config produces without running any timer at all.

class fletchtime.engine.sequence.Step(phase, duration, current_turn='', end_number=0, total_ends=0, unit_number=1, arrow_in_end=0, total_arrows_in_end=0, distance_label='', target_image='', target_image_2='', sound_event=None, orange_threshold=None, orange_sound_event=None)[source]

Bases : object

One timed segment of a match sequence.

duration is normally a number of seconds. It can also be None, meaning « wait here indefinitely until the DOS presses next » – used for the PAUSE step inserted between volées, where archers retrieve arrows at their own pace.

A GREEN step can carry an orange_threshold: once duration seconds have counted down to that many seconds remaining, the engine displays the step as ORANGE, but it stays the same step – the countdown never resets or jumps, it’s one continuous timer that just changes colour near the end (e.g. 240s total, orange in the last 30s).

Paramètres:
  • phase (Phase)

  • duration (float | None)

  • current_turn (str)

  • end_number (int)

  • total_ends (int)

  • unit_number (int)

  • arrow_in_end (int)

  • total_arrows_in_end (int)

  • distance_label (str)

  • target_image (str)

  • target_image_2 (str)

  • sound_event (str | None)

  • orange_threshold (float | None)

  • orange_sound_event (str | None)

Plays back the ordered list of Step produced by a ShootingMode, exposing the controls a DOS (Director Of Shooting) needs: regular ticking, manual advance, stop, restart, jump to a specific end, emergency stop/resume, temporary pause.

This is intentionally the only stateful class in the engine package – modes themselves stay pure/stateless so they are trivial to unit test in isolation (see tests/test_indoor_mode.py, tests/test_flint_mode.py).

Common interface for shooting modes.

Unlike the legacy ArcheryClock approach (a single 12k-line file dispatching on a string like archerysystem = 'fita' in dozens of scattered if branches), each mode here is a self-contained class that only knows how to build its own sequence of steps. Adding a new mode never touches existing ones – see docs/dev-guide for the walkthrough.

class fletchtime.engine.modes.base.ShootingMode[source]

Bases : ABC

Builds the full ordered list of Step for one competition round. Modes are stateless sequence generators; all runtime state (current position, elapsed time, pause/emergency) lives in fletchtime.engine.engine.MatchEngine.

abstractmethod build_sequence()[source]

Return the ordered, non-empty list of steps for this round.

Type renvoyé:

list[Step]

Indoor round: fixed number of series, each made of several ends of a fixed number of arrows, all shot at a single distance. Matches the club’s concours format (« 2 séries de 6 volées de 5 flèches ») but every number is configurable.

A-B / C-D are relays within the same volée, not separate ends: when a target is shared, one pair shoots first, then the other – the volée number does not change between them, only once all configured relays for that end have shot does the engine move to the next volée. Whether a match uses both relays (and in which order) or just one is a per-match setting (turn_mode), fixed before the match starts and never toggled mid-match.

The club’s actual competition alternates which relay leads from one series to the next (série 1 : A-B puis C-D – série 2 : C-D puis A-B) – this is alternate_relay_order_each_series, on by default.

Séries et volées are tracked as separate numbers on every step, matching FlintMode’s model: unit_number is the série (1, 2, …) and end_number/total_ends are the volée within that série (e.g. 1-6), not a running total across the whole match.

class fletchtime.engine.modes.indoor.IndoorConfig(series: 'int' = 2, ends_per_series: 'int' = 6, arrows_per_end: 'int' = 5, prep_time: 'float' = 10.0, shoot_time: 'float' = 240.0, orange_warning_time: 'float' = 30.0, distance_label: 'str' = '20 yards', target_image_recurve: 'str' = 'assets/targets/indoor_recurve.svg', target_image_compound: 'str' = 'assets/targets/indoor_compound.svg', turn_mode: 'str' = 'ab_then_cd', alternate_relay_order_each_series: 'bool' = True)[source]

Bases : object

Paramètres:
  • series (int)

  • ends_per_series (int)

  • arrows_per_end (int)

  • prep_time (float)

  • shoot_time (float)

  • orange_warning_time (float)

  • distance_label (str)

  • target_image_recurve (str)

  • target_image_compound (str)

  • turn_mode (str)

  • alternate_relay_order_each_series (bool)

class fletchtime.engine.modes.indoor.IndoorMode(config=None)[source]

Bases : ShootingMode

Paramètres:

config (IndoorConfig | None)

build_sequence()[source]

Return the ordered, non-empty list of steps for this round.

Type renvoyé:

list[Step]

Flint round (FFTL): each « unité standard » is 6 standard ends (4 arrows, one fixed distance, ~3 minutes) followed by one « walk-up » end (4 arrows, 4 different distances, 45 seconds per arrow, the whole group advancing together between arrows). A « parcours » is units unités standards (2 for the club’s competition).

A-B / C-D relays: unlike Indoor (where both relays share a target boss and can swap within the same volée), Flint’s field-style single-lane course means swapping relays mid-volée isn’t practical or safe – there’s no room for two target faces per distance, and moving people between shooting positions repeatedly would be dangerous. So here, a relay shoots an entire unité standard (all 7 volées) before the other relay repeats that same unité from its own start. Which relays are used, and in which order, is fixed before the match starts (turn_mode) and never changed mid-match.

See docs/specifications.md for the full rule text this is derived from.

class fletchtime.engine.modes.flint.FlintConfig(units: 'int' = 2, standard_ends_per_unit: 'int' = 6, arrows_per_standard_end: 'int' = 4, standard_prep_time: 'float' = 10.0, standard_shoot_time: 'float' = 180.0, standard_orange_warning_time: 'float' = 20.0, standard_distances: 'list[str]' = <factory>, standard_target_image_1spot: 'str' = 'assets/targets/flint_35cm_1spot.svg', standard_target_image_4spot: 'str' = 'assets/targets/flint_20cm_4spot.svg', walkup_arrows: 'int' = 4, walkup_time_per_arrow: 'float' = 45.0, walkup_prep_time: 'float' = 10.0, walkup_orange_warning_time: 'float' = 10.0, walkup_distances: 'list[str]' = <factory>, walkup_target_image: 'str' = 'assets/targets/flint_35cm_1spot.svg', turn_mode: 'str' = 'ab_then_cd', alternate_relay_order_each_unit: 'bool' = True)[source]

Bases : object

Paramètres:
  • units (int)

  • standard_ends_per_unit (int)

  • arrows_per_standard_end (int)

  • standard_prep_time (float)

  • standard_shoot_time (float)

  • standard_orange_warning_time (float)

  • standard_distances (list[str])

  • standard_target_image_1spot (str)

  • standard_target_image_4spot (str)

  • walkup_arrows (int)

  • walkup_time_per_arrow (float)

  • walkup_prep_time (float)

  • walkup_orange_warning_time (float)

  • walkup_distances (list[str])

  • walkup_target_image (str)

  • turn_mode (str)

  • alternate_relay_order_each_unit (bool)

class fletchtime.engine.modes.flint.FlintMode(config=None)[source]

Bases : ShootingMode

Paramètres:

config (FlintConfig | None)

build_sequence()[source]

Return the ordered, non-empty list of steps for this round.

Type renvoyé:

list[Step]

Shared A-B/C-D relay vocabulary, used by both IndoorMode and FlintMode.

A « relay » is a group of archers (A-B or C-D) sharing the same physical shooting position. Which relays are used, and in which order, is a per-match setting chosen before the match starts and never changed mid-match.

fletchtime.server – serveur temps réel

Construit par-dessus le moteur sans jamais le modifier – voir Architecture technique pour le modèle de communication (HTTP + WebSocket sur deux ports séparés).

In-process state shared by every websocket connection: owns the (optional) running MatchEngine, ticks it on a timer, and broadcasts its state to every connected client (control page and every display page).

Deliberately dependency-free beyond the engine + stdlib json/asyncio, so this stays easy to reason about on a phone.

Charge et sauvegarde la configuration Indoor/Flint depuis des fichiers TOML lisibles par un humain, pour qu’un bénévole du club puisse ajuster les temps, distances et images de cible sans toucher au code Python.

Lecture : tomllib, dans la bibliothèque standard depuis Python 3.11 – aucune dépendance externe, ce qui compte pour la compatibilité Pydroid. Écriture : TOML n’a pas de support d’écriture en stdlib ; plutôt que d’ajouter une dépendance tierce pour ça, on utilise un petit sérialiseur maison, largement suffisant pour les types simples qu’on manipule ici (chaînes, nombres, booléens, listes de chaînes).

fletchtime.server.config_store.load_indoor_config()[source]

Reads config/indoor.toml, falling back to IndoorConfig’s own defaults for any missing file or missing field.

Type renvoyé:

IndoorConfig

fletchtime.server.config_store.load_flint_config()[source]

Reads config/flint.toml, falling back to FlintConfig’s own defaults for any missing file or missing field.

Type renvoyé:

FlintConfig

fletchtime.server.config_store.load_app_config()[source]

App-wide settings that aren’t specific to Indoor or Flint (currently just the active sound pack). Not a dataclass – there’s only one field today and it may grow, so a plain dict merged over sensible defaults is simpler than a formal schema for now.

Type renvoyé:

dict[str, Any]

fletchtime.server.config_store.load_gui_config()[source]

Préférences de la fenêtre graphique – voir GUI_TOML. Comme load_app_config, un plain dict plutôt qu’une dataclass formelle : un seul champ aujourd’hui, peut grandir.

Type renvoyé:

dict[str, Any]

fletchtime.server.config_store.save_indoor_config(overrides)[source]

Validates overrides by constructing a real IndoorConfig (raises ValueError if inconsistent, e.g. orange_warning_time > shoot_time) before writing anything to disk.

Paramètres:

overrides (dict[str, Any])

Type renvoyé:

IndoorConfig

fletchtime.server.config_store.save_flint_config(overrides)[source]

Validates overrides by constructing a real FlintConfig (raises ValueError if inconsistent) before writing anything to disk.

Paramètres:

overrides (dict[str, Any])

Type renvoyé:

FlintConfig

fletchtime.server.config_store.save_match_snapshot(data)[source]

Écriture atomique (fichier temporaire puis renommage) : ne laisse jamais un fichier à moitié écrit si le processus s’arrête précisément pendant l’écriture – Path.replace est garanti atomique aussi bien sous Linux/macOS que Windows depuis Python 3.3+.

Ceci dit, « atomique » ne veut pas dire « ne peut jamais échouer » : sous Windows, remplacer un fichier peut lever une erreur de « violation de partage » si un autre processus l’a ouvert au même instant (antivirus, surveillance de fichiers d’un IDE, Git Bash…) – constaté en pratique (voir docs/architecture.md, section sur la résilience de la boucle de décompte, appelante de cette fonction à chaque tick). Ce verrou est transitoire par nature, d’où quelques tentatives avant d’abandonner. Échoue silencieusement (juste un avertissement dans le journal) plutôt que de lever : un instantané manqué n’est jamais grave (le prochain tick, ~200ms plus tard, retente), alors qu’une exception qui remonterait perturberait aussi la diffusion de l’état aux écrans pour ce même tick, pour une raison qui n’a rien à voir avec eux.

Paramètres:

data (dict[str, Any])

Type renvoyé:

None

fletchtime.server.config_store.load_match_snapshot()[source]

None si aucun instantané n’existe, ou s’il est corrompu/ illisible – une reprise ratée doit se rabattre silencieusement sur un démarrage normal, jamais faire planter le serveur au lancement.

Type renvoyé:

dict[str, Any] | None

WebSocket endpoint: one connection handler shared by /control and every /display client. Uses only the base websockets.serve + send/recv API (stable across recent library versions), deliberately avoiding the combined HTTP+WS-on-one-port trick whose API has shifted between websockets releases – see docs/architecture.md for why HTTP (static pages) is served separately by fletchtime.server.http_static.

Serves the static pages (control.html, display.html) over plain HTTP, stdlib only – no risk of incompatible native dependencies on Pydroid.

Kept on a separate port from the WebSocket server on purpose: combining HTTP + WS on a single port requires APIs that have shifted across websockets releases, while two plain servers on two ports is boring and guaranteed to work everywhere, including on a phone.

fletchtime.server.http_static.start_http_server(directory, port, assets_dir=None, ws_port=None, match_server=None)[source]

assets_dir defaults to <directory>/assets when not given, matching the historical single-root behaviour (dev checkout, or a PyInstaller build where everything sits together next to the exe).

ws_port defaults to port + 765 (matching the historical 8000/8765 default pair) when not given – only used as a last-resort fallback, callers should normally pass the real configured value (see fletchtime.runtime.ServerRuntime).

match_server, if given, is exposed read-only via /api/status (see _DualRootHandler._build_status_body) – None by default, in which case that endpoint reports {"available": false} rather than erroring, since not every caller needs/has one to share (e.g. a bare start_http_server() call in isolation, as some tests do).

Returns the bound (but not yet serving) server instance – the caller is expected to run .serve_forever() on it (typically in a background thread) and can call .shutdown() from any other thread for a clean stop (standard library guarantee, see socketserver.BaseServer).

Paramètres:
  • directory (str)

  • port (int)

  • assets_dir (str | None)

  • ws_port (int | None)

Type renvoyé:

ThreadingHTTPServer

fletchtime.runtime – cycle de vie des serveurs

Démarrage/arrêt propre des deux serveurs (HTTP + WebSocket), partagé entre le mode graphique et le mode terminal (--headless) – voir Architecture technique, section « Fenêtre graphique et cycle de vie des serveurs ».

Encapsulates the start/stop lifecycle of both servers (HTTP static + WebSocket) as one logical unit. Used by both the plain CLI entry point (fletchtime.__main__) and the graphical one (fletchtime.gui), so neither duplicates this logic nor risks drifting out of sync with the other.

The WebSocket server needs its own asyncio event loop; since a GUI mainloop (tkinter/customtkinter) must own the main thread, that loop runs in a dedicated background thread here – exactly the same shape as the HTTP server, which already ran in a background thread even before this module existed.

class fletchtime.runtime.ServerRuntime(app_web_dir, assets_dir, http_port, ws_port)[source]

Bases : object

Not thread-safe for concurrent start()/stop() calls from multiple threads at once – callers (CLI, GUI) are expected to only ever call these from a single « control » thread (e.g. the GUI’s main thread reacting to button clicks), which is the normal case.

Paramètres:
  • app_web_dir (str)

  • assets_dir (str)

  • http_port (int)

  • ws_port (int)

start()[source]

No-op if already running – safe to call defensively (e.g. a GUI’s « Démarrer » button handler doesn’t need to separately check state first).

Type renvoyé:

None

stop(timeout=5.0)[source]

No-op if not running. Blocks (briefly) until both server threads have actually terminated, so a caller can safely assume the ports are free again once this returns – important for a GUI’s « Démarrer » button right after « Arrêter »: binding to the same port again would otherwise race with the still-shutting-down previous server.

Paramètres:

timeout (float)

Type renvoyé:

None