Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing issues related with migration to Python 3.12 #14

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: [3.6, 3.9]
python-version: [3.9, 3.11, 3.12]

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Command line interface to the ReCodEx system.

## Requirements
- Python 3.6+
- Python 3.9+
- See `requirements.txt`

## Installation
Expand Down
3 changes: 3 additions & 0 deletions recodex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def get_exercises(self, offset=0, limit=0, orderBy=None, locale=None):
url += "&locale={}".format(urllib.parse.quote_plus(locale))
return self.get(url)["items"]

def get_reference_solution(self, solution_id):
return self.get("/reference-solutions/{}".format(solution_id))

def get_reference_solutions(self, exercise_id):
return self.get("/reference-solutions/exercise/{}".format(exercise_id))

Expand Down
6 changes: 5 additions & 1 deletion recodex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ def cli(ctx: click.Context):
"""
ReCodEx CLI
"""
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')

config_dir = Path(appdirs.user_config_dir("recodex"))
data_dir = Path(appdirs.user_data_dir("recodex"))

context_path = data_dir / "context.yaml"
user_context = UserContext.load(context_path) if context_path.exists() else UserContext()
user_context = UserContext.load(
context_path) if context_path.exists() else UserContext()
api_client = ApiClient(user_context.api_url, user_context.api_token)

if user_context.api_token is not None and user_context.is_token_almost_expired() and not user_context.is_token_expired:
Expand Down
12 changes: 8 additions & 4 deletions recodex/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from functools import lru_cache

