From 7269b2d68f4a2040bf66959bbcaafc7725f0b16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Hlavat=C3=BD?= Date: Sun, 22 Mar 2026 01:06:12 +0100 Subject: [PATCH] Mypy + ruff --- pyproject.toml | 5 ++++- src/beaky/cli.py | 5 ++--- src/beaky/datamodels/ticket.py | 18 +++++++++--------- src/beaky/link_classifier/classifier.py | 15 ++++++++------- src/beaky/resolvers/resolver.py | 19 ++++++++++--------- src/beaky/screenshotter/screenshotter.py | 5 +++-- 6 files changed, 36 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4c2af6a..b84a1a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,9 @@ dependencies = [ dev = [ "pytest>=9.0.2", "ruff==0.15.5", - "pytz" + "pytz", + "types-requests", + "types-PyYAML", # "playwright==1.58.0" # only dev because it cant be installed in a pipeline, just locally ] @@ -37,6 +39,7 @@ lint.select = ["E", "F", "I"] python_version = "3.12" strict = true ignore_missing_imports = true +plugins = ["pydantic.mypy"] [tool.pytest.ini_options] testpaths = ["test"] diff --git a/src/beaky/cli.py b/src/beaky/cli.py index 654cbcf..279b37e 100644 --- a/src/beaky/cli.py +++ b/src/beaky/cli.py @@ -4,11 +4,10 @@ import yaml from pydantic import ValidationError from beaky.config import Config +from beaky.link_classifier.classifier import LinkClassifier +from beaky.resolvers.resolver import _B, _GRAY, _GREEN, _R, _RED, _YELLOW, TicketResolver, TicketVerdict from beaky.scanner.scanner import Links from beaky.screenshotter.screenshotter import Screenshotter -from beaky.link_classifier.classifier import LinkClassifier -from beaky.resolvers.resolver import TicketResolver -from beaky.resolvers.resolver import TicketVerdict, _R, _B, _GREEN, _RED, _YELLOW, _GRAY _VERDICT_COLOR = { TicketVerdict.TRUTHFUL: _GREEN, diff --git a/src/beaky/datamodels/ticket.py b/src/beaky/datamodels/ticket.py index f2b0ea1..5649477 100644 --- a/src/beaky/datamodels/ticket.py +++ b/src/beaky/datamodels/ticket.py @@ -26,38 +26,38 @@ class Bet(ABC): date: datetime league: str @abstractmethod - def resolve(self): pass + def resolve(self) -> None: pass @dataclass class WinDrawLose(Bet): """Výsledek zápasu 1X2""" betType: Literal["X", "0", "1", "2"] = "0" - def resolve(self): + def resolve(self) -> None: ... @dataclass class Advance(Bet): """What team advances to next round""" - def resolve(self): + def resolve(self) -> None: raise NotImplementedError("Vyser si voko vine") @dataclass class WinDrawLoseDouble(Bet): """Výsledek zápasu - double""" betType: Literal["01", "12", "02"] = "01" - def resolve(self): + def resolve(self) -> None: ... @dataclass class WinLose(Bet): """Výsledek zápasu bez remízy""" betType: Literal["1", "2"] = "1" - def resolve(self): + def resolve(self) -> None: ... @dataclass class BothTeamScored(Bet): - def resolve(self): + def resolve(self) -> None: ... @dataclass @@ -65,7 +65,7 @@ class GoalAmount(Bet): """Počet gólů v zápasu — over/under total goals""" line: float = 0.0 # goal line, e.g. 2.5 over: bool = True # True = more than line, False = less than line - def resolve(self): + def resolve(self) -> None: ... @dataclass @@ -73,14 +73,14 @@ class GoalHandicap(Bet): """Goal handicap for a specific team — add handicap_amount to team's score, team wins = you win""" team_bet: Literal["1", "2"] = "1" # which team the handicap is applied to handicap_amount: float = 0.0 # e.g. +1.5 or -0.5 - def resolve(self): + def resolve(self) -> None: ... @dataclass class UnknownTicket(Bet): """Bet type that could not be classified""" raw_text: str = "" - def resolve(self): + def resolve(self) -> None: ... diff --git a/src/beaky/link_classifier/classifier.py b/src/beaky/link_classifier/classifier.py index 549d8d3..df784a9 100644 --- a/src/beaky/link_classifier/classifier.py +++ b/src/beaky/link_classifier/classifier.py @@ -1,19 +1,20 @@ import re from datetime import datetime +from typing import Any from playwright.sync_api import Page, sync_playwright from beaky.datamodels.ticket import ( + Bet, + BetType, BothTeamScored, GoalAmount, GoalHandicap, Ticket, - BetType, UnknownTicket, WinDrawLose, WinDrawLoseDouble, WinLose, - Bet ) from beaky.scanner.scanner import Link @@ -37,24 +38,24 @@ def _parse_teams(title: str) -> tuple[str, str]: def _classify_bet(bet_text: str, team1: str, team2: str, date: datetime, league: str) -> Bet: - common = dict(team1Name=team1, team2Name=team2, date=date, league=league) + common: dict[str, Any] = dict(team1Name=team1, team2Name=team2, date=date, league=league) # WinDrawLose double: "Výsledek zápasu - dvojtip: 10" m = re.search(r"Výsledek zápasu - dvojtip:\s*(\d+)", bet_text) if m: # normalize order: "10" -> "01", "02" -> "02", "12" -> "12" bet_type = "".join(sorted(m.group(1))) - return WinDrawLoseDouble(ticketType=BetType.WIN_DRAW_LOSE_DOUBLE, betType=bet_type, **common) + return WinDrawLoseDouble(ticketType=BetType.WIN_DRAW_LOSE_DOUBLE, betType=bet_type, **common) # type: ignore[arg-type] # WinLose (no draw): "Výsledek bez remízy: 1" m = re.search(r"bez rem[ií]zy:\s*([12])", bet_text) if m: - return WinLose(ticketType=BetType.WIN_LOSE, betType=m.group(1), **common) + return WinLose(ticketType=BetType.WIN_LOSE, betType=m.group(1), **common) # type: ignore[arg-type] # WinDrawLose: "Výsledek zápasu: 1" m = re.search(r"Výsledek zápasu:\s*([012X])\s*$", bet_text.strip()) if m: - return WinDrawLose(ticketType=BetType.WIN_DRAW_LOSE, betType=m.group(1), **common) + return WinDrawLose(ticketType=BetType.WIN_DRAW_LOSE, betType=m.group(1), **common) # type: ignore[arg-type] # BothTeamScored: "Každý z týmů dá gól v zápasu: Ano" if "dá gól" in bet_text or "oba týmy" in bet_text.lower(): @@ -77,7 +78,7 @@ def _classify_bet(bet_text: str, team1: str, team2: str, date: datetime, league: return UnknownTicket(ticketType=BetType.UNKNOWN, raw_text=bet_text, **common) sign = 1.0 if m.group(1) == "+" else -1.0 handicap = sign * float(m.group(2)) - return GoalHandicap(ticketType=BetType.GOAL_HANDICAP, team_bet=team_bet, handicap_amount=handicap, **common) + return GoalHandicap(ticketType=BetType.GOAL_HANDICAP, team_bet=team_bet, handicap_amount=handicap, **common) # type: ignore[arg-type] return UnknownTicket(ticketType=BetType.UNKNOWN, raw_text=bet_text, **common) diff --git a/src/beaky/resolvers/resolver.py b/src/beaky/resolvers/resolver.py index 9e87636..bd63fa5 100644 --- a/src/beaky/resolvers/resolver.py +++ b/src/beaky/resolvers/resolver.py @@ -1,8 +1,9 @@ import time from dataclasses import dataclass, field -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from difflib import SequenceMatcher from enum import Enum +from typing import Any import requests @@ -149,7 +150,7 @@ class ResolvedTicket: return TicketVerdict.TRUTHFUL -def _get(url: str, headers: dict, params: dict, retries: int = 3, backoff: float = 60.0) -> requests.Response: +def _get(url: str, headers: dict[str, str], params: dict[str, str | int], retries: int = 3, backoff: float = 60.0) -> requests.Response: for attempt in range(retries): resp = requests.get(url, headers=headers, params=params) if resp.status_code == 429: @@ -166,7 +167,7 @@ class TicketResolver: def __init__(self, config: ResolverConfig): self._headers = {"x-apisports-key": config.api_key} # Cache maps (center_date_str, league_id | None) -> list of fixture dicts - self._fixture_cache: dict[tuple[str, int | None], list[dict]] = {} + self._fixture_cache: dict[tuple[str, int | None], list[dict[str, Any]]] = {} # Cache maps league name -> (league_id, confidence) self._league_cache: dict[str, tuple[int | None, float]] = {} @@ -215,7 +216,7 @@ class TicketResolver: match_finished=finished, ) - def _find_fixture(self, bet: Bet) -> tuple[dict | None, float, float, float]: + def _find_fixture(self, bet: Bet) -> tuple[dict[str, Any] | None, float, float, float]: """Returns (fixture, name_match, date_proximity, league_confidence).""" center = bet.date.date() date_str = center.strftime("%Y-%m-%d") @@ -225,7 +226,7 @@ class TicketResolver: if cache_key not in self._fixture_cache: date_from = (center - timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") date_to = (center + timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") - params: dict = {"from": date_from, "to": date_to} + params: dict[str, str | int] = {"from": date_from, "to": date_to} if league_id is not None: params["league"] = league_id params["season"] = center.year if center.month >= 7 else center.year - 1 @@ -273,14 +274,14 @@ def _similarity(a: str, b: str) -> float: return SequenceMatcher(None, a.lower(), b.lower()).ratio() -def _date_proximity(fixture: dict, center) -> float: +def _date_proximity(fixture: dict[str, Any], center: date) -> float: """1.0 on exact date, linear decay to 0.0 at _DATE_WINDOW days away.""" fixture_date = datetime.fromisoformat(fixture["fixture"]["date"].replace("Z", "+00:00")).date() days_off = abs((fixture_date - center).days) return max(0.0, 1.0 - days_off / _DATE_WINDOW) -def _best_fixture_match(fixtures: list[dict], team1: str, team2: str, center) -> tuple[dict | None, float, float]: +def _best_fixture_match(fixtures: list[dict[str, Any]], team1: str, team2: str, center: date) -> tuple[dict[str, Any] | None, float, float]: """Returns (best_fixture, name_score, date_proximity) or (None, 0, 0) if no good match.""" best, best_combined, best_name, best_date = None, 0.0, 0.0, 0.0 for f in fixtures: @@ -302,12 +303,12 @@ def _best_fixture_match(fixtures: list[dict], team1: str, team2: str, center) -> return (best, best_name, best_date) if best_name > 0.5 else (None, best_name, best_date) -def _is_finished(fixture: dict) -> float: +def _is_finished(fixture: dict[str, Any]) -> float: status = fixture.get("fixture", {}).get("status", {}).get("short", "") return 1.0 if status in ("FT", "AET", "PEN", "AWD", "WO") else 0.0 -def _evaluate_bet(bet: Bet, fixture: dict) -> BetOutcome: +def _evaluate_bet(bet: Bet, fixture: dict[str, Any]) -> BetOutcome: goals = fixture.get("goals", {}) home = goals.get("home") away = goals.get("away") diff --git a/src/beaky/screenshotter/screenshotter.py b/src/beaky/screenshotter/screenshotter.py index cf1965a..77e6748 100644 --- a/src/beaky/screenshotter/screenshotter.py +++ b/src/beaky/screenshotter/screenshotter.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any from playwright.sync_api import sync_playwright @@ -11,7 +12,7 @@ class Screenshotter: self.config = config - def capture_tickets(self, links: list[Link]): + def capture_tickets(self, links: list[Link]) -> None: with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context() @@ -24,7 +25,7 @@ class Screenshotter: browser.close() - def capture_ticket(self,page, url, target_path, ticket_selector=".betslip-history-detail__left-panel"): + def capture_ticket(self, page: Any, url: str, target_path: Path, ticket_selector: str = ".betslip-history-detail__left-panel") -> None: page.goto(url) page.wait_for_selector(ticket_selector, timeout=10000) page.wait_for_timeout(1000)