import tomllib from datetime import datetime from pathlib import Path import json import enum from pydantic import BaseModel, SecretStr, constr import secrets from fastapi import FastAPI, HTTPException, Security from fastapi.openapi.models import APIKey from fastapi.security.api_key import APIKeyHeader from starlette.status import HTTP_403_FORBIDDEN import sys class Settings(BaseModel): api_key: SecretStr path_support_reports: Path with open(Path(__file__).parent / 'settings.toml', 'r') as f: settings_dict = tomllib.loads(f.read()) SETTINGS = Settings(**settings_dict) #################### # - Utilities #################### api_key_header = APIKeyHeader( name="access_token", auto_error=False, ) async def g_api_key( api_key_header: APIKey = Security(api_key_header), ) -> APIKey: if api_key_header and secrets.compare_digest( str(api_key_header), SETTINGS.api_key.get_secret_value(), ): return api_key_header raise HTTPException( status_code=HTTP_403_FORBIDDEN, detail="Wrong API key.", ) #################### # - Types #################### class PyInstSupTag(enum.StrEnum): INST_WIN_OFFICIAL = enum.auto() INST_WIN_WINSTORE = enum.auto() INST_MAC_OFFICIAL = enum.auto() INST_MAC_HOMEBREW = enum.auto() INST_LIN_OFFICIAL = enum.auto() INST_LIN_PKG = enum.auto() COURSE_MATH1A = enum.auto() COURSE_INTROPROG = enum.auto() ENV_PIP = enum.auto() ENV_CONDA = enum.auto() IDE_VSCODE = enum.auto() IDE_PYCHARM = enum.auto() IDE_SPYDER = enum.auto() IDE_NOTEPADPP = enum.auto() class ClockOption(enum.IntEnum): CLOCK_IN = enum.auto() CLOCK_OUT = enum.auto() class AttendanceEntry(BaseModel): at: datetime status: ClockOption class SupportEntry(BaseModel): study_id: constr(pattern = r'^s[0-9]{6}$') tags: list[PyInstSupTag] description: str #################### # - FastAPI App #################### app = FastAPI( #prefix="/v1", dependencies=[Security(g_api_key)], ) #################### # - Support #################### @app.get("/report/support") async def g_support_entry() -> list[SupportEntry]: """Report an instance of granted Python Installation support.""" with open(SETTINGS.path_support_reports, 'r') as f: return [ SupportEntry(support_entry_snippet) for support_entry_snippet in f.readlines() ] @app.post("/report/support") async def mk_support_entry( support_entry: SupportEntry, ) -> bool: """Print granted Python Support.""" with open(SETTINGS.path_support_reports, 'a') as f: print( json.dumps(support_entry.model_dump()), file = f, ) return True #################### # - Hours #################### @app.get("/report/hours") async def g_hours_entry() -> list[SupportEntry]: """Print support attendance record.""" with open(SETTINGS.path_support_reports, 'r') as f: return [ SupportEntry(support_entry_snippet) for support_entry_snippet in f.readlines() ] @app.post("/report/hours") async def mk_hours_entry( attendance_entry: AttendanceEntry, ) -> bool: """Clock in / out to Python support.""" with open(SETTINGS.path_support_reports, 'a') as f: print( json.dumps(attendance_entry.model_dump()), file = f, ) return True