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 :
StrEnumVisual/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 :
objectImmutable 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 :
objectOne timed segment of a match sequence.
durationis normally a number of seconds. It can also beNone, 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: oncedurationseconds 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 :
ABCBuilds the full ordered list of
Stepfor one competition round. Modes are stateless sequence generators; all runtime state (current position, elapsed time, pause/emergency) lives infletchtime.engine.engine.MatchEngine.
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)
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)
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é:
- 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é:
- 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
overridesby 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é:
- fletchtime.server.config_store.save_flint_config(overrides)[source]¶
Validates
overridesby constructing a real FlintConfig (raises ValueError if inconsistent) before writing anything to disk.- Paramètres:
overrides (dict[str, Any])
- Type renvoyé:
- 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.replaceest 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
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_dirdefaults to<directory>/assetswhen not given, matching the historical single-root behaviour (dev checkout, or a PyInstaller build where everything sits together next to the exe).ws_portdefaults toport + 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 (seefletchtime.runtime.ServerRuntime).match_server, if given, is exposed read-only via/api/status(see_DualRootHandler._build_status_body) –Noneby default, in which case that endpoint reports{"available": false}rather than erroring, since not every caller needs/has one to share (e.g. a barestart_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, seesocketserver.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 :
objectNot 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