Skip to content

Commit

Permalink
Fixing issues related with migration to Python 3.12 (especially bc-in…
Browse files Browse the repository at this point in the history
…compatible changes in ruamel.yaml).
  • Loading branch information
krulis-martin committed Dec 21, 2023
1 parent c30e63a commit c871ee0
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 68 deletions.
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

0 comments on commit c871ee0

Please sign in to comment.