From 9a422a00beddff5179a4e116a521e645ed1d6ab8 Mon Sep 17 00:00:00 2001 From: Edwin Hermans Date: Sat, 11 May 2024 20:22:49 -0400 Subject: [PATCH 1/5] Add debug flag --- .pre-commit-config.yaml | 5 +++- moocfi_cses.py | 61 ++++++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 29 deletions(-) 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..10a5f5f 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?) @@ -39,7 +38,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 +109,37 @@ 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", "all"], + default="all", + ) return parser.parse_args(args) @@ -154,9 +158,10 @@ 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(configfile).expanduser() print("Writing config to file") - with open(config_file, "w") as f: + with open(file, "w") as f: json.dump(config, f) @@ -231,7 +236,7 @@ def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]: class Task: id: str name: str - complete: bool + state: str # NOTE: I could simply use html2text to output the list of tasks @@ -260,14 +265,14 @@ def parse_task_list(html: str | bytes) -> list[Task]: task = Task( id=item_id, name=item_name, - complete="full" in item_class, + state="complete" if "full" in item_class else "incomplete", ) task_list.append(task) return task_list -TASK_DONE_ICON = {True: "✅", False: "❌"} +TASK_DONE_ICON = {"complete": "✅", "incomplete": "❌"} # TODO: todo todo @@ -287,7 +292,7 @@ def submit_task(task_id: str, filename: str) -> None: # TODO: todo todo todo def parse_task(html: str | bytes, task: Task) -> Task: - task = Task("a", "b", True) + task = Task("a", "b", "complete") return task @@ -295,7 +300,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,7 +319,6 @@ 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(): @@ -338,7 +342,8 @@ def main() -> None: 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]}") + if args.filter == "all" or args.filter == task.state: + print(f"- {task.id}: {task.name} {TASK_DONE_ICON[task.state]}") if __name__ == "__main__": From f35f765a135076b14ac615816de0f9b9ae90143e Mon Sep 17 00:00:00 2001 From: Edwin Hermans Date: Sat, 11 May 2024 22:12:42 -0400 Subject: [PATCH 2/5] Implement Enum instead of dict --- moocfi_cses.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/moocfi_cses.py b/moocfi_cses.py index 10a5f5f..fa904fc 100644 --- a/moocfi_cses.py +++ b/moocfi_cses.py @@ -6,6 +6,7 @@ # TODO: UI for submitting solutions import argparse from dataclasses import dataclass, field +from enum import Enum, auto from getpass import getpass import logging import json @@ -232,11 +233,22 @@ def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]: return form_data +class TaskState(Enum): + COMPLETE = auto() + INCOMPLETE = auto() + + +TASK_DONE_ICON = { + TaskState.COMPLETE: "✅", + TaskState.INCOMPLETE: "❌", +} + + @dataclass class Task: id: str name: str - state: str + state: TaskState # NOTE: I could simply use html2text to output the list of tasks @@ -265,16 +277,15 @@ def parse_task_list(html: str | bytes) -> list[Task]: task = Task( id=item_id, name=item_name, - state="complete" if "full" in item_class else "incomplete", + state=TaskState.COMPLETE + if "full" in item_class + else TaskState.INCOMPLETE, ) task_list.append(task) return task_list -TASK_DONE_ICON = {"complete": "✅", "incomplete": "❌"} - - # TODO: todo todo def print_task_list(html: str | bytes) -> None: "i❌ ✅ X or ✔" @@ -292,7 +303,7 @@ def submit_task(task_id: str, filename: str) -> None: # TODO: todo todo todo def parse_task(html: str | bytes, task: Task) -> Task: - task = Task("a", "b", "complete") + task = Task("a", "b", TaskState.COMPLETE) return task @@ -342,7 +353,7 @@ def main() -> None: html = session.http_request(base_url) task_list = parse_task_list(html) for task in task_list: - if args.filter == "all" or args.filter == task.state: + if args.filter == "all" or args.filter == task.state.name.lower(): print(f"- {task.id}: {task.name} {TASK_DONE_ICON[task.state]}") From 3d7e3b51ad30956d51a291839f9213cbb093d11a Mon Sep 17 00:00:00 2001 From: Edwin Hermans Date: Sun, 12 May 2024 14:09:19 -0400 Subject: [PATCH 3/5] Replace task state dict with Enum --- moocfi_cses.py | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/moocfi_cses.py b/moocfi_cses.py index fa904fc..f88610b 100644 --- a/moocfi_cses.py +++ b/moocfi_cses.py @@ -138,8 +138,7 @@ def parse_args(args: Optional[list[str]] = None) -> argparse.Namespace: parser_list.add_argument( "--filter", help="List complete, incomplete or all tasks (default: %(default)s)", - choices=["complete", "incomplete", "all"], - default="all", + choices=["complete", "incomplete"], ) return parser.parse_args(args) @@ -158,11 +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(configfile: str, config: dict[str, str]) -> None: - file = Path(configfile).expanduser() + 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(file, "w") as f: + with open(file_path, "w") as f: json.dump(config, f) @@ -170,8 +173,8 @@ def write_config(configfile: 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 @@ -234,11 +237,11 @@ def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]: class TaskState(Enum): - COMPLETE = auto() - INCOMPLETE = auto() + COMPLETE = "complete" + INCOMPLETE = "incompletE" -TASK_DONE_ICON = { +TASK_STATE_ICON = { TaskState.COMPLETE: "✅", TaskState.INCOMPLETE: "❌", } @@ -251,9 +254,7 @@ class Task: 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"]') @@ -286,22 +287,21 @@ def parse_task_list(html: str | bytes) -> list[Task]: return task_list -# 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", TaskState.COMPLETE) return task @@ -333,7 +333,7 @@ def main() -> None: 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)) @@ -352,9 +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: - if args.filter == "all" or args.filter == task.state.name.lower(): - print(f"- {task.id}: {task.name} {TASK_DONE_ICON[task.state]}") + print_task_list(task_list, filter=args.filter) if __name__ == "__main__": From 8978e6158be728b4f3c0ad235c35201c64fb01b1 Mon Sep 17 00:00:00 2001 From: Edwin Hermans Date: Sun, 12 May 2024 14:35:59 -0400 Subject: [PATCH 4/5] Update moocfi_cses.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- moocfi_cses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moocfi_cses.py b/moocfi_cses.py index f88610b..fbed65a 100644 --- a/moocfi_cses.py +++ b/moocfi_cses.py @@ -6,7 +6,7 @@ # TODO: UI for submitting solutions import argparse from dataclasses import dataclass, field -from enum import Enum, auto +from enum import Enum from getpass import getpass import logging import json From b16aeab3f54fb0263dc20d57e60fdc0e9d1bf310 Mon Sep 17 00:00:00 2001 From: Edwin Hermans Date: Sun, 12 May 2024 14:39:11 -0400 Subject: [PATCH 5/5] Update moocfi_cses.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- moocfi_cses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moocfi_cses.py b/moocfi_cses.py index fbed65a..77c0a19 100644 --- a/moocfi_cses.py +++ b/moocfi_cses.py @@ -238,7 +238,7 @@ def parse_form(html: AnyStr, xpath: str = ".//form") -> dict[str, str | None]: class TaskState(Enum): COMPLETE = "complete" - INCOMPLETE = "incompletE" + INCOMPLETE = "incomplete" TASK_STATE_ICON = {