From c871ee0cd7536aefffc4fa3053f956e128e1f189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kruli=C5=A1?= Date: Thu, 21 Dec 2023 01:28:58 +0100 Subject: [PATCH] Fixing issues related with migration to Python 3.12 (especially bc-incompatible changes in ruamel.yaml). --- .github/workflows/ci.yml | 4 +- README.md | 2 +- recodex/api.py | 3 ++ recodex/cli.py | 6 ++- recodex/config.py | 12 +++-- recodex/plugins/assignments/cli.py | 15 ++++-- recodex/plugins/codex/cli.py | 54 ++++++++++++------- recodex/plugins/codex/plugin_config.py | 6 +-- recodex/plugins/exercises/cli.py | 64 ++++++++++++++++++----- recodex/plugins/groups/cli.py | 9 +++- recodex/plugins/plagiarisms/cli.py | 5 +- recodex/plugins/shadow_assignments/cli.py | 12 +++-- recodex/plugins/solutions/cli.py | 8 ++- recodex/plugins/users/cli.py | 21 +++++--- requirements.txt | 3 +- setup.py | 7 +-- 16 files changed, 163 insertions(+), 68 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b30b86b..b9d7c7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 2b0637e..3faa415 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Command line interface to the ReCodEx system. ## Requirements -- Python 3.6+ +- Python 3.9+ - See `requirements.txt` ## Installation diff --git a/recodex/api.py b/recodex/api.py index df9ce55..b37dd6a 100644 --- a/recodex/api.py +++ b/recodex/api.py @@ -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)) diff --git a/recodex/cli.py b/recodex/cli.py index 8ab77e0..c357dd0 100644 --- a/recodex/cli.py +++ b/recodex/cli.py @@ -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: diff --git a/recodex/config.py b/recodex/config.py index 3b1f928..89a30ec 100644 --- a/recodex/config.py +++ b/recodex/config.py @@ -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 @@ -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 @@ -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) diff --git a/recodex/plugins/assignments/cli.py b/recodex/plugins/assignments/cli.py index efbf157..515b4d3 100644 --- a/recodex/plugins/assignments/cli.py +++ b/recodex/plugins/assignments/cli.py @@ -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 @@ -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 ... @@ -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() @@ -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: diff --git a/recodex/plugins/codex/cli.py b/recodex/plugins/codex/cli.py index 2a3a2cf..bf720d3 100644 --- a/recodex/plugins/codex/cli.py +++ b/recodex/plugins/codex/cli.py @@ -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 @@ -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() @@ -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") @@ -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") @@ -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 @@ -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 @@ -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 @@ -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( @@ -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"]) diff --git a/recodex/plugins/codex/plugin_config.py b/recodex/plugins/codex/plugin_config.py index 5092d51..87b8b25 100644 --- a/recodex/plugins/codex/plugin_config.py +++ b/recodex/plugins/codex/plugin_config.py @@ -1,4 +1,4 @@ -from ruamel import yaml +from ruamel.yaml import YAML from pathlib import Path from typing import NamedTuple, Dict @@ -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) diff --git a/recodex/plugins/exercises/cli.py b/recodex/plugins/exercises/cli.py index 694a02c..a580cfb 100644 --- a/recodex/plugins/exercises/cli.py +++ b/recodex/plugins/exercises/cli.py @@ -1,5 +1,5 @@ import json -from ruamel import yaml +from ruamel.yaml import YAML import logging import sys import io @@ -36,6 +36,7 @@ def list_all(api: ApiClient, useJson, offset, limit, order, locale): if useJson is True: json.dump(exercises, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(exercises, sys.stdout) else: for exercise in exercises: @@ -54,9 +55,31 @@ def get(api: ApiClient, exercise_id, useJson): if useJson: json.dump(exercise, sys.stdout, sort_keys=True, indent=4) else: + yaml = YAML(typ="safe") yaml.dump(exercise, sys.stdout) +@cli.command() +@click.argument("solution_id") +@click.option("--json/--yaml", "useJson", default=None) +@pass_api_client +def get_ref_solution(api: ApiClient, solution_id, useJson): + """ + Get one reference solution by its id. + """ + solution = api.get_reference_solution(solution_id) + if useJson is True: + json.dump(solution, sys.stdout, sort_keys=True, indent=4) + elif useJson is False: + yaml = YAML(typ="safe") + yaml.dump(solution, sys.stdout) + else: + ts = int(solution["createdAt"]) + date = datetime.utcfromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') + click.echo("{} {} {} {}".format(solution["id"], solution["runtimeEnvironmentId"], + date, solution["description"])) + + @cli.command() @click.argument("exercise_id") @click.option("--json/--yaml", "useJson", default=None) @@ -69,6 +92,7 @@ def get_ref_solutions(api: ApiClient, exercise_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: @@ -90,6 +114,7 @@ def get_ref_solution_evaluations(api: ApiClient, ref_solution_id, useJson): if useJson is True: json.dump(evaluations, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(evaluations, sys.stdout) else: for evaluation in evaluations: @@ -147,7 +172,8 @@ def add_localization(api: ApiClient, locale, exercise_id, include_name): for prop in copy_props: exercise[prop] = full_exercise[prop] - localizedText = next((lt for lt in exercise["localizedTexts"] if lt["locale"] == locale), None) + localizedText = next( + (lt for lt in exercise["localizedTexts"] if lt["locale"] == locale), None) if localizedText is None: localizedText = { "locale": locale, @@ -167,7 +193,8 @@ def add_localization(api: ApiClient, locale, exercise_id, include_name): def _add_reference_solution(api: ApiClient, exercise_id, note, runtime_environment, files): - uploaded_files = [api.upload_file(file, open(file, "r"))["id"] for file in files] + uploaded_files = [api.upload_file(file, open(file, "r"))[ + "id"] for file in files] preflight = api.presubmit_check(exercise_id, uploaded_files) if (preflight["environments"] is None or len(preflight["environments"]) == 0): @@ -178,7 +205,8 @@ def _add_reference_solution(api: ApiClient, exercise_id, note, runtime_environme runtime_environment = preflight["environments"][0] if runtime_environment not in preflight["environments"]: - print('Selected runtime {} is not allowed by the preflight check.'.format(runtime_environment), file=sys.stderr) + print('Selected runtime {} is not allowed by the preflight check.'.format( + runtime_environment), file=sys.stderr) return submit_data = { @@ -188,9 +216,11 @@ def _add_reference_solution(api: ApiClient, exercise_id, note, runtime_environme } variables = next((sv for sv in preflight['submitVariables'] if sv.get( "runtimeEnvironmentId") == runtime_environment), {}).get('variables', []) - entry_point = next((v for v in variables if v.get('name') == 'entry-point'), None) + entry_point = next( + (v for v in variables if v.get('name') == 'entry-point'), None) if entry_point is not None: - submit_data["solutionParams"] = {"variables": [{"name": "entry-point", "value": os.path.basename(files[0])}]} + submit_data["solutionParams"] = {"variables": [ + {"name": "entry-point", "value": os.path.basename(files[0])}]} return api.create_reference_solution(exercise_id, submit_data) @@ -205,7 +235,8 @@ def add_reference_solution(api: ApiClient, exercise_id, note, runtime_environmen if len(files) == 0: print('No files given.', file=sys.stderr) return - result = _add_reference_solution(api, exercise_id, note, runtime_environment, files) + result = _add_reference_solution( + api, exercise_id, note, runtime_environment, files) click.echo(result["referenceSolution"]["id"]) @@ -221,7 +252,8 @@ def evaluate_all_rs(api: ApiClient): try: api.evaluate_reference_solutions(exercise["id"]) except Exception as e: - logging.error("Error in exercise {}: {}".format(exercise["id"], str(e))) + logging.error("Error in exercise {}: {}".format( + exercise["id"], str(e))) @cli.command() @@ -235,7 +267,8 @@ def check_rs_evaluations(api: ApiClient, threshold): for exercise in api.get_exercises(): solutions = api.get_reference_solutions(exercise["id"]) if not solutions: - logging.error("Exercise %s has no reference solutions", exercise["id"]) + logging.error( + "Exercise %s has no reference solutions", exercise["id"]) continue found = False @@ -257,9 +290,11 @@ def check_rs_evaluations(api: ApiClient, threshold): if not found_recent: if found: - logging.error("Exercise %s has no recent successful evaluations", exercise["id"]) + logging.error( + "Exercise %s has no recent successful evaluations", exercise["id"]) else: - logging.error("Exercise %s has never had any successful evaluations", exercise["id"]) + logging.error( + "Exercise %s has never had any successful evaluations", exercise["id"]) @cli.command() @@ -281,6 +316,7 @@ def get_config(api: ApiClient, exercise_id, useJson): if useJson: json.dump(config, sys.stdout, sort_keys=True, indent=4) else: + yaml = YAML(typ="safe") yaml.dump(config, sys.stdout) @@ -297,7 +333,8 @@ def set_config(api: ApiClient, exercise_id, file_name, useJson): if useJson: config = json.load(stream) else: - config = yaml.safe_load(stream) + yaml = YAML(typ="safe") + config = yaml.load(stream) api.update_exercise_config(exercise_id, config) @@ -447,7 +484,8 @@ def fork(api: ApiClient, exercise_id, group_id, complete): api.download_file(file["id"], path) if entry_point is not None: - file_names.insert(0, entry_point) # make sure entry point is the first file on the list + # make sure entry point is the first file on the list + file_names.insert(0, entry_point) if len(file_names) > 0: ref_res = _add_reference_solution(api, new_id, solution["description"], diff --git a/recodex/plugins/groups/cli.py b/recodex/plugins/groups/cli.py index 644fef9..fb9e36b 100644 --- a/recodex/plugins/groups/cli.py +++ b/recodex/plugins/groups/cli.py @@ -1,5 +1,5 @@ import json -from ruamel import yaml +from ruamel.yaml import YAML import sys import click @@ -29,6 +29,7 @@ def all(api: ApiClient, useJson, archived): if useJson is True: json.dump(groups, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(groups, sys.stdout) @@ -45,6 +46,7 @@ def detail(api: ApiClient, group_id, useJson): if useJson is True: json.dump(group, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(group, sys.stdout) @@ -109,6 +111,7 @@ def students(api: ApiClient, group_id, useJson): if useJson is True: json.dump(students, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(students, sys.stdout) else: for student in students: @@ -128,7 +131,9 @@ def assignments(api: ApiClient, group_id, useJson): if useJson is True: json.dump(assignments, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(assignments, sys.stdout) else: for assignment in assignments: - click.echo("{} {}".format(assignment["id"], get_localized_name(assignment["localizedTexts"]))) + click.echo("{} {}".format( + assignment["id"], get_localized_name(assignment["localizedTexts"]))) diff --git a/recodex/plugins/plagiarisms/cli.py b/recodex/plugins/plagiarisms/cli.py index 5f474bc..c9805a1 100644 --- a/recodex/plugins/plagiarisms/cli.py +++ b/recodex/plugins/plagiarisms/cli.py @@ -1,7 +1,7 @@ import sys import click import json -from ruamel import yaml +from ruamel.yaml import YAML from recodex.api import ApiClient from recodex.decorators import pass_api_client @@ -50,6 +50,7 @@ def add_similarity(api: ApiClient, id, solution_id, useJson): if useJson: data = json.load(sys.stdin) else: - data = yaml.safe_load(sys.stdin) + yaml = YAML(typ="safe") + data = yaml.load(sys.stdin) print(data) api.add_plagiarism_detected_similarity(id, solution_id, data) diff --git a/recodex/plugins/shadow_assignments/cli.py b/recodex/plugins/shadow_assignments/cli.py index db72d89..979a122 100644 --- a/recodex/plugins/shadow_assignments/cli.py +++ b/recodex/plugins/shadow_assignments/cli.py @@ -1,5 +1,5 @@ import json -from ruamel import yaml +from ruamel.yaml import YAML import sys import time @@ -29,13 +29,16 @@ def get(api: ApiClient, assignment_id, useJson): if useJson is True: json.dump(assignment, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(assignment, sys.stdout) else: for localizedText in assignment["localizedTexts"]: - click.echo("{} {}".format(localizedText["locale"], localizedText["name"])) + click.echo("{} {}".format( + localizedText["locale"], localizedText["name"])) click.echo() for points in assignment["points"]: - click.echo("{} {} {}".format(points["awardeeId"], points["points"], points["note"])) + click.echo("{} {} {}".format( + points["awardeeId"], points["points"], points["note"])) @cli.command() @@ -50,7 +53,8 @@ def create_points(api: ApiClient, assignment_id, user_id, points, note): """ awarded_at = int(time.time()) - api.create_shadow_assignment_points(assignment_id, user_id, points, note, awarded_at) + api.create_shadow_assignment_points( + assignment_id, user_id, points, note, awarded_at) @cli.command() diff --git a/recodex/plugins/solutions/cli.py b/recodex/plugins/solutions/cli.py index b8552ed..55ea6e0 100644 --- a/recodex/plugins/solutions/cli.py +++ b/recodex/plugins/solutions/cli.py @@ -1,7 +1,7 @@ import click import sys import json -from ruamel import yaml +from ruamel.yaml import YAML import datetime import pprint @@ -29,6 +29,7 @@ def detail(api: ApiClient, solution_id, useJson): if useJson is True: json.dump(solution, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(solution, sys.stdout) else: pp = pprint.PrettyPrinter(indent=4) @@ -60,6 +61,7 @@ def get_files(api: ApiClient, solution_id, useJson): if useJson is True: json.dump(files, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(files, sys.stdout) else: pp = pprint.PrettyPrinter(indent=4) @@ -80,10 +82,12 @@ def get_comments(api: ApiClient, solution_id, useJson): if useJson is True: json.dump(comments["comments"], sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(comments["comments"], sys.stdout) else: for comment in comments["comments"]: - posted = datetime.datetime.fromtimestamp(comment["postedAt"]).strftime('%Y-%m-%d %H:%M:%S') + posted = datetime.datetime.fromtimestamp( + comment["postedAt"]).strftime('%Y-%m-%d %H:%M:%S') click.echo("\n>>> {} at {} ({}) [{}]:".format(comment["user"]["name"], posted, "private" if comment["isPrivate"] else "public", comment["id"])) click.echo(comment["text"]) diff --git a/recodex/plugins/users/cli.py b/recodex/plugins/users/cli.py index f02c05f..9170a4f 100644 --- a/recodex/plugins/users/cli.py +++ b/recodex/plugins/users/cli.py @@ -2,7 +2,7 @@ import csv import sys import json -from ruamel import yaml +from ruamel.yaml import YAML from recodex.api import ApiClient from recodex.config import UserContext @@ -39,13 +39,14 @@ def get(api: ApiClient, user_id, useJson): if useJson: json.dump(user, sys.stdout, sort_keys=True, indent=4) else: + yaml = YAML(typ="safe") yaml.dump(user, sys.stdout) @cli.command() @click.option("--json/--yaml", "useJson", default=None, help='Default is CSV.') @click.option('--only-active', 'onlyActive', is_flag=True, help='Return full records formated into CSV.') -@click.option("search", "-s", default=None, help="Roles split by comma") +@click.option("search", "-s", default=None, help="Substring to search") @click.option("roles", "-r", default=None, help="Roles split by comma") @pass_user_context @pass_api_client @@ -57,7 +58,8 @@ def search(api: ApiClient, context: UserContext, search, roles, useJson, onlyAct roles = roles.split(',') users = [] - instances_ids = api.get_user(context.user_id)["privateData"]["instancesIds"] + instances_ids = api.get_user(context.user_id)[ + "privateData"]["instancesIds"] for instance_id in instances_ids: for user in api.search_users(instance_id, search, roles): if not onlyActive or user.get("privateData", {}).get("isAllowed", False): @@ -66,10 +68,12 @@ def search(api: ApiClient, context: UserContext, search, roles, useJson, onlyAct if useJson is True: json.dump(users, sys.stdout, sort_keys=True, indent=4) elif useJson is False: + yaml = YAML(typ="safe") yaml.dump(users, sys.stdout) else: # print CSV header - fieldnames = ['id', 'title_before', 'first_name', 'last_name', 'title_after', 'avatar_url'] + fieldnames = ['id', 'title_before', 'first_name', + 'last_name', 'title_after', 'avatar_url'] csv_writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames) csv_writer.writeheader() @@ -92,13 +96,16 @@ def register(api: ApiClient, context: UserContext, email, first_name, last_name, Register new user with local account """ if instance_id is None: - instances_ids = api.get_user(context.user_id)["privateData"]["instancesIds"] + instances_ids = api.get_user(context.user_id)[ + "privateData"]["instancesIds"] if len(instances_ids) != 1: - click.echo("Instance ID is ambiguous. Provide explicit ID via --instance_id option.") + click.echo( + "Instance ID is ambiguous. Provide explicit ID via --instance_id option.") return instance_id = instances_ids[0] - res = api.register_user(instance_id, email, first_name, last_name, password) + res = api.register_user( + instance_id, email, first_name, last_name, password) user_id = res['user']['id'] click.echo("User {id} ({first_name} {last_name}, {email}) registered in instance {instance_id}".format( id=user_id, first_name=first_name, last_name=last_name, email=email, instance_id=instance_id)) diff --git a/requirements.txt b/requirements.txt index f27a098..a90339c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ +setuptools click>=6.0,<=7.0 requests -ruamel.yaml +ruamel.yaml>=0.17 appdirs pyjwt diff --git a/setup.py b/setup.py index d01cec0..bcc8604 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ 'Operating System :: Unix', 'Programming Language :: Python :: Implementation :: CPython', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6' + 'Programming Language :: Python :: 3.9' ], keywords='recodex', url='https://github.com/ReCodEx/cli', @@ -21,8 +21,9 @@ license='MIT', packages=find_packages("."), include_package_data=True, - python_requires='>=3.6', - install_requires=['click', 'requests', 'appdirs', 'ruamel.yaml', 'bs4', 'lxml', 'html2text', 'pyjwt'], + python_requires='>=3.9', + install_requires=['setuptools', 'click', 'requests', 'appdirs', + 'ruamel.yaml', 'bs4', 'lxml', 'html2text', 'pyjwt'], entry_points={ 'console_scripts': ['recodex = recodex.cli:cli'],