191 lines
10 KiB
Python
191 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import lvpwb as app
|
|
|
|
def default_card_path(show: str, show_date: str) -> Path:
|
|
app.CARD_DIR.mkdir(parents=True, exist_ok=True)
|
|
stem = f"{show_date}-{app.slugify(show)}"
|
|
path = app.CARD_DIR / f"{stem}.md"
|
|
suffix = 2
|
|
while path.exists():
|
|
path = app.CARD_DIR / f"{stem}-{suffix}.md"
|
|
suffix += 1
|
|
return path
|
|
|
|
|
|
def save_card(card: dict[str, Any]) -> None:
|
|
path = Path(card["path"])
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
card.setdefault("game_title", app.game_title(card["game_id"]))
|
|
card.setdefault("federation_name", app.federation_name(card["game_id"]))
|
|
card.setdefault("title_definitions", app.title_definitions_for_game(card["game_id"]))
|
|
ppv_rating = app.calculate_ppv_rating(card)
|
|
card["ppv_rating"] = ppv_rating
|
|
lines = [
|
|
f"# Show: {json.dumps(card['show'], ensure_ascii=False)}",
|
|
f"- City: {json.dumps(card.get('city', ''), ensure_ascii=False)}",
|
|
f"- Attendance: {json.dumps(card.get('attendance'))}",
|
|
f"- Game ID: {json.dumps(card['game_id'])}",
|
|
f"- Game: {json.dumps(card['game_title'], ensure_ascii=False)}",
|
|
f"- Federation: {json.dumps(card['federation_name'], ensure_ascii=False)}",
|
|
f"- Title Definitions: {json.dumps(card['title_definitions'], ensure_ascii=False)}",
|
|
f"- Date: {json.dumps(card['date'])}",
|
|
]
|
|
if app.is_ppv_card(card):
|
|
lines.append(f"- PPV Rating: {json.dumps(ppv_rating)}")
|
|
if card.get("season_id"):
|
|
lines.extend([
|
|
f"- Season ID: {json.dumps(card['season_id'])}",
|
|
f"- Season Show: {json.dumps(card.get('season_show', ''))}",
|
|
f"- Season Show Number: {json.dumps(card.get('season_show_number'))}",
|
|
])
|
|
lines.append("")
|
|
for number, match in enumerate(card["matches"], 1):
|
|
lines.append(f"## Match {number}")
|
|
lines.append(f"- Title: {json.dumps(match.get('title', ''), ensure_ascii=False)}")
|
|
lines.append(f"- Type: {json.dumps(match['type'], ensure_ascii=False)}")
|
|
lines.append(f"- Status: {json.dumps('Complete' if match.get('winner_side') is not None else 'Pending')}")
|
|
lines.append(f"- Sides: {json.dumps(match['sides'], ensure_ascii=False)}")
|
|
lines.append(f"- Winner side: {json.dumps(match.get('winner_side'))}")
|
|
lines.append(f"- Finish: {json.dumps(match.get('finish', ''), ensure_ascii=False)}")
|
|
lines.append(f"- Promo: {json.dumps(match.get('promo'), ensure_ascii=False)}")
|
|
lines.append(f"- Image: {json.dumps(match.get('image', ''), ensure_ascii=False)}")
|
|
lines.append(f"- Ranking Division: {json.dumps(match.get('ranking_division'))}")
|
|
lines.append(f"- Title Defense: {json.dumps(bool(match.get('title_defense', False)))}")
|
|
lines.append(f"- Rivalry Payoff: {json.dumps(bool(match.get('rivalry_payoff', False)))}")
|
|
lines.append(f"- Match Time Minutes: {json.dumps(match.get('match_time_minutes'))}")
|
|
lines.append(f"- Near Falls: {json.dumps(match.get('near_falls', 0))}")
|
|
lines.append(f"- Finishers: {json.dumps(match.get('finishers', 0))}")
|
|
lines.append(f"- Match Rating: {json.dumps(match.get('rating'))}")
|
|
lines.append(f"- Base Rating: {json.dumps(match.get('base_rating'))}")
|
|
lines.append(f"- Valid Outcomes: {json.dumps(match.get('valid_outcomes'), ensure_ascii=False)}")
|
|
lines.append("")
|
|
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
card["path"] = str(path)
|
|
|
|
|
|
def load_card(path: Path) -> dict[str, Any]:
|
|
"""Load a card written in the app's readable Markdown format."""
|
|
text = path.read_text(encoding="utf-8")
|
|
card: dict[str, Any] = {"path": str(path), "matches": []}
|
|
current: dict[str, Any] | None = None
|
|
for line in text.splitlines():
|
|
if line.startswith("# Show: "):
|
|
card["show"] = json.loads(line[len("# Show: "):])
|
|
continue
|
|
match_header = re.fullmatch(r"## Match (\d+)", line)
|
|
if match_header:
|
|
current = {"title": "", "type": "Singles", "sides": [], "winner_side": None,
|
|
"finish": "", "promo": None, "image": "", "ranking_division": None,
|
|
"title_defense": False, "rivalry_payoff": False,
|
|
"match_time_minutes": None, "near_falls": 0, "finishers": 0,
|
|
"rating": None, "base_rating": None, "valid_outcomes": None}
|
|
card["matches"].append(current)
|
|
continue
|
|
field = re.match(r"^- ([A-Za-z ]+): (.*)$", line)
|
|
if not field:
|
|
continue
|
|
key, raw = field.group(1).strip().lower(), field.group(2)
|
|
try:
|
|
value = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid Markdown card field '{key}' in {path}") from exc
|
|
if key in ("game id", "game", "federation", "title definitions", "date", "city", "attendance",
|
|
"season id", "season show", "season show number", "ppv rating"):
|
|
metadata_key = {"game": "game_title", "game id": "game_id"}.get(
|
|
key, key.replace(" ", "_")
|
|
)
|
|
card[metadata_key] = value
|
|
elif current is not None:
|
|
mapping = {"title": "title", "type": "type", "sides": "sides",
|
|
"winner side": "winner_side", "finish": "finish", "promo": "promo",
|
|
"image": "image", "ranking division": "ranking_division",
|
|
"title defense": "title_defense", "rivalry payoff": "rivalry_payoff",
|
|
"match time minutes": "match_time_minutes", "near falls": "near_falls",
|
|
"finishers": "finishers", "match rating": "rating",
|
|
"base rating": "base_rating", "valid outcomes": "valid_outcomes"}
|
|
if key in mapping:
|
|
current[mapping[key]] = value
|
|
if card.get("game_id") not in app.GAMES:
|
|
raise ValueError(f"Unknown or missing game ID in {path}")
|
|
if len(card["matches"]) > 3:
|
|
raise ValueError(f"A card cannot contain more than three matches: {path}")
|
|
while len(card["matches"]) < 3:
|
|
card["matches"].append({"title": "", "type": "Singles", "sides": [],
|
|
"winner_side": None, "finish": "", "promo": None,
|
|
"image": "", "ranking_division": None,
|
|
"title_defense": False, "rivalry_payoff": False,
|
|
"match_time_minutes": None, "near_falls": 0,
|
|
"finishers": 0, "rating": None, "base_rating": None,
|
|
"valid_outcomes": None})
|
|
if not card.get("show") or not card.get("date"):
|
|
raise ValueError(f"Card is missing its show name or date: {path}")
|
|
card.setdefault("city", "")
|
|
card.setdefault("attendance", None)
|
|
if card["attendance"] is not None and (
|
|
not isinstance(card["attendance"], int) or card["attendance"] <= 0
|
|
):
|
|
raise ValueError(f"Card has an invalid attendance: {path}")
|
|
for key in ("season_show_number",):
|
|
value = card.get(key)
|
|
if value is not None and (not isinstance(value, int) or value < 1):
|
|
raise ValueError(f"Card has invalid season metadata: {path}")
|
|
for match in card["matches"]:
|
|
if any(not side for side in match.get("sides", [])):
|
|
raise ValueError(f"A match is missing participants: {path}")
|
|
winner = match.get("winner_side")
|
|
if winner is not None and (not isinstance(winner, int) or not 0 <= winner < len(match["sides"])):
|
|
raise ValueError(f"A match has an invalid winner side: {path}")
|
|
division = match.get("ranking_division")
|
|
card_titles = card.get("title_definitions") or app.title_definitions_for_game(card["game_id"])
|
|
if division is not None and division not in [title["name"] for title in card_titles]:
|
|
raise ValueError(f"A match has an unknown ranking division: {path}")
|
|
if match.get("title_defense") and division is None:
|
|
raise ValueError(f"A title defense must have a ranking division: {path}")
|
|
minutes = match.get("match_time_minutes")
|
|
if minutes is not None and (
|
|
not isinstance(minutes, (int, float)) or not math.isfinite(minutes) or minutes < 0
|
|
):
|
|
raise ValueError(f"A match has an invalid duration: {path}")
|
|
for counter in ("near_falls", "finishers"):
|
|
value = match.get(counter, 0)
|
|
if not isinstance(value, int) or value < 0:
|
|
raise ValueError(f"A match has an invalid {counter.replace('_', ' ')} count: {path}")
|
|
rating = match.get("rating")
|
|
if rating is not None and (
|
|
not isinstance(rating, (int, float)) or not math.isfinite(rating) or not 0 <= rating <= 100
|
|
):
|
|
raise ValueError(f"A match has an invalid rating: {path}")
|
|
base_rating = match.get("base_rating")
|
|
if base_rating is not None and (
|
|
not isinstance(base_rating, (int, float)) or not math.isfinite(base_rating)
|
|
or not 0 <= base_rating <= 100
|
|
):
|
|
raise ValueError(f"A match has an invalid base rating: {path}")
|
|
outcomes = match.get("valid_outcomes")
|
|
if outcomes is not None and (
|
|
not isinstance(outcomes, list) or not outcomes
|
|
or any(not isinstance(outcome, str) or not outcome.strip() for outcome in outcomes)
|
|
):
|
|
raise ValueError(f"A match has invalid outcome options: {path}")
|
|
card.setdefault("game_title", app.game_title(card["game_id"]))
|
|
card.setdefault("federation_name", app.federation_name(card["game_id"]))
|
|
card.setdefault("title_definitions", app.title_definitions_for_game(card["game_id"]))
|
|
card["ppv_rating"] = app.calculate_ppv_rating(card)
|
|
return card
|
|
|
|
|
|
def card_summary(path: Path) -> str:
|
|
"""One-line listing for a saved card; falls back to the file stem."""
|
|
try:
|
|
return app.show_location_text(app.load_card(path))
|
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
return path.stem
|
|
|
|
__all__ = ['card_summary', 'default_card_path', 'load_card', 'save_card']
|