Merge branch 'master' into nightly

This commit is contained in:
Régis Behmo 2023-12-09 17:42:22 +01:00
commit 241283ea95
6 changed files with 146 additions and 54 deletions

22
.github/workflows/test.yml vendored Normal file
View File

@ -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

34
Makefile Normal file
View File

@ -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}'

View File

@ -0,0 +1 @@
- [Improvement] Added Typing to code, Makefile and test action to the repository and formatted code with Black and isort. (by @CodeWithEmad)

View File

@ -34,6 +34,9 @@ setup(
include_package_data=True, include_package_data=True,
python_requires=">=3.8", python_requires=">=3.8",
install_requires=["tutor>=16.0.0,<17.0.0", "requests"], 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"]}, entry_points={"tutor.plugin.v1": ["xqueue = tutorxqueue.plugin"]},
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",

View File

@ -1,2 +1 @@
__version__ = "16.0.2" __version__ = "16.0.2"

View File

@ -2,17 +2,17 @@ from __future__ import annotations
import json import json
import os import os
import typing as t
from glob import glob from glob import glob
from typing import Any, Literal, Optional, Union
import click import click
import pkg_resources import pkg_resources
import requests import requests # type: ignore
from tutor import config as tutor_config from tutor import config as tutor_config
from tutor.__about__ import __version_suffix__ from tutor.__about__ import __version_suffix__
from tutor import exceptions from tutor import exceptions
from tutor import hooks as tutor_hooks from tutor import hooks as tutor_hooks
from tutor.__about__ import __version_suffix__
from .__about__ import __version__ from .__about__ import __version__
@ -20,12 +20,7 @@ from .__about__ import __version__
if __version_suffix__: if __version_suffix__:
__version__ += "-" + __version_suffix__ __version__ += "-" + __version_suffix__
config = { config: dict[str, dict[str, Any]] = {
"unique": {
"AUTH_PASSWORD": "{{ 8|random_string }}",
"MYSQL_PASSWORD": "{{ 8|random_string }}",
"SECRET_KEY": "{{ 24|random_string }}",
},
"defaults": { "defaults": {
"VERSION": __version__, "VERSION": __version__,
"AUTH_USERNAME": "lms", "AUTH_USERNAME": "lms",
@ -36,6 +31,11 @@ config = {
"REPOSITORY": "https://github.com/openedx/xqueue", "REPOSITORY": "https://github.com/openedx/xqueue",
"REPOSITORY_VERSION": "{{ OPENEDX_COMMON_VERSION }}", "REPOSITORY_VERSION": "{{ OPENEDX_COMMON_VERSION }}",
}, },
"unique": {
"AUTH_PASSWORD": "{{ 8|random_string }}",
"MYSQL_PASSWORD": "{{ 8|random_string }}",
"SECRET_KEY": "{{ 24|random_string }}",
},
} }
# Initialization hooks # Initialization hooks
@ -61,25 +61,31 @@ for service, template_path in MY_INIT_TASKS:
tutor_hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task)) tutor_hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))
# Image management # Image management
tutor_hooks.Filters.IMAGES_BUILD.add_item(( tutor_hooks.Filters.IMAGES_BUILD.add_item(
"xqueue", (
("plugins", "xqueue", "build", "xqueue"), "xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}", ("plugins", "xqueue", "build", "xqueue"),
(), "{{ XQUEUE_DOCKER_IMAGE }}",
)) (),
)
)
tutor_hooks.Filters.IMAGES_PULL.add_item(( tutor_hooks.Filters.IMAGES_PULL.add_item(
"xqueue", (
"{{ XQUEUE_DOCKER_IMAGE }}", "xqueue",
)) "{{ XQUEUE_DOCKER_IMAGE }}",
tutor_hooks.Filters.IMAGES_PUSH.add_item(( )
"xqueue", )
"{{ XQUEUE_DOCKER_IMAGE }}", tutor_hooks.Filters.IMAGES_PUSH.add_item(
)) (
"xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}",
)
)
@tutor_hooks.Filters.COMPOSE_MOUNTS.add() @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`, When mounting xqueue with `--mount=/path/to/xqueue`,
bind-mount the host repo in the xqueue container. bind-mount the host repo in the xqueue container.
@ -94,11 +100,11 @@ def _mount_xqueue(volumes, name):
@click.group(help="Interact with the Xqueue server", name="xqueue") @click.group(help="Interact with the Xqueue server", name="xqueue")
def command(): def command() -> None:
pass pass
@click.group(help="List and grade submissions") @click.group(help="list and grade submissions")
@click.pass_obj @click.pass_obj
@click.option("-q", "--queue", default="openedx", show_default=True, help="Queue name") @click.option("-q", "--queue", default="openedx", show_default=True, help="Queue name")
@click.option( @click.option(
@ -111,21 +117,21 @@ def command():
"from the TUTOR_XQUEUE_URL environment variable." "from the TUTOR_XQUEUE_URL environment variable."
), ),
) )
def submissions(context, queue, url): def submissions(context: click.Context, queue: str, url: str) -> None:
context.queue = queue context.queue = queue # type: ignore
context.url = url context.url = url # type: ignore
@click.command(name="count", help="Count submissions in queue") @click.command(name="count", help="Count submissions in queue")
@click.pass_obj @click.pass_obj
def count_submissions(context): def count_submissions(context: click.Context) -> None:
print_result(context, "count_submissions", context.queue) print_result(context, "count_submissions", context.queue) # type: ignore
@click.command(name="show", help="Show last submission") @click.command(name="show", help="Show last submission")
@click.pass_obj @click.pass_obj
def show_submission(context): def show_submission(context: click.Context) -> None:
print_result(context, "show_submission", context.queue) print_result(context, "show_submission", context.queue) # type: ignore
@click.command(name="grade", help="Grade a specific submission") @click.command(name="grade", help="Grade a specific submission")
@ -135,29 +141,43 @@ def show_submission(context):
@click.argument("correct", type=click.BOOL) @click.argument("correct", type=click.BOOL)
@click.argument("message") @click.argument("message")
@click.pass_obj @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( print_result(
context, context,
"grade_submission", "grade_submission",
submission_id, (
submission_key, submission_id,
grade, submission_key,
correct, grade,
message, correct,
message,
),
) )
def print_result(context, client_func_name, *args, **kwargs): def print_result(
user_config = tutor_config.load(context.root) context: click.Context,
client = Client(user_config, url=context.url) 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) func = getattr(client, client_func_name)
result = func(*args, **kwargs) result = func(*args, **kwargs)
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
class Client: class Client:
def __init__(self, user_config, url=None): def __init__(self, user_config: dict[str, Any], url: str = "") -> None:
self._session = None self._session: Optional[requests.Session] = None
self.username = user_config["XQUEUE_AUTH_USERNAME"] self.username = user_config["XQUEUE_AUTH_USERNAME"]
self.password = user_config["XQUEUE_AUTH_PASSWORD"] self.password = user_config["XQUEUE_AUTH_PASSWORD"]
@ -169,17 +189,17 @@ class Client:
self.login() self.login()
@property @property
def session(self): def session(self) -> requests.Session:
if self._session is None: if self._session is None:
self._session = requests.Session() self._session = requests.Session()
return self._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 # Don't forget to add a trailing slash to all endpoints: this is how xqueue
# works... # works...
return self.base_url + endpoint return self.base_url + endpoint
def login(self): def login(self) -> None:
response = self.request( response = self.request(
"/xqueue/login/", "/xqueue/login/",
method="POST", method="POST",
@ -193,7 +213,7 @@ class Client:
) )
) )
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}) response = self.request("/xqueue/get_submission/", params={"queue_name": queue})
if response["return_code"] != 0: if response["return_code"] != 0:
return response return response
@ -216,10 +236,17 @@ class Client:
"return_code": response["return_code"], "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}) 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( return self.request(
"/xqueue/put_result/", "/xqueue/put_result/",
method="POST", method="POST",
@ -233,7 +260,13 @@ class Client:
}, },
) )
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()) func = getattr(self.session, method.lower())
response = func(self.url(endpoint), data=data, params=params) response = func(self.url(endpoint), data=data, params=params)
# TODO handle errors >= 400 and non-parsable json responses # TODO handle errors >= 400 and non-parsable json responses
@ -245,7 +278,6 @@ submissions.add_command(show_submission)
submissions.add_command(grade_submission) submissions.add_command(grade_submission)
command.add_command(submissions) command.add_command(submissions)
####### Boilerplate code
# Add the "templates" folder as a template root # Add the "templates" folder as a template root
tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_item( tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_item(
pkg_resources.resource_filename("tutorxqueue", "templates") pkg_resources.resource_filename("tutorxqueue", "templates")
@ -288,9 +320,10 @@ tutor_hooks.Filters.CONFIG_OVERRIDES.add_items(
# Xqueue Public Host # Xqueue Public Host
######################################## ########################################
@tutor_hooks.Filters.APP_PUBLIC_HOSTS.add() @tutor_hooks.Filters.APP_PUBLIC_HOSTS.add()
def _xqueue_public_hosts( def _xqueue_public_hosts(
hosts: list[str], context_name: t.Literal["local", "dev"] hosts: list[str], context_name: Literal["local", "dev"]
) -> list[str]: ) -> list[str]:
if context_name == "dev": if context_name == "dev":
hosts += ["{{ XQUEUE_HOST }}:8000"] hosts += ["{{ XQUEUE_HOST }}:8000"]