Assignment 2

This commit is contained in:
Uthman Fatih 2026-08-31 20:08:56 +01:00
parent 9207cc2eb1
commit 53ea335f9e
9 changed files with 405 additions and 1 deletions

View File

@ -17,7 +17,6 @@ def transcript(text: str) -> str:
)
)
class Rot13App(App):
"""A Textual app for ROT13 encryption and decryption."""

246
Practice Assignment 2/main.py Executable file
View File

@ -0,0 +1,246 @@
#!/usr/bin/env python3
import os
import sys
from collections import defaultdict
from datetime import datetime
# Constants for fitness levels
FITNESS_LEVELS = {
(0, 9.0): "Fitness level below pace for long games",
(9.1, 10.0): "Fitness level for amateur player",
(10.1, 11.0): "Fitness level for above average amateur player",
(11.1, 12.0): "Fitness level for elite player",
(12.1, 13.0): "Very high fitness level"
}
class ValidationError(Exception):
pass
class PlayerFitnessData:
def __init__(self, date, player, level, laps, speed, lap_time):
self.date = date
self.player = player
self.level = level
self.laps = laps
self.speed = speed
self.lap_time = lap_time
def __repr__(self):
return f"PlayerFitnessData({self.player}, {self.date})"
class PlayerFitnessAnalyzer:
def __init__(self, data_file):
self.data_file = data_file
self.player_data = defaultdict(list)
self.errors = []
def validate_data(self, date, player, level, laps, speed, lap_time):
# Validate date format (DD Mon)
if not isinstance(date, str) or len(date.split()) != 2:
raise ValidationError(f"Invalid date format: {date}. Expected 'DD Mon'")
if not isinstance(player, str) or not player.strip():
raise ValidationError("Player name cannot be empty")
try:
level = float(level)
if level < 0 or level > 13:
raise ValidationError(f"Level {level} out of range (0-13)")
except ValueError:
raise ValidationError(f"Level must be numeric: {level}")
try:
laps = int(laps)
if laps <= 0:
raise ValidationError(f"Laps must be positive: {laps}")
except ValueError:
raise ValidationError(f"Laps must be integer: {laps}")
try:
speed = float(speed)
if speed <= 0:
raise ValidationError(f"Speed must be positive: {speed}")
except ValueError:
raise ValidationError(f"Speed must be numeric: {speed}")
try:
lap_time = float(lap_time)
if lap_time <= 0:
raise ValidationError(f"Lap time must be positive: {lap_time}")
except ValueError:
raise ValidationError(f"Lap time must be numeric: {lap_time}")
return level, laps, speed, lap_time
def load_data(self):
"""
Load and parse player data from file.
Expected format (space/tab separated):
Date Player Level Laps Speed Lap Time(s)
11 Jan Harry Keane 9 11 12.5 5.76
...
"""
if not os.path.exists(self.data_file):
raise FileNotFoundError(f"Data file not found: {self.data_file}")
try:
with open(self.data_file, 'r') as f:
lines = f.readlines()
except IOError as e:
raise IOError(f"Cannot read file {self.data_file}: {e}")
if not lines:
raise ValidationError("Data file is empty")
# Skip header line if present
start_line = 0
if lines[0].strip().lower().startswith('date'):
start_line = 1
for line_num, line in enumerate(lines[start_line:], start=start_line + 1):
line = line.strip()
if not line or line.startswith('#'):
continue
try:
parts = line.split()
if len(parts) < 8:
self.errors.append(f"Line {line_num}: Not enough fields: {line}")
continue
date = f"{parts[0]} {parts[1]}"
player = f"{parts[2]} {parts[3]}"
level_str = parts[4]
laps_str = parts[5]
speed_str = parts[6]
lap_time_str = parts[7]
level, laps, speed, lap_time = self.validate_data(
date, player, level_str, laps_str, speed_str, lap_time_str
)
data = PlayerFitnessData(date, player, level, laps, speed, lap_time)
self.player_data[player].append(data)
except ValidationError as e:
self.errors.append(f"Line {line_num}: {str(e)}")
except (IndexError, ValueError) as e:
self.errors.append(f"Line {line_num}: Parse error: {str(e)}")
def get_fitness_level_description(self, average_level):
for (min_val, max_val), description in FITNESS_LEVELS.items():
if min_val <= average_level <= max_val:
return description
return "Unknown fitness level"
def analyze_player(self, player_name, weeks=4):
if player_name not in self.player_data:
return None
data_list = self.player_data[player_name]
if len(data_list) < weeks:
recent_data = data_list
weeks_used = len(data_list)
else:
recent_data = data_list[-weeks:]
weeks_used = weeks
if not recent_data:
return None
avg_level = sum(d.level for d in recent_data) / len(recent_data)
avg_laps = sum(d.laps for d in recent_data) / len(recent_data)
avg_speed = sum(d.speed for d in recent_data) / len(recent_data)
avg_lap_time = sum(d.lap_time for d in recent_data) / len(recent_data)
return {
'player': player_name,
'weeks_analyzed': weeks_used,
'average_level': round(avg_level, 2),
'average_laps': round(avg_laps, 2),
'average_speed': round(avg_speed, 2),
'average_lap_time': round(avg_lap_time, 2),
'fitness_description': self.get_fitness_level_description(avg_level)
}
def generate_report(self):
if not self.player_data:
print("No player data loaded.")
return
print("\n" + "=" * 80)
print("PENDLETOWN FS - MONTHLY FITNESS REPORT")
print("=" * 80)
for player_name in sorted(self.player_data.keys()):
summary = self.analyze_player(player_name)
if summary:
print(f"\nPlayer: {summary['player']}")
print("-" * 80)
print(f" Weeks Analyzed: {summary['weeks_analyzed']}")
print(f" Average Level: {summary['average_level']:.2f}")
print(f" Average Laps: {summary['average_laps']:.2f}")
print(f" Average Speed: {summary['average_speed']:.2f} km/h")
print(f" Average Lap Time: {summary['average_lap_time']:.2f} seconds")
print(f"\n Fitness Level: {summary['fitness_description']}")
print("\n" + "=" * 80 + "\n")
def print_errors(self):
if self.errors:
print("\n" + "!" * 80)
print("DATA VALIDATION ERRORS:")
print("!" * 80)
for error in self.errors:
print(f" {error}")
print("!" * 80 + "\n")
return True
return False
def main():
data_file = "player_data.txt"
if len(sys.argv) > 1:
data_file = sys.argv[1]
print(f"\nLoading player data from: {data_file}")
try:
analyzer = PlayerFitnessAnalyzer(data_file)
analyzer.load_data()
has_errors = analyzer.print_errors()
if analyzer.player_data:
analyzer.generate_report()
if has_errors:
print("Note: Some lines had errors but valid data was processed.\n")
else:
print("No valid data to process.")
sys.exit(1)
except FileNotFoundError as e:
print(f"ERROR: {e}")
sys.exit(1)
except IOError as e:
print(f"ERROR: {e}")
sys.exit(1)
except ValidationError as e:
print(f"ERROR: {e}")
sys.exit(1)
except Exception as e:
print(f"UNEXPECTED ERROR: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
Date Player Level Laps Speed Lap Time(s)
11 Jan Harry Keane 9 11 12.5 5.76
11 Jan Nat Sturridge 8 10 11.5 6.55
11 Jan Dwayne Vardy 10 11 13 5.54
11 Jan Kyle Baker 8 10 12 6
11 Jan Elliott Ramsey 10 11 13 5.54
18 Jan Harry Keane 9.2 11 12.6 5.74
18 Jan Nat Sturridge 8.1 10 11.6 6.52
18 Jan Dwayne Vardy 10.1 11 13.1 5.53
18 Jan Kyle Baker 8.3 11 12.2 5.95
18 Jan Elliott Ramsey 10.2 11 13.1 5.52
25 Jan Harry Keane 9.4 12 12.8 5.70
25 Jan Nat Sturridge 8.3 11 11.8 6.48
25 Jan Dwayne Vardy 10.3 12 13.2 5.50
25 Jan Kyle Baker 8.5 11 12.4 5.90
25 Jan Elliott Ramsey 10.4 12 13.2 5.50
01 Feb Harry Keane 9.6 12 13 5.68
01 Feb Nat Sturridge 8.5 11 12 6.45
01 Feb Dwayne Vardy 10.5 12 13.3 5.48
01 Feb Kyle Baker 8.7 12 12.6 5.85
01 Feb Elliott Ramsey 10.6 12 13.3 5.48

1
test/.python-version Normal file
View File

@ -0,0 +1 @@
3.14

0
test/README.md Normal file
View File

17
test/main.py Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
import typer
from typing import Annotated
app = typer.Typer()
@app.command()
def goodbye(name: Annotated[str, typer.Option(prompt="What is your name?")] = "World"):
typer.echo(f"goodbye {name}")
@app.command()
def main():
typer.echo(f"Hello")
if __name__ == "__main__":
app()

19
test/pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[project]
name = "test"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Uthman Fatih", email = "kingvcheese@gmail.com" }
]
requires-python = ">=3.14"
dependencies = [
"typer>=0.27.2",
]
[project.scripts]
test = "test:main"
[build-system]
requires = ["uv_build>=0.12.7,<0.13.0"]
build-backend = "uv_build"

View File

@ -0,0 +1,2 @@
def main() -> None:
print("Hello from test!")

99
test/uv.lock generated Normal file
View File

@ -0,0 +1,99 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "test"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "typer" },
]
[package.metadata]
requires-dist = [{ name = "typer", specifier = ">=0.27.2" }]
[[package]]
name = "typer"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
]