-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerate_readme.py
executable file
Β·205 lines (163 loc) Β· 8.06 KB
/
generate_readme.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
import argparse
import os
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from rich.console import Console
from rich.progress import track
console = Console()
@dataclass
class DayStatus:
year: int
day: int
has_part1: bool
has_part2: bool
class AoCStats:
def __init__(self, root_dir: Path):
self.root_dir = root_dir
self.current_year = datetime.now().year
def validate_directory_structure(self) -> bool:
"""Validate the directory structure and provide helpful feedback"""
if not self.root_dir.exists():
console.print(f"[red]Error: Directory '{self.root_dir}' not found![/red]")
return False
year_dirs = [d for d in self.root_dir.iterdir() if d.is_dir() and d.name.isdigit() and len(d.name) == 4]
if not year_dirs:
console.print(f"[yellow]Warning: No year directories (YYYY format) found in '{self.root_dir}'[/yellow]")
console.print("Expected structure:")
console.print(" root/")
console.print(" βββ 2024/")
console.print(" β βββ Day-1/")
console.print(" β βββ Day-2/")
console.print(" βββ 2023/")
console.print(" βββ Day-1/")
console.print(" βββ Day-2/")
return False
return True
def check_day_status(self, year: int, day_dir: Path) -> DayStatus:
"""Check the status of files in a day directory"""
return DayStatus(
year=year,
day=int(day_dir.name.split("-")[1]),
has_part1=(day_dir / "part-1.py").exists() or (day_dir / "part-1.rb").exists(),
has_part2=(day_dir / "part-2.py").exists() or (day_dir / "part-2.rb").exists(),
)
def create_year_status_table(self, days: List[DayStatus]) -> str:
"""Create a markdown table showing completion status for a specific year"""
header = ["| Day | Part 1 | Part 2 |", "|--------|---------|---------|"]
rows = []
for day in sorted(days, key=lambda x: x.day):
rows.append(f"| Day {day.day:2d} | " f"{'β
' if day.has_part1 else 'β'} | " f"{'β
' if day.has_part2 else 'β'} |")
return "\n".join(header + rows)
def create_year_statistics(self, year: int, days: List[DayStatus]) -> str:
"""Create statistics about completion status for a specific year"""
total_days = len(days)
if total_days == 0:
return f"### Year {year} Statistics\n- No solutions yet!"
completed_part1 = sum(1 for day in days if day.has_part1)
completed_part2 = sum(1 for day in days if day.has_part2)
stars = completed_part1 + completed_part2
max_stars = total_days * 2
# Calculate percentage of year completed
days_in_year = 25 # Total days in AoC
completion_percentage = (total_days / days_in_year) * 100
stats = f"""### Year {year} Statistics
- Progress: {total_days}/{days_in_year} days ({completion_percentage:.1f}% of challenges attempted)
- Stars Collected: {stars}/{max_stars} ({(stars/max_stars*100):.1f}% of attempted challenges completed)
- Part 1 Completion: {completed_part1}/{total_days} ({completed_part1/total_days*100:.1f}%)
- Part 2 Completion: {completed_part2}/{total_days} ({completed_part2/total_days*100:.1f}%)"""
if year == self.current_year:
remaining_days = 25 - total_days
if remaining_days > 0:
stats += f"\n- Remaining Challenges: {remaining_days} days"
return stats
def create_overall_statistics(self, all_days: List[DayStatus]) -> str:
"""Create overall statistics across all years"""
if not all_days:
return "## Overall Progress\n- No solutions yet!"
total_days = len(all_days)
completed_part1 = sum(1 for day in all_days if day.has_part1)
completed_part2 = sum(1 for day in all_days if day.has_part2)
total_stars = completed_part1 + completed_part2
max_stars = total_days * 2
years_completed = {day.year for day in all_days}
years_list = sorted(years_completed, reverse=True)
years_str = ", ".join([str(y) for y in years_list])
total_possible_days = len(years_completed) * 25
total_possible_stars = total_possible_days * 2
return f"""## Overall Progress
- Years Participated: {years_str}
- Days Completed: {total_days}/{total_possible_days} ({(total_days/total_possible_days*100):.1f}% of all possible days)
- Total Stars: {total_stars}/{total_possible_stars} ({(total_stars/total_possible_stars*100):.1f}% of all possible stars)
- Average Stars per Day: {(total_stars/total_days):.1f} (when attempted)"""
def create_main_header(self) -> str:
"""Create the main header section with badges"""
stars_repo_name = self.root_dir.resolve().name
return """# π Advent of Code Solutions π
This repository contains my solutions for [Advent of Code](https://adventofcode.com/) challenges across multiple years.
> [!TIP]
> Each year's solutions are organized in their respective directories. Solutions are automatically tracked and this README is updated after each push."""
def generate_readme(self) -> str:
"""Generate the complete README content"""
if not self.validate_directory_structure():
return None
# Get all year directories
year_dirs = [d for d in self.root_dir.iterdir() if d.is_dir() and d.name.isdigit() and len(d.name) == 4]
if not year_dirs:
return "# Advent of Code\nNo solutions yet!"
# Collect all days across all years
all_days = []
year_days: Dict[int, List[DayStatus]] = {}
for year_dir in track(year_dirs, description="Processing years..."):
year = int(year_dir.name)
day_dirs = [d for d in year_dir.iterdir() if d.is_dir() and d.name.startswith("Day-")]
if day_dirs:
days = [self.check_day_status(year, day_dir) for day_dir in day_dirs]
all_days.extend(days)
year_days[year] = days
# Create README sections
sections = [self.create_main_header(), self.create_overall_statistics(all_days), "\n## Year-by-Year Progress"]
# Add year-specific sections
for year in sorted(year_days.keys(), reverse=True):
sections.extend([f"\n### {year}", self.create_year_status_table(year_days[year]), self.create_year_statistics(year, year_days[year])])
return "\n\n".join(sections)
def main():
parser = argparse.ArgumentParser(
description="Generate consolidated README for Advent of Code solutions",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Example directory structure:
repository_root/
βββ 2024/
β βββ Day-1/
β β βββ part-1.py
β β βββ part-2.py
β βββ Day-2/
βββ 2023/
β βββ Day-1/
β βββ Day-2/
βββ README.md
""",
)
parser.add_argument("--root", type=str, default=".", help="Root directory containing year directories (default: current directory)")
args = parser.parse_args()
try:
stats = AoCStats(Path(args.root))
readme_content = stats.generate_readme()
if readme_content:
readme_path = Path(args.root) / "README.md"
with open(readme_path, "w") as f:
f.write(readme_content)
console.print(f"[green]Successfully generated README at {readme_path}[/green]")
# Preview the changes only in interactive terminal
if sys.stdout.isatty() and not os.getenv("GITHUB_ACTIONS"):
console.print("\n[yellow]Preview of the first 500 characters:[/yellow]")
console.print(readme_content[:500] + "...\n")
except Exception as e:
console.print(f"[red]Error generating README: {str(e)}[/red]")
sys.exit(1)
if __name__ == "__main__":
main()