Skip to content

Commit ed111df

Browse files
Merge branch 'main' into feature/search-history-ui-redesign
2 parents f0f0412 + 8543888 commit ed111df

19 files changed

Lines changed: 582 additions & 1404 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
[![Python Version](https://img.shields.io/badge/python-3.10--3.12-blue.svg)](https://www.python.org/downloads/)
1010
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
1111

12+
### 🧪 New: Standardized Error Logging
13+
A shared error logger is now available for mini-projects. It captures exception type, message, timestamp, traceback, and project name into JSONL logs under the logs folder, making failures easier to review and analyze.
14+
15+
[Quick Start](#-quick-start)[Projects](#-projects)[Features](#-features)[Contributing](#-contributing)
1216
<p align="center">
1317
<a href="https://python-mini-project-lovat.vercel.app/">
1418
<img src="https://img.shields.io/badge/live_demo-View%20App-22c55e?style=for-the-badge&logo=vercel&logoColor=white" alt="Live Demo" />

error_logger.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import traceback
5+
from collections import Counter
6+
from datetime import datetime, timezone
7+
from pathlib import Path
8+
from typing import Any
9+
10+
11+
DEFAULT_LOG_PATH = Path(__file__).resolve().parent / "logs" / "error-logs.jsonl"
12+
13+
14+
def _ensure_log_path(log_path: str | Path | None) -> Path:
15+
path = Path(log_path) if log_path is not None else DEFAULT_LOG_PATH
16+
path.parent.mkdir(parents=True, exist_ok=True)
17+
return path
18+
19+
20+
def log_exception(
21+
project_name: str,
22+
exception: Exception,
23+
log_path: str | Path | None = None,
24+
additional_data: dict[str, Any] | None = None,
25+
) -> dict[str, Any]:
26+
"""Write a structured exception entry to a JSON Lines log file."""
27+
path = _ensure_log_path(log_path)
28+
payload = {
29+
"project_name": project_name,
30+
"exception_type": type(exception).__name__,
31+
"error_message": str(exception),
32+
"timestamp": datetime.now(timezone.utc).isoformat(),
33+
"traceback": "".join(traceback.format_exception(type(exception), exception, exception.__traceback__)),
34+
}
35+
36+
if additional_data:
37+
payload["additional_data"] = additional_data
38+
39+
with path.open("a", encoding="utf-8") as handle:
40+
handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
41+
42+
return payload
43+
44+
45+
def summarize_logs(log_path: str | Path | None = None) -> dict[str, Any]:
46+
"""Return a compact summary of the stored error logs."""
47+
path = _ensure_log_path(log_path)
48+
if not path.exists():
49+
return {
50+
"total_errors": 0,
51+
"exception_counts": {},
52+
"project_counts": {},
53+
"latest_errors": [],
54+
}
55+
56+
exception_counts: Counter[str] = Counter()
57+
project_counts: Counter[str] = Counter()
58+
latest_errors: list[dict[str, Any]] = []
59+
60+
with path.open("r", encoding="utf-8") as handle:
61+
for line in handle:
62+
line = line.strip()
63+
if not line:
64+
continue
65+
payload = json.loads(line)
66+
exception_counts[payload["exception_type"]] += 1
67+
project_counts[payload["project_name"]] += 1
68+
latest_errors.append(payload)
69+
70+
return {
71+
"total_errors": sum(exception_counts.values()),
72+
"exception_counts": dict(exception_counts),
73+
"project_counts": dict(project_counts),
74+
"latest_errors": latest_errors[-5:],
75+
}
76+
77+
78+
def safe_run(project_name: str, action, log_path: str | Path | None = None):
79+
"""Run an action while logging any unexpected exceptions."""
80+
try:
81+
return action()
82+
except Exception as exc: # pylint: disable=broad-except
83+
log_exception(project_name, exc, log_path=log_path)
84+
raise

games/Hangman-Game/Hangman-Game.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
import random
2+
import os
3+
import sys
4+
5+
# Add project root to sys.path
6+
if "__file__" in globals():
7+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
8+
else:
9+
sys.path.append(os.path.abspath(os.getcwd()))
10+
11+
# Reconfigure stdout/stderr to use UTF-8 encoding (resolves CP1252/Unicode issues on Windows)
12+
if sys.stdout.encoding != 'utf-8':
13+
try:
14+
sys.stdout.reconfigure(encoding='utf-8')
15+
except AttributeError:
16+
pass
17+
if sys.stderr.encoding != 'utf-8':
18+
try:
19+
sys.stderr.reconfigure(encoding='utf-8')
20+
except AttributeError:
21+
pass
22+
23+
from utils.banners import print_victory_banner, print_game_over_banner
224

325
def main():
426
# Cleaned up global variables (not strictly necessary unless modifying in nested scopes, but kept for your structure)
@@ -121,15 +143,13 @@ def main():
121143
print("-" * 50)
122144

123145
# Endgame sequence
124-
print("\n" + "=" * 50)
125146
if won:
126-
print("🎉 CONGRATULATIONS! YOU WON LIGHTNING FAST!")
147+
print_victory_banner()
127148
print(f"The word was: {word}")
128149
print(f"You guessed it with {max_attempts - attempts} attempts remaining!")
129150
else:
130-
print("😔 GAME OVER! YOU LOST!")
151+
print_game_over_banner()
131152
print(f"The word was: {word}")
132-
print("=" * 50)
133153

134154
play_again = input("\n🔄 Do you want to play again? (y/n): ").strip().lower()
135155
if play_again not in ['y', 'yes']:

games/Minesweeper/minesweeper.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import random
2+
import os
23
import sys
34

5+
# Add project root to sys.path
6+
if "__file__" in globals():
7+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
8+
else:
9+
sys.path.append(os.path.abspath(os.getcwd()))
10+
11+
from utils.banners import print_victory_banner, print_game_over_banner
12+
413
# Emojis for cell states
514
UNOPENED = "⬜"
615
FLAGGED = "🚩"
@@ -183,10 +192,10 @@ def play_game():
183192

184193
print("\n" + "="*40 + "\n")
185194
if game.game_over:
186-
print("💥 BOOM! You hit a mine. GAME OVER. 💥")
195+
print_game_over_banner()
187196
game.print_board(show_all=True)
188197
elif game.victory:
189-
print("🎉 CONGRATULATIONS! You cleared the minefield! 🎉")
198+
print_victory_banner()
190199
game.print_board(show_all=True)
191200

192201
if __name__ == "__main__":

games/Rock-Paper-Scissor/Rock-Paper-Scissor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import random
2-
import os
32
from pathlib import Path
43

54
RESULTS_FILE = Path(__file__).parent / "game_results.txt"
5+
RESULTS_FILE.parent.mkdir(parents=True, exist_ok=True)
66

77

88
def parse_results():
@@ -210,11 +210,11 @@ def main():
210210
f"(User-Computer), Rounds: {rounds_played}\n"
211211
)
212212
try:
213-
with open(RESULTS_FILE, "a", encoding="utf-8") as f:
213+
with RESULTS_FILE.open("a", encoding="utf-8") as f:
214214
f.write(result_string)
215215
print("Game results saved successfully.")
216-
except IOError:
217-
print("Error: Could not save game results to file.")
216+
except OSError as e:
217+
print(f"Error: Could not save game results: {e}")
218218
break
219219

220220

games/Simon-Says/Simon-Says.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import random
22
import time
3+
import os
4+
import sys
5+
6+
# Add project root to sys.path
7+
if "__file__" in globals():
8+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
9+
else:
10+
sys.path.append(os.path.abspath(os.getcwd()))
11+
12+
from utils.banners import print_game_over_banner
313

414

515
def main():
@@ -78,9 +88,7 @@ def main():
7888
print(f"❌ You entered: {' '.join(player_sequence)}\n")
7989
game_over = True
8090

81-
print(f"\n{'='*50}")
82-
print(f"🏁 GAME OVER!")
83-
print(f"{'='*50}")
91+
print_game_over_banner()
8492
print(f"🎯 Rounds Completed: {round_number - 1}")
8593
print(f"📊 Sequence Length Reached: {len(sequence)}")
8694

games/Sudoku-Game/Sudoku-Game.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
sys.path.append(os.path.abspath(os.getcwd()))
2424

2525
from utils.validation import get_choice, get_int
26+
from utils.banners import print_victory_banner
2627

2728
# Enable ANSI escape sequences on Windows
2829
if os.name == 'nt':
@@ -254,7 +255,7 @@ def play_game():
254255

255256
# Check if solved
256257
if find_empty(current_board) is None:
257-
print(f"\n🎉 Congratulations! You completed the Sudoku puzzle! 🏆")
258+
print_victory_banner()
258259
print(f"Hints used: {hints_used}")
259260
input("\nPress Enter to return to menu...")
260261
break
@@ -352,7 +353,7 @@ def play_game():
352353
os.system('cls' if os.name == 'nt' else 'clear')
353354
print_header()
354355
print_board(current_board, puzzle)
355-
print(f"\n🎉 Congratulations! You completed the Sudoku puzzle! 🏆")
356+
print_victory_banner()
356357
print(f"Hints used: {hints_used}")
357358
input("\nPress Enter to return to menu...")
358359
break

games/Whack-a-Mole/Whack-a-Mole.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def main():
1313

1414
# Add root directory to sys.path to import utils package
1515
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
16-
from utils import save_score, get_top_scores
16+
from utils import save_score, get_top_scores, print_game_over_banner
1717

1818
# Windows only for non-blocking input
1919
try:
@@ -188,9 +188,7 @@ def main():
188188
rnd += 1
189189

190190
os.system("cls" if os.name == "nt" else "clear")
191-
print("\n╔══════════════════════════════════════╗")
192-
print("║ 🏁 GAME OVER! ║")
193-
print("╚══════════════════════════════════════╝\n")
191+
print_game_over_banner()
194192
print(f" 🎯 Final Score : {score}")
195193
print(f" ❌ Total Misses: {misses}")
196194
max_score = rounds * n_moles * 10

games/Word-Building/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
def DataAdding(word):
77
file = open(WORDS_FILE, 'rb')
88
words = pickle.load(file)
9-
a = words[word[0]]
9+
a = words[word[0].lower()]
1010
a.append(word)
11-
b = word[0]
11+
b = word[0].lower()
1212
words[b] = a
1313
file.close()
1414

games/Word-Building/word_building.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def Main():
5454
used_words.add(user_word)
5555

5656
# Add user's word to the data if it's not already there
57-
if user_word not in data.words[user_word[0]]:
57+
if user_word not in data.words[user_word[0].lower()]:
5858
data.DataAdding(user_word)
5959
time.sleep(1)
6060

0 commit comments

Comments
 (0)