Update calendar generation and data normalization; add read check command

- Add config file support for calendar colors and logos in generate_calendar
- Extend normalization mappings in normalization.toml for fields and teams
- Add 'read check' command to summarize game and field data from input files
- Fix normalization to correctly handle visitor field and value lookups
- Update launch configurations for new data and options
This commit is contained in:
2025-08-26 16:42:19 -05:00
parent c1628f5dfe
commit 2a521e9016
6 changed files with 112 additions and 33 deletions

22
.vscode/launch.json vendored
View File

@@ -12,8 +12,8 @@
"args": [ "args": [
"convert", "convert",
"sportspress", "sportspress",
"data/2025-hounds.csv", "/Users/asc/Downloads/2025 CSYBA vs CMBA SCHEDULE.xlsx",
"data/output/out.csv", "data/output/2025-hounds.csv",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true
@@ -27,7 +27,8 @@
"args": [ "args": [
"generate", "generate",
"calendar", "calendar",
"./data/2025-hounds.csv" "./data/2025-hounds.csv",
"-c", "./data/output/calendar_config.toml"
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true
@@ -41,11 +42,24 @@
"args": [ "args": [
"generate", "generate",
"calendar-config", "calendar-config",
"./data/2024-hounds.csv", "./data/2025-hounds.csv",
"./data/output/" "./data/output/"
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true
},
{
"name": "read check",
"type": "debugpy",
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "src",
"args": [
"read", "check",
"./data/2024.csv"
],
"console": "integratedTerminal",
"justMyCode": true
} }
] ]
} }

View File

@@ -40,6 +40,18 @@ normalized = "Taft High School"
[[field.values]] [[field.values]]
original = ["Southwest"] original = ["Southwest"]
normalized = "Southwest Park" normalized = "Southwest Park"
[[field.values]]
original = ["Comed", "COMED", "ComEd"]
normalized = "ComEd Recreation Center"
[[field.values]]
original = ["Laramie"]
normalized = "Laramie Park"
[[field.values]]
original = ["78"]
normalized = "The 78"
[[field.values]]
original = ["Kilbourn"]
normalized = "Kilbourn Park"
[results] [results]
potential_keys = ["Final Score", "Score", "Result", "Outcome"] potential_keys = ["Final Score", "Score", "Result", "Outcome"]
@@ -54,4 +66,10 @@ normalized = "Red Sox"
[[team.values]] [[team.values]]
original = ["NorthSide White Sox"] original = ["NorthSide White Sox"]
normalized = "North Side White Sox" normalized = "North Side White Sox"
[[team.values]]
original = ["Chicago Rebels", "CHICAGO REBELS"]
normalized = "Rebels"
[[team.values]]
original = ["Lombard Expors", "LOMBARD EXPORS"]
normalized = "Lombard Expos"

View File

@@ -1,6 +1,6 @@
import typer import typer
from rich.console import Console from rich.console import Console
from typing import Annotated, List from typing import Annotated, List, Optional
from pathlib import Path from pathlib import Path
from ...utils.sportspress import validate_keys from ...utils.sportspress import validate_keys
from ...utils.normalize import normalize_header_key, load_config from ...utils.normalize import normalize_header_key, load_config
@@ -15,13 +15,14 @@ app = typer.Typer()
@app.command(name="calendar") @app.command(name="calendar")
def generate_calendar_app( def generate_calendar_app(
input_file: Annotated[List[Path], typer.Argument(..., help="Path(s) to the CSV file")], input_file: Annotated[List[Path], typer.Argument(..., help="Path(s) to the CSV file")],
config_file: Annotated[Optional[typer.FileText], typer.Option(..., "--config", "-c", help="Path to a config file")]=None
): ):
# Read CSV data # Read CSV data
data = read_and_normalize_csv_or_xlsx(input_file) data = read_and_normalize_csv_or_xlsx(input_file)
data = personalize_data_for_team(data, "Hounds") data = personalize_data_for_team(data, "Hounds")
# data = parse_datetime(data) # data = parse_datetime(data)
generate_calendar(data) generate_calendar(data, config_file)
pass pass
@app.command(name="calendar-config") @app.command(name="calendar-config")

View File

