diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc3e715..bf50f5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,10 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-yaml + - id: debug-statements + - id: check-merge-conflict + - id: check-json + - id: no-commit-to-branch - repo: https://github.com/psf/black rev: 22.10.0 hooks: diff --git a/moocfi_cses.py b/moocfi_cses.py index c2ba272..77c0a19 100644 --- a/moocfi_cses.py +++ b/moocfi_cses.py @@ -1,4 +1,3 @@ -# TEST: is the `click` library a useful option? # TODO: if config doesn't exist fail and ask to run config creation # TODO: make sure the correct directories exist # TODO: check validity of config after creation (can we log in?) @@ -7,6 +6,7 @@ # TODO: UI for submitting solutions import argparse from dataclasses import dataclass, field +from enum import Enum from getpass import getpass import logging import json @@ -39,7 +39,7 @@ def is_logged_in(self) -> bool: login_text = login_link.get("text") or "" return self.username in login_text - # TODO: add a debug flag/verbose flag and allow printing of html and forms + # TODO: create custom exceptions def login(self) -> None: """Logs into the site using webscraping @@ -110,32 +110,36 @@ def parse_args(args: Optional[list[str]] = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Interact with mooc.fi CSES instance") parser.add_argument("--username", help="tmc.mooc.fi username") parser.add_argument("--password", help="tmc.mooc.fi password") - ( - parser.add_argument( - "--course", - help="SLUG of the course (default: %(default)s)", - default="dsa24k", - ), - ) # pyright: ignore[reportUnusedExpression] - ( - parser.add_argument( - "--config", - help="Location of config file (default: %(default)s)", - default="~/.config/moocfi_cses/config.json", - ), - ) # pyright: ignore[reportUnusedExpression] + parser.add_argument( + "--debug", help="set logging level to debug", action="store_true" + ) + parser.add_argument( + "--course", + help="SLUG of the course (default: %(default)s)", + default="dsa24k", + ) + parser.add_argument( + "--config", + help="Location of config file (default: %(default)s)", + default="~/.config/moocfi_cses/config.json", + ) parser.add_argument( "--no-state", + help="Don't store cookies or cache (they're used for faster access on the future runs)", action="store_true", - help="Don't store cookies or cache (used for faster access on the future runs)", ) subparsers = parser.add_subparsers(required=True) - parser_config = subparsers.add_parser("configure", help="configure moocfi_cses") + parser_config = subparsers.add_parser("configure", help="Configure moocfi_cses") parser_config.set_defaults(cmd="configure") - parser_list = subparsers.add_parser("list", help="list exercises") + parser_list = subparsers.add_parser("list", help="List exercises") parser_list.set_defaults(cmd="list") + parser_list.add_argument( + "--filter", + help="List complete, incomplete or all tasks (default: %(default)s)", + choices=["complete", "incomplete"], + ) return parser.parse_args(args) @@ -153,10 +157,15 @@ def create_config() -> dict[str, str]: # TODO: check if file exists and ask permission to overwrite -# TODO: check if path exists, otherwise create -def write_config(config_file: str, config: dict[str, str]) -> None: +def write_config(configfile: str, config: dict[str, str]) -> None: + file_path = Path(configfile).expanduser() + if file_path.exists(): + # TODO: check if file exists and ask permission to overwrite + # Prompt user or handle file overwrite scenario + ... + file_path.parent.mkdir(parents=True, exist_ok=True) # Ensure directory exists print("Writing config to file") - with open(config_file, "w") as f: + with open(file_path, "w") as f: json.dump(config, f) @@ -164,8 +173,8 @@ def write_config(config_file: str, config: dict[str, str]) -> None: # TODO: try/except around open and json.load, return empty dict on failure def read_config(configfile: str) -> dict[str, str]: config = dict() - file = Path(configfile).expanduser() - with open(file, "r") as f: + file_path = Path(configfile).expanduser() + with open(file_path, "r") as f: config = json.load(f) for setting in ("username", "password"): assert setting in config @@ -227,16 +236,25 @@ def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]: return form_data +class TaskState(Enum): + COMPLETE = "complete" + INCOMPLETE = "incomplete" + + +TASK_STATE_ICON = { + TaskState.COMPLETE: "✅", + TaskState.INCOMPLETE: "❌", +} + + @dataclass class Task: id: str name: str - complete: bool + state: TaskState -# NOTE: I could simply use html2text to output the list of tasks -# it needs some work to replace the list[Task]: """Parse html to find tasks and their status, return something useful, possibly a specific data class""" content_element = htmlement.fromstring(html).find('.//div[@class="content"]') @@ -260,34 +278,32 @@ def parse_task_list(html: str | bytes) -> list[Task]: task = Task( id=item_id, name=item_name, - complete="full" in item_class, + state=TaskState.COMPLETE + if "full" in item_class + else TaskState.INCOMPLETE, ) task_list.append(task) return task_list -TASK_DONE_ICON = {True: "✅", False: "❌"} - - -# TODO: todo todo -def print_task_list(html: str | bytes) -> None: - "i❌ ✅ X or ✔" - print("These are you tasks") - print("these are the args") - print(html) +# TODO: This should be part of a UI class or module +def print_task_list(task_list: list[Task], filter: Optional[str] = None) -> None: + for task in task_list: + if not filter or filter == task.state.value: + print(f"- {task.id}: {task.name} {TASK_STATE_ICON[task.state]}") -# TODO: todo todo todo +# TODO: Implement function that posts the submit form with the correct file def submit_task(task_id: str, filename: str) -> None: """submit file to the submit form or task_id""" # NOTE: use parse_form ... -# TODO: todo todo todo +# TODO: Implement function that parser the specific task page into Task object def parse_task(html: str | bytes, task: Task) -> Task: - task = Task("a", "b", True) + task = Task("a", "b", TaskState.COMPLETE) return task @@ -295,7 +311,7 @@ def main() -> None: args = parse_args() logging.basicConfig( - level=logging.WARNING, + level=logging.DEBUG if args.debug else logging.WARNING, format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) @@ -314,11 +330,10 @@ def main() -> None: cookiefile = None cookies = dict() - print(args) if not args.no_state: state_dir = Path("~/.local/state/moocfi_cses").expanduser() if not state_dir.exists(): - state_dir.mkdir(parents=True) + state_dir.mkdir(parents=True, exist_ok=True) cookiefile = state_dir / "cookies.txt" cookies = read_cookie_file(str(cookiefile)) @@ -337,8 +352,7 @@ def main() -> None: if args.cmd == "list": html = session.http_request(base_url) task_list = parse_task_list(html) - for task in task_list: - print(f"- {task.id}: {task.name} {TASK_DONE_ICON[task.complete]}") + print_task_list(task_list, filter=args.filter) if __name__ == "__main__":