steam-backlog-enforcer/steam_backlog_enforcer/_web_models.py
Krzysztof kuhy Rudnicki 2ff39edca4 Bring _web_dataset.py under the 250-line cap — cap campaign complete
_web_dataset.py  436 -> 178  + _web_models, _web_games, _web_pace

The six dataclasses (WebGame, WebStateInfo, WebDefaults, DefaultSummary,
PaceVsHLTB, WebDataset) are used by BOTH the parent and the helpers, so they
go to their own leaf model module that both import. Leaving them in the
parent is what created a circular import and broke ./run.sh on the previous
attempt.

All public names plus the seven private helpers tests reach for are
re-exported from _web_dataset via __all__, so import paths are unchanged.
Test patch targets split per class: TestBuildWebDataset, TestDatasetToPayload
and TestDatasetEndpoint still patch load_snapshot on _web_dataset (that
function did not move) while their cache patches move to _web_games.

`bash scripts/check_file_length.sh --all` now exits 0 — no file in the repo
is over 250 lines. 1344 tests, 100.00% coverage. Verified live: ./run.sh
status works and build_web_dataset projects all 958 games with pace and
summary sections intact; service at 0 restarts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A1EYiv7e7HGhET84oa9M3
2026-08-24 16:08:12 +02:00

111 lines
2.9 KiB
Python

"""Dataclasses for the web UI's read-only dataset projection.
A leaf module: :mod:`steam_backlog_enforcer._web_dataset` and its helper
modules all import these types, so nothing here may import them back.
"""
from __future__ import annotations
from dataclasses import dataclass, field
# Mirrors ``_stats._HOURS_PER_DAY_PRESETS`` but mutable/JSON-friendly.
HOURS_PER_DAY_PRESETS = [2.0, 4.0, 6.0, 8.0]
@dataclass
class WebGame:
"""One incomplete candidate game, with raw filterable fields.
Hour fields use ``-1`` to mean "no data" (matching the cache convention),
so the client can choose to include or exclude unknown-length games.
"""
app_id: int
name: str
completion_pct: float
playtime_minutes: int
rush_hours: float
leisure_hours: float
worst_hours: float
count_comp: int
comp_100_count: int
hltb_game_id: int
protondb_tier: str
protondb_trending_tier: str
protondb_score: float
@dataclass
class WebStateInfo:
"""Pace inputs and current-assignment metadata for the UI."""
current_app_id: int | None
current_game_name: str
games_done: int
games_done_since_start: int
days_elapsed: int
enforcement_started_at: str
pace_games_per_day: float
@dataclass
class WebDefaults:
"""The CLI's hardcoded filter thresholds, surfaced as editable defaults."""
min_comp_100_polls: int
min_count_comp: int
min_confidence_sum: int
min_playable_tier: str
hours_per_day_presets: list[float]
@dataclass
class DefaultSummary:
"""Totals the CLI ``stats`` command would print at default thresholds.
Used as a parity oracle: the client's own default-filtered totals must
reproduce these numbers.
"""
qualifying: int
rush_total: float
leisure_total: float
worst_total: float
@dataclass
class PaceVsHLTB:
"""Player pace calibrated against HLTB rush/leisure averages.
Derived from completed games that have HLTB detail data. All ratio /
interpolation fields use ``-1`` to mean "insufficient data", matching the
cache convention used elsewhere.
Fields:
calibration_count: number of completed games used for calibration.
ratio_vs_rush: actual_hours / rush_hours across calibration games.
ratio_vs_leisure: actual_hours / leisure_hours (-1 if no leisure data).
interpolation_t: position between rush (0.0) and leisure (1.0) speed.
Negative means faster than rush; >1 means slower than leisure.
-1 means insufficient data.
player_style: human-readable style label.
"""
calibration_count: int
ratio_vs_rush: float
ratio_vs_leisure: float
interpolation_t: float
player_style: str
@dataclass
class WebDataset:
"""Full payload served to the browser."""
games: list[WebGame]
state: WebStateInfo
defaults: WebDefaults
default_summary: DefaultSummary
pace_vs_hltb: PaceVsHLTB | None
generated_at: str = field(default="")