Merge branch 'master' into nightly
This commit is contained in:
commit
241283ea95
22
.github/workflows/test.yml
vendored
Normal file
22
.github/workflows/test.yml
vendored
Normal 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
34
Makefile
Normal 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}'
|
||||||
1
changelog.d/20231118_161232_codewithemad.md
Normal file
1
changelog.d/20231118_161232_codewithemad.md
Normal 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)
|
||||||
3
setup.py
3
setup.py
@ -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",
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
__version__ = "16.0.2"
|
__version__ = "16.0.2"
|
||||||
|
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user