From fa347b8af47e94f9eb8bf57ac7c7579b5167436a Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 12:04:00 +0330 Subject: [PATCH 1/6] feat: Makefile added --- Makefile | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..05667ba --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +.DEFAULT_GOAL := help +.PHONY: docs +SRC_DIRS = ./tutorxqueue +BLACK_OPTS = --exclude templates ${SRC_DIRS} + +# Warning: These checks are not necessarily run on every PR. +test: test-lint test-types test-format # Run some static checks. + +test-format: ## Run code formatting tests + black --check --diff $(BLACK_OPTS) + +test-lint: ## Run code linting tests + pylint --errors-only --enable=unused-import,unused-argument --ignore=templates --ignore=docs/_ext ${SRC_DIRS} + +test-types: ## Run type checks. + mypy --exclude=templates --ignore-missing-imports --implicit-reexport --strict ${SRC_DIRS} + +format: ## Format code automatically + black $(BLACK_OPTS) + +isort: ## Sort imports. This target is not mandatory because the output may be incompatible with black formatting. Provided for convenience purposes. + isort --skip=templates ${SRC_DIRS} + +changelog-entry: ## Create a new changelog entry. + scriv create + +changelog: ## Collect changelog entries in the CHANGELOG.md file. + scriv collect + +ESCAPE =  +help: ## Print this help + @grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \ + | sed 's/######* \(.*\)/@ $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' | tr '@' '\n' \ + | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}' From 2da4ea51bb34d2cb291add14a70e1313cbb671bb Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 12:04:03 +0330 Subject: [PATCH 2/6] ci: test action added --- .github/workflows/test.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..829f613 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Run tests + +on: + pull_request: + branches: [master] + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Upgrade pip + run: python -m pip install --upgrade pip setuptools + - name: Install dependencies + run: | + pip install .[dev] + - name: Test lint, types, and format + run: make test From 3069192e1253e9c55fda409ee6eed5a63b2dd266 Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 12:04:07 +0330 Subject: [PATCH 3/6] chore: changelog entry added --- changelog.d/20231118_161232_codewithemad.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/20231118_161232_codewithemad.md diff --git a/changelog.d/20231118_161232_codewithemad.md b/changelog.d/20231118_161232_codewithemad.md new file mode 100644 index 0000000..952a383 --- /dev/null +++ b/changelog.d/20231118_161232_codewithemad.md @@ -0,0 +1 @@ +- [Improvement] Added Typing to code, Makefile and test action to the repository and formatted code with Black and isort. (by @CodeWithEmad) \ No newline at end of file From 38d7edad08c6223117f6993594db99098904ffd0 Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 15:38:15 +0330 Subject: [PATCH 4/6] chore: cleanup with black and isort --- tutorxqueue/__about__.py | 1 - tutorxqueue/plugin.py | 50 ++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/tutorxqueue/__about__.py b/tutorxqueue/__about__.py index 99e36f2..eabf3d3 100644 --- a/tutorxqueue/__about__.py +++ b/tutorxqueue/__about__.py @@ -1,2 +1 @@ __version__ = "16.0.2" - diff --git a/tutorxqueue/plugin.py b/tutorxqueue/plugin.py index 727f5ac..d17541f 100644 --- a/tutorxqueue/plugin.py +++ b/tutorxqueue/plugin.py @@ -8,11 +8,11 @@ import click import pkg_resources import requests - from tutor import config as tutor_config from tutor.__about__ import __version_suffix__ from tutor import exceptions from tutor import hooks as tutor_hooks +from tutor.__about__ import __version_suffix__ from .__about__ import __version__ @@ -21,11 +21,6 @@ __version__ += "-" + __version_suffix__ config = { - "unique": { - "AUTH_PASSWORD": "{{ 8|random_string }}", - "MYSQL_PASSWORD": "{{ 8|random_string }}", - "SECRET_KEY": "{{ 24|random_string }}", - }, "defaults": { "VERSION": __version__, "AUTH_USERNAME": "lms", @@ -36,6 +31,11 @@ "REPOSITORY": "https://github.com/openedx/xqueue", "REPOSITORY_VERSION": "{{ OPENEDX_COMMON_VERSION }}", }, + "unique": { + "AUTH_PASSWORD": "{{ 8|random_string }}", + "MYSQL_PASSWORD": "{{ 8|random_string }}", + "SECRET_KEY": "{{ 24|random_string }}", + }, } # Initialization hooks @@ -61,21 +61,27 @@ tutor_hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task)) # Image management -tutor_hooks.Filters.IMAGES_BUILD.add_item(( - "xqueue", - ("plugins", "xqueue", "build", "xqueue"), - "{{ XQUEUE_DOCKER_IMAGE }}", - (), -)) - -tutor_hooks.Filters.IMAGES_PULL.add_item(( - "xqueue", - "{{ XQUEUE_DOCKER_IMAGE }}", -)) -tutor_hooks.Filters.IMAGES_PUSH.add_item(( - "xqueue", - "{{ XQUEUE_DOCKER_IMAGE }}", -)) +tutor_hooks.Filters.IMAGES_BUILD.add_item( + ( + "xqueue", + ("plugins", "xqueue", "build", "xqueue"), + "{{ XQUEUE_DOCKER_IMAGE }}", + (), + ) +) + +tutor_hooks.Filters.IMAGES_PULL.add_item( + ( + "xqueue", + "{{ XQUEUE_DOCKER_IMAGE }}", + ) +) +tutor_hooks.Filters.IMAGES_PUSH.add_item( + ( + "xqueue", + "{{ XQUEUE_DOCKER_IMAGE }}", + ) +) @tutor_hooks.Filters.COMPOSE_MOUNTS.add() @@ -245,7 +251,6 @@ def request(self, endpoint, method="GET", data=None, params=None): submissions.add_command(grade_submission) command.add_command(submissions) -####### Boilerplate code # Add the "templates" folder as a template root tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_item( pkg_resources.resource_filename("tutorxqueue", "templates") @@ -288,6 +293,7 @@ def request(self, endpoint, method="GET", data=None, params=None): # Xqueue Public Host ######################################## + @tutor_hooks.Filters.APP_PUBLIC_HOSTS.add() def _xqueue_public_hosts( hosts: list[str], context_name: t.Literal["local", "dev"] From de9a76125ce638fbeca1119f4642f519dfce2260 Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 15:40:30 +0330 Subject: [PATCH 5/6] feat: typing added --- tutorxqueue/plugin.py | 92 +++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/tutorxqueue/plugin.py b/tutorxqueue/plugin.py index d17541f..0d3b095 100644 --- a/tutorxqueue/plugin.py +++ b/tutorxqueue/plugin.py @@ -2,14 +2,13 @@ import json import os -import typing as t from glob import glob +from typing import Any, Literal, Optional, Union import click import pkg_resources -import requests +import requests # type: ignore from tutor import config as tutor_config -from tutor.__about__ import __version_suffix__ from tutor import exceptions from tutor import hooks as tutor_hooks from tutor.__about__ import __version_suffix__ @@ -20,7 +19,7 @@ if __version_suffix__: __version__ += "-" + __version_suffix__ -config = { +config: dict[str, dict[str, Any]] = { "defaults": { "VERSION": __version__, "AUTH_USERNAME": "lms", @@ -85,7 +84,7 @@ @tutor_hooks.Filters.COMPOSE_MOUNTS.add() -def _mount_xqueue(volumes, name): +def _mount_xqueue(volumes: list[tuple[str, str]], name: str) -> list[tuple[str, str]]: """ When mounting xqueue with `--mount=/path/to/xqueue`, bind-mount the host repo in the xqueue container. @@ -100,11 +99,11 @@ def _mount_xqueue(volumes, name): @click.group(help="Interact with the Xqueue server", name="xqueue") -def command(): +def command() -> None: pass -@click.group(help="List and grade submissions") +@click.group(help="list and grade submissions") @click.pass_obj @click.option("-q", "--queue", default="openedx", show_default=True, help="Queue name") @click.option( @@ -117,21 +116,21 @@ def command(): "from the TUTOR_XQUEUE_URL environment variable." ), ) -def submissions(context, queue, url): - context.queue = queue - context.url = url +def submissions(context: click.Context, queue: str, url: str) -> None: + context.queue = queue # type: ignore + context.url = url # type: ignore @click.command(name="count", help="Count submissions in queue") @click.pass_obj -def count_submissions(context): - print_result(context, "count_submissions", context.queue) +def count_submissions(context: click.Context) -> None: + print_result(context, "count_submissions", context.queue) # type: ignore @click.command(name="show", help="Show last submission") @click.pass_obj -def show_submission(context): - print_result(context, "show_submission", context.queue) +def show_submission(context: click.Context) -> None: + print_result(context, "show_submission", context.queue) # type: ignore @click.command(name="grade", help="Grade a specific submission") @@ -141,29 +140,43 @@ def show_submission(context): @click.argument("correct", type=click.BOOL) @click.argument("message") @click.pass_obj -def grade_submission(context, submission_id, submission_key, grade, correct, message): +def grade_submission( + context: click.Context, + submission_id: str, + submission_key: str, + grade: str, + correct: str, + message: str, +) -> None: print_result( context, "grade_submission", - submission_id, - submission_key, - grade, - correct, - message, + ( + submission_id, + submission_key, + grade, + correct, + message, + ), ) -def print_result(context, client_func_name, *args, **kwargs): - user_config = tutor_config.load(context.root) - client = Client(user_config, url=context.url) +def print_result( + context: click.Context, + client_func_name: str, + *args: tuple[Any, ...], + **kwargs: dict[str, Any], +) -> None: + user_config = tutor_config.load(context.root) # type: ignore + client = Client(user_config, url=context.url) # type: ignore func = getattr(client, client_func_name) result = func(*args, **kwargs) print(json.dumps(result, indent=2)) class Client: - def __init__(self, user_config, url=None): - self._session = None + def __init__(self, user_config: dict[str, Any], url: str = "") -> None: + self._session: Optional[requests.Session] = None self.username = user_config["XQUEUE_AUTH_USERNAME"] self.password = user_config["XQUEUE_AUTH_PASSWORD"] @@ -175,17 +188,17 @@ def __init__(self, user_config, url=None): self.login() @property - def session(self): + def session(self) -> requests.Session: if self._session is None: self._session = requests.Session() return self._session - def url(self, endpoint): + def url(self, endpoint: str) -> str: # Don't forget to add a trailing slash to all endpoints: this is how xqueue # works... return self.base_url + endpoint - def login(self): + def login(self) -> None: response = self.request( "/xqueue/login/", method="POST", @@ -199,7 +212,7 @@ def login(self): ) ) - def show_submission(self, queue): + def show_submission(self, queue: str) -> Union[dict[str, Any], Any]: response = self.request("/xqueue/get_submission/", params={"queue_name": queue}) if response["return_code"] != 0: return response @@ -222,10 +235,17 @@ def show_submission(self, queue): "return_code": response["return_code"], } - def count_submissions(self, queue): + def count_submissions(self, queue: str) -> Any: return self.request("/xqueue/get_queuelen/", params={"queue_name": queue}) - def grade_submission(self, submission_id, submission_key, grade, correct, msg): + def grade_submission( + self, + submission_id: str, + submission_key: str, + grade: str, + correct: bool, + msg: str, + ) -> Any: return self.request( "/xqueue/put_result/", method="POST", @@ -239,7 +259,13 @@ def grade_submission(self, submission_id, submission_key, grade, correct, msg): }, ) - def request(self, endpoint, method="GET", data=None, params=None): + def request( + self, + endpoint: str, + method: str = "GET", + data: Optional[dict[str, Any]] = None, + params: Optional[dict[str, Any]] = None, + ) -> Any: func = getattr(self.session, method.lower()) response = func(self.url(endpoint), data=data, params=params) # TODO handle errors >= 400 and non-parsable json responses @@ -296,7 +322,7 @@ def request(self, endpoint, method="GET", data=None, params=None): @tutor_hooks.Filters.APP_PUBLIC_HOSTS.add() def _xqueue_public_hosts( - hosts: list[str], context_name: t.Literal["local", "dev"] + hosts: list[str], context_name: Literal["local", "dev"] ) -> list[str]: if context_name == "dev": hosts += ["{{ XQUEUE_HOST }}:8000"] From 255a13081f505766625aa1710df99bf444904763 Mon Sep 17 00:00:00 2001 From: Emad Rad Date: Fri, 24 Nov 2023 23:11:00 +0330 Subject: [PATCH 6/6] feat: Added 'dev' requirements to 'extras_require' in setup These include 'tutor' (version 16.x) for development and 'types-requests' (version 2.31.0.0). This addition helps in managing package dependencies during development. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 2b65ca5..b157bb8 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,9 @@ include_package_data=True, python_requires=">=3.8", install_requires=["tutor>=16.0.0,<17.0.0", "requests"], + extras_require={ + "dev": ["tutor[dev]>=16.0.0,<17.0.0"], + }, entry_points={"tutor.plugin.v1": ["xqueue = tutorxqueue.plugin"]}, classifiers=[ "Development Status :: 5 - Production/Stable",