import jwt
from ruamel import yaml
from ruamel.yaml import YAML
from typing import NamedTuple, Optional
from datetime import datetime, timezone
from pathlib import Path
Expand Down Expand Up @@ -31,7 +31,8 @@ def is_token_almost_expired(self, threshold=0.5) -> bool:
"""

validity_period = self.token_data["exp"] - self.token_data["iat"]
time_until_expiration = self.token_data["exp"] - datetime.now(timezone.utc).timestamp()
time_until_expiration = self.token_data["exp"] - \
datetime.now(timezone.utc).timestamp()
return validity_period * threshold > time_until_expiration

@property
Expand All @@ -43,9 +44,12 @@ def replace_token(self, new_token) -> 'UserContext':

@classmethod
def load(cls, config_path: Path):
config = yaml.safe_load(config_path.open("r"))
yaml = YAML(typ="safe")
config = yaml.load(config_path.open("r")) or {}
return cls(**config)

def store(self, config_path: Path):
config_path.parent.mkdir(parents=True, exist_ok=True)
yaml.dump(dict(self._asdict()), config_path.open("w"))
yaml = YAML(typ="safe")
with config_path.open("w") as fp:
yaml.dump(dict(self._asdict()), fp)
15 changes: 10 additions & 5 deletions recodex/plugins/assignments/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
import datetime
import json
from ruamel import yaml
from ruamel.yaml import YAML

from recodex.api import ApiClient
from recodex.decorators import pass_api_client
Expand Down Expand Up @@ -52,7 +52,8 @@ def download_best_solutions(api: ApiClient, download_dir, assignment_id):
if download_dir is None:
download_dir = "."
if not os.path.exists(download_dir) or not os.path.isdir(download_dir):
click.echo("Download path '{}' must exist and must be a directory.".format(download_dir))
click.echo(
"Download path '{}' must exist and must be a directory.".format(download_dir))
return

# Get assignment metadata and best solution for each student ...
Expand All @@ -74,9 +75,12 @@ def download_best_solutions(api: ApiClient, download_dir, assignment_id):
asciiize_string(student["name"]["lastName"]),
asciiize_string(student["name"]["firstName"]), student["id"])
points = safe_get_solution_points(best)
created = datetime.datetime.fromtimestamp(best["createdAt"]).strftime('%Y-%m-%d %H:%M:%S')
click.echo("Saving {} ... {} points, {}".format(file_name, points, created))
api.download_solution(best['id'], "{}/{}".format(download_dir, file_name))
created = datetime.datetime.fromtimestamp(
best["createdAt"]).strftime('%Y-%m-%d %H:%M:%S')
click.echo("Saving {} ... {} points, {}".format(
file_name, points, created))
api.download_solution(
best['id'], "{}/{}".format(download_dir, file_name))


@cli.command()
Expand All @@ -92,6 +96,7 @@ def get_solutions(api: ApiClient, assignment_id, useJson):
if useJson is True:
json.dump(solutions, sys.stdout, sort_keys=True, indent=4)
elif useJson is False:
yaml = YAML(typ="safe")
yaml.dump(solutions, sys.stdout)
else:
for solution in solutions:
Expand Down
54 changes: 36 additions & 18 deletions recodex/plugins/codex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
from pprint import pprint
from pathlib import Path
from ruamel import yaml
from ruamel.yaml import YAML

from recodex.decorators import pass_config_dir, pass_api_client
from recodex.api import ApiClient
Expand Down Expand Up @@ -36,17 +36,20 @@ def details(api: ApiClient, exercise_folder):
print()

config = Config.load(Path.cwd() / "import-config.yml")
tests = load_codex_test_config(Path(exercise_folder) / "testdata" / "config")
tests = load_codex_test_config(
Path(exercise_folder) / "testdata" / "config")
test_id_map = {test.name: test.number for test in tests}
files = []

print("### Exercise files")
for name, path in load_exercise_files(exercise_folder):
print(f"{path} as {name}")
files.append(name) # Make sure the file names are present in the exercise file list
# Make sure the file names are present in the exercise file list
files.append(name)

print("### Exercise configuration")
pprint(make_exercise_config(config, soup, files, api.get_pipelines(), tests, test_id_map))
pprint(make_exercise_config(config, soup, files,
api.get_pipelines(), tests, test_id_map))
print()


Expand All @@ -61,7 +64,8 @@ def name(exercise_folder):
@cli.command()
@click.argument("exercise_folder")
def has_dir_test(exercise_folder):
tests = load_codex_test_config(Path(exercise_folder) / "testdata" / "config")
tests = load_codex_test_config(
Path(exercise_folder) / "testdata" / "config")
for test in tests:
if test.in_type == "dir":
print(test.number, "in")
Expand Down Expand Up @@ -98,10 +102,13 @@ def get_id(api: ApiClient, exercise_folder):
@click.argument("exercise_folder")
@pass_api_client
def set_score_config(api: ApiClient, exercise_id, exercise_folder):
tests = load_codex_test_config(Path(exercise_folder) / "testdata" / "config")
tests = load_codex_test_config(
Path(exercise_folder) / "testdata" / "config")

score_config = {test.name: int(test.points) for test in tests}
api.set_exercise_score_config(exercise_id, yaml.dump({"testWeights": score_config}, default_flow_style=False))
yaml = YAML(typ="safe")
api.set_exercise_score_config(exercise_id, yaml.dump(
{"testWeights": score_config}, default_flow_style=False))


@cli.command(name="import")
Expand Down Expand Up @@ -144,7 +151,8 @@ def run_import(config_dir: Path, api: ApiClient, exercise_folder, group_id, exer

# Prepare the exercise text
attachments = api.get_exercise_attachments(exercise_id)
url_map = {item["name"]: "{}/v1/uploaded-files/{}/download".format(api.api_url, item["id"]) for item in attachments}
url_map = {item["name"]: "{}/v1/uploaded-files/{}/download".format(
api.api_url, item["id"]) for item in attachments}
text = replace_file_references(text, url_map)

# Set the details of the new exercise
Expand All @@ -169,7 +177,8 @@ def run_import(config_dir: Path, api: ApiClient, exercise_folder, group_id, exer
for name, path in load_exercise_files(exercise_folder):
exercise_file_data[name] = upload_file(api, path, name)

api.add_exercise_files(exercise_id, [data["id"] for data in exercise_file_data.values()])
api.add_exercise_files(exercise_id, [data["id"]
for data in exercise_file_data.values()])
logging.info("Uploaded exercise files associated with the exercise")

# Configure environments
Expand All @@ -187,10 +196,13 @@ def run_import(config_dir: Path, api: ApiClient, exercise_folder, group_id, exer
logging.info("Added environments %s", ", ".join(environments))

# Configure tests
tests = load_codex_test_config(Path(exercise_folder) / "testdata" / "config")
tests = load_codex_test_config(
Path(exercise_folder) / "testdata" / "config")

api.set_exercise_tests(exercise_id, [{"name": test.name} for test in tests])
test_id_map = {test["name"]: test["id"] for test in api.get_exercise_tests(exercise_id)}
api.set_exercise_tests(
exercise_id, [{"name": test.name} for test in tests])
test_id_map = {test["name"]: test["id"]
for test in api.get_exercise_tests(exercise_id)}
logging.info("Exercise tests configured")

# Upload custom judges
Expand All @@ -200,10 +212,13 @@ def run_import(config_dir: Path, api: ApiClient, exercise_folder, group_id, exer
if custom_judges:
logging.info("Uploading custom judges")
for judge in custom_judges:
judge_path = Path(exercise_folder).joinpath("testdata").joinpath(judge)
custom_judge_files[judge] = upload_file(api, judge_path, judge_path.name)
judge_path = Path(exercise_folder).joinpath(
"testdata").joinpath(judge)
custom_judge_files[judge] = upload_file(
api, judge_path, judge_path.name)

api.add_exercise_files(exercise_id, [data["id"] for data in custom_judge_files.values()])
api.add_exercise_files(
exercise_id, [data["id"] for data in custom_judge_files.values()])
logging.info("Uploaded judge files associated with the exercise")

exercise_config = make_exercise_config(
Expand Down Expand Up @@ -232,13 +247,16 @@ def run_import(config_dir: Path, api: ApiClient, exercise_folder, group_id, exer
"memory": test.limits[key].mem_limit
}

api.update_limits(exercise_id, environment_id, hwgroup_id, limits_config)
api.update_limits(exercise_id, environment_id,
hwgroup_id, limits_config)
logging.info("Limits set for environment %s", environment_id)

# Upload reference solutions
for solution_id, solution in load_reference_solution_details(content_soup, config.extension_to_runtime):
path = load_reference_solution_file(solution_id, content_soup, exercise_folder)
path = load_reference_solution_file(
solution_id, content_soup, exercise_folder)
solution["files"] = [upload_file(api, path)["id"]]
payload = api.create_reference_solution(exercise_id, solution)

logging.info("New reference solution created, with id %s", payload["referenceSolution"]["id"])
logging.info("New reference solution created, with id %s",
payload["referenceSolution"]["id"])
6 changes: 3 additions & 3 deletions recodex/plugins/codex/plugin_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ruamel import yaml
from ruamel.yaml import YAML

from pathlib import Path
from typing import NamedTuple, Dict
Expand Down Expand Up @@ -26,6 +26,6 @@ class Config(NamedTuple):
def load(cls, config_path: Path):
if not config_path.exists():
return cls()

config = yaml.safe_load(config_path.open("r"))
yaml = YAML(typ="safe")
config = yaml.load(config_path.open("r"))
return cls(**config)
Loading
Loading