@@ -3,6 +3,7 @@ from calendar import Calendar
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from typing import Tuple from typing import Tuple
from pathlib import Path from pathlib import Path
import toml
calendar_cell_size = (400, 500) calendar_cell_size = (400, 500)
calendar_cell_width, calendar_cell_height = calendar_cell_size calendar_cell_width, calendar_cell_height = calendar_cell_size
@@ -72,10 +73,13 @@ def calendar_cell(
return cell_img return cell_img
def generate_calendar(data): def generate_calendar(data, config_file=None):
result_calendar = Calendar() result_calendar = Calendar()
result_calendar.setfirstweekday(6) result_calendar.setfirstweekday(6)
baseball_bat = Image.open(f"data/logos/baseball_bat_2.png") baseball_bat = Image.open(f"data/logos/baseball_bat_2.png")
if config_file:
config = toml.load(config_file)
baseball_bat = baseball_bat.resize((90, 90)) baseball_bat = baseball_bat.resize((90, 90))
for year, month in {(row['datetime'].year, row['datetime'].month) for row in data}: for year, month in {(row['datetime'].year, row['datetime'].month) for row in data}:
month_days=list(result_calendar.monthdayscalendar(year, month)) month_days=list(result_calendar.monthdayscalendar(year, month))
@@ -83,21 +87,15 @@ def generate_calendar(data):
first_thursday=(month, [w[4] for w in month_days if w[4] != 0][0]) first_thursday=(month, [w[4] for w in month_days if w[4] != 0][0])
colors = { colors = {
# 'proviso-west': (139, 69, 19, 256), 'default': (128, 128, 128, 256),
'winnemac': (37, 89, 164, 256),
# 'walther': (251, 231, 77, 256),
# 'taft': (64, 119, 0, 256),
'southwest': (230, 136, 60, 256),
# 'maywood': (107, 5, 4, 256),
# 'ozinga': (170, 143, 102, 256),
# 'simeon':(167,192,226),
'Skokie':(72,159,88),
# 'comed':(206,45,137),
'default': (0, 0, 0, 256),
'Loyola': (206,45,137),
'Piotrowski': (251, 231, 77, 256),
'Baseball Alley': (167,192,226),
} }
team_logos={}
if config:
for field, field_options in config['fields'].items():
colors[field] = tuple(field_options.get('bg_color', colors.get('default')))
for team, team_options in config['teams'].items():
team_logos[team] = team_options.get('logo')
for week_num, week in enumerate(month_days): for week_num, week in enumerate(month_days):
for day_num, date in enumerate(week): for day_num, date in enumerate(week):
@@ -111,9 +109,9 @@ def generate_calendar(data):
# Gen square that has one game # Gen square that has one game
if len (filtered_data) == 1: if len (filtered_data) == 1:
game = filtered_data[0] game = filtered_data[0]
opponent_logo_path = Path(f"data/logos/{game['opponent'].lower()}.png") opponent_logo_path = team_logos.get(game['opponent'])
if opponent_logo_path.exists(): if opponent_logo_path and (opponent_logo_path := Path(opponent_logo_path)) and opponent_logo_path.exists():
opponent_logo = Image.open(f"data/logos/{game['opponent'].lower()}.png") opponent_logo = Image.open(opponent_logo_path)
else: else:
opponent_logo = text_rectangle(text=game['opponent'][0].upper(),width=500, height=500, font_size=400, font="data/fonts/college.ttf") opponent_logo = text_rectangle(text=game['opponent'][0].upper(),width=500, height=500, font_size=400, font="data/fonts/college.ttf")
is_home_game = game['homevisitor'] == "home" is_home_game = game['homevisitor'] == "home"
@@ -124,7 +122,7 @@ def generate_calendar(data):
img = calendar_cell( img = calendar_cell(
height=calendar_cell_height, height=calendar_cell_height,
width=calendar_cell_width, width=calendar_cell_width,
background_color=colors[game['field']], background_color=colors.get(game['field'], colors['default']),
left_top_corner = text_rectangle('H' if is_home_game else "A", left_top_corner = text_rectangle('H' if is_home_game else "A",
"data/fonts/refrigerator-deluxe-bold.otf", "data/fonts/refrigerator-deluxe-bold.otf",
80, 80,
@@ -134,7 +132,7 @@ def generate_calendar(data):
width=calendar_cell_width*.2), width=calendar_cell_width*.2),
right_top_corner = date_text_image, right_top_corner = date_text_image,
center = opponent_logo.resize((int(opponent_logo.width*.5), int(opponent_logo.height*.5))), center = opponent_logo.resize((int(opponent_logo.width*.5), int(opponent_logo.height*.5))),
bottom_center = text_rectangle(f"{game['time']:%-I:%M}", bottom_center = text_rectangle(f"{game['time']:%-I:%M}" if game.get('time') else "",
"data/fonts/refrigerator-deluxe-bold.otf", "data/fonts/refrigerator-deluxe-bold.otf",
120, 120,
foreground_color='white', foreground_color='white',
@@ -145,10 +143,16 @@ def generate_calendar(data):
# img.show() # img.show()
elif len(filtered_data) == 2: elif len(filtered_data) == 2:
game1, game2 = filtered_data[:2] game1, game2 = filtered_data[:2]
opponent_logo_path = team_logos.get(game1['opponent'])
if opponent_logo_path and (opponent_logo_path := Path(opponent_logo_path)) and opponent_logo_path.exists():
opponent_logo = Image.open(opponent_logo_path)
else:
opponent_logo = text_rectangle(text=game1['opponent'][0].upper(),width=500, height=500, font_size=400, font="data/fonts/college.ttf")
img = calendar_cell( img = calendar_cell(
height=calendar_cell_height, height=calendar_cell_height,
width=calendar_cell_width, width=calendar_cell_width,
background_color='red', background_color=colors.get(game1['field'], colors['default']),
left_top_corner = text_rectangle('DH', left_top_corner = text_rectangle('DH',
"data/fonts/refrigerator-deluxe-bold.otf", "data/fonts/refrigerator-deluxe-bold.otf",
80, 80,

View File

@@ -50,10 +50,52 @@ def print_table(
# breakpoint() # breakpoint()
for row in data: for row in data:
table.add_row(*[row[key] for key in keys]) table.add_row(*[str(row[key]) for key in keys])
console.print(table) console.print(table)
@app.command()
def check(
input_file: Annotated[List[Path], typer.Argument(..., help="Path(s) to the CSV or XLSX file")]
):
# Read CSV data
data = []
for f in input_file:
data.extend(read_and_normalize_csv_or_xlsx(f))
teams = set([row['visitor'] for row in data] + [row['home'] for row in data])
fields = set([row['field'] for row in data])
console = Console()
table = Table("Team", "Number of Games")
for team in teams:
rows = [row for row in data if row['visitor']==team or row['home']==team]
table.add_row(team, str(len(rows)))
console.print(table)
table = Table("Field", "Number of Games")
for field in fields:
rows = [row for row in data if row['field']==field]
table.add_row(field, str(len(rows)))
console.print(table)
table = Table("Field", "Datetime", "Games")
field_times = [(row['field'], row['datetime']) for row in data]
for field, datetime in field_times:
rows = [row for row in data if row['field'] == field and row['datetime'] == datetime]
if len(rows) != 1:
table.add_row(str(field), str(datetime), str(len(rows)))
console.print(table)
matchups = set([tuple([*sorted((row['home'], row['visitor']))]) for row in data])
table =Table("Team 1", "Team 2", "Games")
for team1, team2 in matchups:
rows = [row for row in data if (row['visitor']==team1 or row['home']==team1) and (row['visitor']==team2 or row['home']==team2)]
table.add_row(str(team1), str(team2), str(len(rows)))
console.print(table)
pass
@app.command() @app.command()
def standings( def standings(
input_file: Annotated[List[Path], typer.Argument(..., help="Path(s) to the CSV or XLSX file")], input_file: Annotated[List[Path], typer.Argument(..., help="Path(s) to the CSV or XLSX file")],

View File

@@ -22,9 +22,9 @@ def normalize_header_key(original_key: str, normalization_config) -> str:
def normalize_value(value, key, normalization_config): def normalize_value(value, key, normalization_config):
value = value.strip() value = value.strip()
for value in normalization_config.get(key,{}).get('values',[]): for normalization_pair in normalization_config.get(key if not key == "home" or key == "away" else "team",{}).get('values',[]):
if value in value["original"]: if value in normalization_pair["original"]:
value = value["normalized"] value = normalization_pair["normalized"]
match key.lower(): match key.lower():
case "date": case "date":
if value: if value:
@@ -33,7 +33,7 @@ def normalize_value(value, key, normalization_config):
pass pass
case "home": case "home":
value = value.title() value = value.title()
case "away": case "visitor":
value = value.title() value = value.title()
case "time": case "time":
if value: if value:
@@ -48,7 +48,7 @@ def normalize_value(value, key, normalization_config):
def normalize_keyvalue(key: str,value:str, normalization_config): def normalize_keyvalue(key: str,value:str, normalization_config):
key, value = key.strip(), value.strip() key, value = key.strip(), value.strip()
normalized_k = normalize_header_key(key, normalization_config) normalized_k = normalize_header_key(key, normalization_config)
normalized_v = normalize_value(value, key, normalization_config) normalized_v = normalize_value(value, normalized_k, normalization_config)
return normalized_k, normalized_v return normalized_k, normalized_v
def normalize_row(row:dict, normalization_config): def normalize_row(row:dict, normalization_config):