From 0a232a85e8bbe73f5044ad03c5e17767b63f5a77 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 16 Aug 2022 16:59:01 -0400 Subject: [PATCH 01/27] Add flag to download groups in OH-queue-friendly format --- agiocli/__main__.py | 20 +++++++++++++++++--- agiocli/utils.py | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/agiocli/__main__.py b/agiocli/__main__.py index af03e95..c67d401 100644 --- a/agiocli/__main__.py +++ b/agiocli/__main__.py @@ -145,12 +145,14 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30 help="Project pk, name, or shorthand.") @click.option("-l", "--list", "show_list", is_flag=True, help="List groups and exit.") +@click.option("-q", "--queue", "show_queue", is_flag=True, + help="List groups in OH Queue format and exit.") @click.option("-w", "--web", is_flag=True, help="Open group in browser.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. # We need to tell pycodestyle to ignore it. # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def groups(ctx, group_arg, project_arg, course_arg, show_list, web): # noqa: D301 +def groups(ctx, group_arg, project_arg, course_arg, show_list, show_queue, web): # noqa: D301 """Show group detail or list groups. GROUP_ARG is a primary key, name, or member uniqname. @@ -158,6 +160,7 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, web): # noqa: D3 \b EXAMPLES: agio groups --list + agio groups --queue agio groups agio groups 246965 agio groups awdeorio @@ -173,14 +176,25 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, web): # noqa: D3 except TokenFileNotFound as err: sys.exit(err) - # Handle --list: list groups and exit - if show_list: + # Common logic for both --list and --queue + if show_list or show_queue: project = utils.get_project_smart(project_arg, course_arg, client) group_list = utils.get_group_list(project, client) + course_pk = project['course'] + + # Handle --list: list groups and exit + if show_list: for i in group_list: print(utils.group_str(i)) return + # Handle --queue: list groups in OH Queue format and exit + if show_queue: + students_list = utils.get_students(course_pk, client) + filtered_groups = utils.filter_students_only(group_list, students_list) + print(filtered_groups) + return + # Select a group and print or open it group = utils.get_group_smart(group_arg, project_arg, course_arg, client) if web: diff --git a/agiocli/utils.py b/agiocli/utils.py index 225da5f..47ee1b7 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -358,6 +358,12 @@ def get_course_project_list(course, client): projects = sorted(projects, key=lambda x: x["name"]) return projects +def get_students(course_pk, client): + """Return a sorted list of projects for course.""" + students = client.get(f"/api/courses/{course_pk}/students/") + students = sorted(students, key=lambda x: x["username"]) + return students + def get_project_smart(project_arg, course_arg, client): """Interact with the user to select a project. @@ -414,6 +420,20 @@ def group_str(group): uniqnames_str = ", ".join(uniqnames) return f"[{group['pk']}] {uniqnames_str}" +def filter_students_only(groups, students): + students = set(student["username"] for student in students) + groups = [group["member_names"] for group in groups] + filtered_groups = filter( + lambda group: + all( + member in students + for member in group + ) + , + groups + ) + return json.dumps(list(filtered_groups)) + def group_uniqnames(group): """Return group member uniqnames.""" From 390d6958e0c929247a138ddc235ce4cb3d77f036 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 16 Aug 2022 17:03:39 -0400 Subject: [PATCH 02/27] autopep8 --- agiocli/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 47ee1b7..1f94c4d 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -358,6 +358,7 @@ def get_course_project_list(course, client): projects = sorted(projects, key=lambda x: x["name"]) return projects + def get_students(course_pk, client): """Return a sorted list of projects for course.""" students = client.get(f"/api/courses/{course_pk}/students/") @@ -420,16 +421,16 @@ def group_str(group): uniqnames_str = ", ".join(uniqnames) return f"[{group['pk']}] {uniqnames_str}" + def filter_students_only(groups, students): students = set(student["username"] for student in students) groups = [group["member_names"] for group in groups] filtered_groups = filter( - lambda group: + lambda group: all( member in students for member in group - ) - , + ), groups ) return json.dumps(list(filtered_groups)) From fd347427c9a528da4cfac0515c34046e09f495dc Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 16 Aug 2022 17:05:36 -0400 Subject: [PATCH 03/27] Add docstring --- agiocli/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agiocli/utils.py b/agiocli/utils.py index 1f94c4d..d69bfd3 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -423,6 +423,7 @@ def group_str(group): def filter_students_only(groups, students): + """Extract only the students from project groups.""" students = set(student["username"] for student in students) groups = [group["member_names"] for group in groups] filtered_groups = filter( From bd8429786755a7ffd34412bc8950fafbe9e35690 Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Wed, 17 Aug 2022 15:32:13 -0400 Subject: [PATCH 04/27] Refactor :) --- agiocli/__main__.py | 15 ++++++--------- agiocli/utils.py | 20 +++++--------------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/agiocli/__main__.py b/agiocli/__main__.py index c67d401..f04085b 100644 --- a/agiocli/__main__.py +++ b/agiocli/__main__.py @@ -176,23 +176,20 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, show_queue, web): except TokenFileNotFound as err: sys.exit(err) - # Common logic for both --list and --queue - if show_list or show_queue: - project = utils.get_project_smart(project_arg, course_arg, client) - group_list = utils.get_group_list(project, client) - course_pk = project['course'] - # Handle --list: list groups and exit if show_list: + project = utils.get_project_smart(project_arg, course_arg, client) + group_list = utils.get_group_list(project, client) for i in group_list: print(utils.group_str(i)) return # Handle --queue: list groups in OH Queue format and exit if show_queue: - students_list = utils.get_students(course_pk, client) - filtered_groups = utils.filter_students_only(group_list, students_list) - print(filtered_groups) + project = utils.get_project_smart(project_arg, course_arg, client) + group_list = utils.get_group_list(project, client) + output = [utils.group_emails(group) for group in group_list] + print(utils.dict_str(output)) return # Select a group and print or open it diff --git a/agiocli/utils.py b/agiocli/utils.py index d69bfd3..c0390ae 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -422,25 +422,15 @@ def group_str(group): return f"[{group['pk']}] {uniqnames_str}" -def filter_students_only(groups, students): - """Extract only the students from project groups.""" - students = set(student["username"] for student in students) - groups = [group["member_names"] for group in groups] - filtered_groups = filter( - lambda group: - all( - member in students - for member in group - ), - groups - ) - return json.dumps(list(filtered_groups)) +def group_emails(group): + """Return group member email addresses.""" + members = group["members"] + return [x["username"] for x in members] def group_uniqnames(group): """Return group member uniqnames.""" - members = group["members"] - return [x["username"].replace("@umich.edu", "") for x in members] + return [x.replace("@umich.edu", "") for x in group_emails(group)] def is_group_member(uniqname, group): From 5b241b68e9bba91d38012f3aa789d79a71012e8f Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Wed, 17 Aug 2022 15:33:11 -0400 Subject: [PATCH 05/27] kill dead code --- agiocli/utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index c0390ae..ef99d2f 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -359,13 +359,6 @@ def get_course_project_list(course, client): return projects -def get_students(course_pk, client): - """Return a sorted list of projects for course.""" - students = client.get(f"/api/courses/{course_pk}/students/") - students = sorted(students, key=lambda x: x["username"]) - return students - - def get_project_smart(project_arg, course_arg, client): """Interact with the user to select a project. From ae77fef2a2b08182781b93f9b16ae47f64719983 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Mon, 12 Sep 2022 17:41:19 -0400 Subject: [PATCH 06/27] Filter out unparsable projects when smart matching --- agiocli/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 225da5f..62ed773 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -303,6 +303,7 @@ def parse_project_string(user_input): asstype_abbrev = match.group("asstype").lower() if asstype_abbrev not in assignment_types: asstypes = ", ".join(assignment_types.keys()) + return False sys.exit( f"Error: unsupported assignment type: '{asstype_abbrev}'. " f"Recognized shortcuts: {asstypes}" @@ -326,6 +327,11 @@ def project_match(search, projects): assert projects asstype, num, subtitle = parse_project_string(search) + # Filter for only parsable projects + projects = filter( + lambda x: parse_project_string(x["name"]), projects + ) + # Remove projects with an assignment type mismatch (Lab vs. Project, etc.) if asstype: projects = filter( @@ -373,6 +379,7 @@ def get_project_smart(project_arg, course_arg, client): if project_arg and project_arg.isnumeric(): return client.get(f"/api/projects/{project_arg}/") + # Get a course and a sorted list of projects course = get_course_smart(course_arg, client) projects = get_course_project_list(course, client) @@ -390,7 +397,6 @@ def get_project_smart(project_arg, course_arg, client): ) assert selected_projects return selected_projects[0] - # User provides strings, try to match a project matches = project_match(project_arg, projects) if not matches: @@ -565,7 +571,6 @@ def get_submission_smart( # Get a group group = get_group_smart(group_arg, project_arg, course_arg, client) - # User provides "best" if submission_arg == "best": return client.get(f"/api/groups/{group['pk']}/ultimate_submission/") @@ -586,7 +591,6 @@ def get_submission_smart( ) assert selected_submissions return selected_submissions[0] - # User provides string "last" if submission_arg == "last": return submissions[0] From c5e35ff117dcb438db34cb3a5e5a8e5c4d7d4a85 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:46:15 -0400 Subject: [PATCH 07/27] New exception and wrapper --- agiocli/utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 62ed773..e0f5589 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -258,6 +258,10 @@ def get_course_smart(course_arg, client): return matches[0] +class UnsupportedAssignmentError(Exception): + pass + + def parse_project_string(user_input): """Return assignment type, number, and subtitle from a user input string. @@ -303,8 +307,7 @@ def parse_project_string(user_input): asstype_abbrev = match.group("asstype").lower() if asstype_abbrev not in assignment_types: asstypes = ", ".join(assignment_types.keys()) - return False - sys.exit( + raise UnsupportedAssignmentError( f"Error: unsupported assignment type: '{asstype_abbrev}'. " f"Recognized shortcuts: {asstypes}" ) @@ -316,6 +319,11 @@ def parse_project_string(user_input): return asstype, num, subtitle +def parse_project_string_filter(user_input): + try: + return parse_project_string(user_input) + except: + return None def project_str(project): """Format project as a string.""" @@ -329,7 +337,7 @@ def project_match(search, projects): # Filter for only parsable projects projects = filter( - lambda x: parse_project_string(x["name"]), projects + lambda x: parse_project_string_filter(x["name"]), projects ) # Remove projects with an assignment type mismatch (Lab vs. Project, etc.) From cc6ef01715d0b880cc3a7b48e9cf7af069b79e48 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:46:21 -0400 Subject: [PATCH 08/27] Docstrings --- agiocli/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agiocli/utils.py b/agiocli/utils.py index e0f5589..6ab0d95 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -259,6 +259,7 @@ def get_course_smart(course_arg, client): class UnsupportedAssignmentError(Exception): + """Raised if the assignment string cannot be parsed.""" pass @@ -320,6 +321,7 @@ def parse_project_string(user_input): return asstype, num, subtitle def parse_project_string_filter(user_input): + """Wrapper for parse_project_string that skips errors.""" try: return parse_project_string(user_input) except: From e08414d3c23864057eb320362b084a87aa7afcd0 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:47:46 -0400 Subject: [PATCH 09/27] Lint --- agiocli/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 6ab0d95..fef3917 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -320,13 +320,15 @@ def parse_project_string(user_input): return asstype, num, subtitle + def parse_project_string_filter(user_input): """Wrapper for parse_project_string that skips errors.""" try: return parse_project_string(user_input) - except: + except UnsupportedAssignmentError: return None + def project_str(project): """Format project as a string.""" return f"[{project['pk']}] {project['name']}" @@ -389,7 +391,6 @@ def get_project_smart(project_arg, course_arg, client): if project_arg and project_arg.isnumeric(): return client.get(f"/api/projects/{project_arg}/") - # Get a course and a sorted list of projects course = get_course_smart(course_arg, client) projects = get_course_project_list(course, client) From 65dc986fc067cc0d6745e64d4106ddf6b757d915 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:49:24 -0400 Subject: [PATCH 10/27] Lint --- agiocli/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index fef3917..c613ab1 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -259,6 +259,7 @@ def get_course_smart(course_arg, client): class UnsupportedAssignmentError(Exception): + """Raised if the assignment string cannot be parsed.""" pass @@ -322,7 +323,7 @@ def parse_project_string(user_input): def parse_project_string_filter(user_input): - """Wrapper for parse_project_string that skips errors.""" + """Wraps for parse_project_string that skips errors.""" try: return parse_project_string(user_input) except UnsupportedAssignmentError: From 8224d64a7d77c40ea85ead60c31c0287ebf813c5 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:51:10 -0400 Subject: [PATCH 11/27] Lint :( --- agiocli/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index c613ab1..fadb5b8 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -259,8 +259,8 @@ def get_course_smart(course_arg, client): class UnsupportedAssignmentError(Exception): - """Raised if the assignment string cannot be parsed.""" + pass @@ -323,7 +323,7 @@ def parse_project_string(user_input): def parse_project_string_filter(user_input): - """Wraps for parse_project_string that skips errors.""" + """Wrap parse_project_string to skip errors.""" try: return parse_project_string(user_input) except UnsupportedAssignmentError: From c0bd51f12dbc072494d967b4a9c57e7c2cf68d88 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 14 Sep 2022 18:52:57 -0400 Subject: [PATCH 12/27] Use all the remaining CI minutes --- agiocli/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index fadb5b8..d26f1c5 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -261,8 +261,6 @@ def get_course_smart(course_arg, client): class UnsupportedAssignmentError(Exception): """Raised if the assignment string cannot be parsed.""" - pass - def parse_project_string(user_input): """Return assignment type, number, and subtitle from a user input string. From cf72df38158ac0d24d1b6f12b76e6d657d1cd553 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 20 Sep 2022 12:30:31 -0400 Subject: [PATCH 13/27] Replace options_map_func with pick.Option --- agiocli/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 225da5f..15f10f0 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -226,6 +226,7 @@ def get_course_smart(course_arg, client): # Get a list of courses sorted by year, semester and name courses = get_current_course_list(client) + courses_options = [pick.Option(course_str(x), x) for x in courses] # No course input from the user. Show all courses and prompt the user. if not course_arg: @@ -233,9 +234,8 @@ def get_course_smart(course_arg, client): sys.exit("Error: No current courses, try 'agio courses -l'") else: selected_courses = pick.pick( - options=courses, + options=courses_options, title=("Select a course:"), - options_map_func=course_str, multiselect=False, ) assert selected_courses @@ -378,14 +378,14 @@ def get_project_smart(project_arg, course_arg, client): projects = get_course_project_list(course, client) if not projects: sys.exit("Error: No projects for course, try 'agio courses -l'") + projects_options = [pick.Option(project_str(x), x) for x in projects] # No project input from the user. Show all projects for current course and # and prompt the user. if not project_arg: selected_projects = pick.pick( - options=projects, + options=projects_options, title="Select a project:", - options_map_func=project_str, multiselect=False, ) assert selected_projects @@ -574,14 +574,14 @@ def get_submission_smart( submissions = get_submission_list(group, client) if not submissions: sys.exit("Error: No submissions, try 'agio submissions -l'") + submissions_options = [pick.Option(submission_str(x), x) for x in submissions] # No submissions input from the user. Show all submissions for this group # and prompt the user. if not submission_arg: selected_submissions = pick.pick( - options=submissions, + options=submissions_options, title="Select a submission:", - options_map_func=submission_str, multiselect=False, ) assert selected_submissions From 26eb1c149da8af3ec3922dcb800b20f61b3aae6c Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 20 Sep 2022 12:35:24 -0400 Subject: [PATCH 14/27] Lint --- agiocli/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 15f10f0..d506cf4 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -574,7 +574,8 @@ def get_submission_smart( submissions = get_submission_list(group, client) if not submissions: sys.exit("Error: No submissions, try 'agio submissions -l'") - submissions_options = [pick.Option(submission_str(x), x) for x in submissions] + submissions_options = [pick.Option(submission_str(x), x) + for x in submissions] # No submissions input from the user. Show all submissions for this group # and prompt the user. From d2c3217bfe5b0aa85c10c690cae2cc99e13da93b Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Tue, 20 Sep 2022 13:14:37 -0400 Subject: [PATCH 15/27] Require pick >= 2.0.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4355527..d23bcf8 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ ], install_requires=[ "click", - "pick", + "pick>=2.0.0", "python-dateutil", "requests", ], From decd59cd0c1828bf308554cafeacee6567b8ecfd Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Tue, 20 Sep 2022 13:19:20 -0400 Subject: [PATCH 16/27] refactor --- agiocli/utils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index d506cf4..a38134f 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -226,15 +226,15 @@ def get_course_smart(course_arg, client): # Get a list of courses sorted by year, semester and name courses = get_current_course_list(client) - courses_options = [pick.Option(course_str(x), x) for x in courses] # No course input from the user. Show all courses and prompt the user. if not course_arg: if not courses: sys.exit("Error: No current courses, try 'agio courses -l'") else: + options = [pick.Option(course_str(x), x) for x in courses] selected_courses = pick.pick( - options=courses_options, + options=options, title=("Select a course:"), multiselect=False, ) @@ -378,13 +378,13 @@ def get_project_smart(project_arg, course_arg, client): projects = get_course_project_list(course, client) if not projects: sys.exit("Error: No projects for course, try 'agio courses -l'") - projects_options = [pick.Option(project_str(x), x) for x in projects] # No project input from the user. Show all projects for current course and # and prompt the user. if not project_arg: + options = [pick.Option(project_str(x), x) for x in projects] selected_projects = pick.pick( - options=projects_options, + options=options, title="Select a project:", multiselect=False, ) @@ -574,14 +574,13 @@ def get_submission_smart( submissions = get_submission_list(group, client) if not submissions: sys.exit("Error: No submissions, try 'agio submissions -l'") - submissions_options = [pick.Option(submission_str(x), x) - for x in submissions] # No submissions input from the user. Show all submissions for this group # and prompt the user. if not submission_arg: + options = [pick.Option(submission_str(x), x) for x in submissions] selected_submissions = pick.pick( - options=submissions_options, + options=options, title="Select a submission:", multiselect=False, ) From 2ec4dfbf65eaf9d405f932445c684165f14e183c Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 20 Sep 2022 14:14:00 -0400 Subject: [PATCH 17/27] Require Python>=3.7 --- .github/workflows/continuous_integration.yml | 2 +- setup.py | 2 +- tox.ini | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 36d5649..476b233 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -21,7 +21,7 @@ jobs: strategy: # Define OS and Python versions to use. 3.x is the latest minor version. matrix: - python-version: ["3.6", "3.x"] # 3.x is the latest minor version + python-version: ["3.7", "3.x"] # 3.x is the latest minor version os: [ubuntu-latest] # Sequence of tasks for this job diff --git a/setup.py b/setup.py index d23bcf8..623465b 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ "requests-mock", ], }, - python_requires='>=3.6', + python_requires='>=3.7', entry_points={ "console_scripts": [ "agio = agiocli.__main__:main", diff --git a/tox.ini b/tox.ini index e6d873e..43ea57c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ # Local host configuration with one Python 3 version [tox] -envlist = py36, py37, py38, py39, py310 +envlist = py37, py38, py39, py310 # GitHub Actions configuration with multiple Python versions # https://github.com/ymyzk/tox-gh-actions#tox-gh-actions-configuration [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 From b4c38fe94729086a74598fa2e8d1e4c3b7bfaed0 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 20 Sep 2022 16:33:21 -0400 Subject: [PATCH 18/27] Add queue test --- tests/test_groups.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_groups.py b/tests/test_groups.py index 81dfcea..8681e1f 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -36,6 +36,30 @@ def test_groups_list(api_mock): assert "[246965] awdeorio" in result.output +def test_groups_queue(api_mock): + """Verify agio groups queue option when project is specified. + + $ agio groups --queue --project 1005 + + api_mock is a shared test fixture that mocks responses to REST API + requests. It is implemented in conftest.py. + + """ + runner = click.testing.CliRunner() + result = runner.invoke( + main, [ + "groups", + "--queue", + "--project", "1005", + ], + catch_exceptions=False, + ) + assert result.exit_code == 0, result.output + result_list = json.loads(result.output) + assert ["achitta@umich.edu"] in result.output + assert ["awdeorio@umich.edu"] in result.output + + def test_groups_pk(api_mock): """Verify groups subcommand with primary key input. From baa5ebfb9e54346b9df0c0925e561b2e533bdfd4 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Tue, 20 Sep 2022 16:34:11 -0400 Subject: [PATCH 19/27] Fix test --- tests/test_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_groups.py b/tests/test_groups.py index 8681e1f..31f294b 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -56,8 +56,8 @@ def test_groups_queue(api_mock): ) assert result.exit_code == 0, result.output result_list = json.loads(result.output) - assert ["achitta@umich.edu"] in result.output - assert ["awdeorio@umich.edu"] in result.output + assert ["achitta@umich.edu"] in result_list + assert ["awdeorio@umich.edu"] in result_list def test_groups_pk(api_mock): From 9c83882420c964d8e67a66cb5a844f217d976656 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 21 Sep 2022 12:21:34 -0400 Subject: [PATCH 20/27] Access value inside pick.Option --- agiocli/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index a38134f..a137b72 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -239,7 +239,7 @@ def get_course_smart(course_arg, client): multiselect=False, ) assert selected_courses - return selected_courses[0] + return selected_courses[0].value # Try to match a course matches = course_match(course_arg, courses) @@ -389,7 +389,7 @@ def get_project_smart(project_arg, course_arg, client): multiselect=False, ) assert selected_projects - return selected_projects[0] + return selected_projects[0].value # User provides strings, try to match a project matches = project_match(project_arg, projects) @@ -585,7 +585,7 @@ def get_submission_smart( multiselect=False, ) assert selected_submissions - return selected_submissions[0] + return selected_submissions[0].value # User provides string "last" if submission_arg == "last": From 82ab746cf9d970ef00dffa7830a5ccea2c146952 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 21 Sep 2022 12:32:12 -0400 Subject: [PATCH 21/27] Fix mocking in tests --- tests/test_courses.py | 3 ++- tests/test_groups.py | 5 +++-- tests/test_projects.py | 7 ++++--- tests/test_submissions.py | 7 ++++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_courses.py b/tests/test_courses.py index 3204f07..3809a11 100644 --- a/tests/test_courses.py +++ b/tests/test_courses.py @@ -8,6 +8,7 @@ import click import click.testing import freezegun +from pick import Option from agiocli.__main__ import main @@ -55,7 +56,7 @@ def test_courses_empty(api_mock, mocker): 'allowed_guest_domain': '@umich.edu', 'last_modified': '2021-04-07T02:19:22.818992Z' } - mocker.patch("pick.pick", return_value=(course_109, 1)) + mocker.patch("pick.pick", return_value=(Option(course_109, course_109), 1)) # Run agio runner = click.testing.CliRunner() diff --git a/tests/test_groups.py b/tests/test_groups.py index 81dfcea..999bf47 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -6,6 +6,7 @@ import json import click import click.testing +from pick import Option from agiocli.__main__ import main @@ -102,8 +103,8 @@ def test_groups_empty(api_mock, mocker, constants): # These are constants in conftest.py. Mock input "awdeorio", which selects # a group. mocker.patch("pick.pick", side_effect=[ - (constants["COURSE_109"], 1), # First call to pick() selects course - (constants["PROJECT_1005"], 0), # Second call selects project + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project ]) mocker.patch("builtins.input", return_value="awdeorio") diff --git a/tests/test_projects.py b/tests/test_projects.py index 829d069..3a9cbb5 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -9,6 +9,7 @@ import click import click.testing import utils +from pick import Option from agiocli.__main__ import main @@ -116,7 +117,7 @@ def test_projects_no_course(api_mock, mocker, constants): """ # Mock user-selection menu, users selects course 109. This constant is # defined in conftest.py - mocker.patch("pick.pick", return_value=(constants["COURSE_109"], 1)) + mocker.patch("pick.pick", return_value=(Option(constants["COURSE_109"], constants["COURSE_109"]), 1)) # Run agio runner = click.testing.CliRunner() @@ -140,8 +141,8 @@ def test_projects_empty(api_mock, mocker, constants): # Mock user-selection menu, users selects course 109, then project 1005. # These constants are defined in conftest.py mocker.patch("pick.pick", side_effect=[ - (constants["COURSE_109"], 1), # First call to pick() selects course - (constants["PROJECT_1005"], 0), # Second call selects project + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project ]) # Run agio diff --git a/tests/test_submissions.py b/tests/test_submissions.py index 05cb60f..88b081f 100644 --- a/tests/test_submissions.py +++ b/tests/test_submissions.py @@ -6,6 +6,7 @@ import json import click import click.testing +from pick import Option from agiocli.__main__ import main @@ -126,9 +127,9 @@ def test_submissions_empty(api_mock, mocker, constants): # then submission 1128572. These are constants in conftest.py. Mock input # "awdeorio", which selects a group. mocker.patch("pick.pick", side_effect=[ - (constants["COURSE_109"], 1), # First call to pick() selects course - (constants["PROJECT_1005"], 0), # Second call selects project - (constants["SUBMISSION_1128572"], 0), # Third call selects submission + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project + (Option(constants["SUBMISSION_1128572"], constants["SUBMISSION_1128572"]), 0), # Third call selects submission ]) mocker.patch("builtins.input", return_value="awdeorio") From 03a50575ea854b429929ba946463e07f5b82c934 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Wed, 21 Sep 2022 12:34:39 -0400 Subject: [PATCH 22/27] Lint --- tests/test_groups.py | 6 ++++-- tests/test_projects.py | 9 ++++++--- tests/test_submissions.py | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/test_groups.py b/tests/test_groups.py index 999bf47..1f8d233 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -103,8 +103,10 @@ def test_groups_empty(api_mock, mocker, constants): # These are constants in conftest.py. Mock input "awdeorio", which selects # a group. mocker.patch("pick.pick", side_effect=[ - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), ]) mocker.patch("builtins.input", return_value="awdeorio") diff --git a/tests/test_projects.py b/tests/test_projects.py index 3a9cbb5..d8e35af 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -117,7 +117,8 @@ def test_projects_no_course(api_mock, mocker, constants): """ # Mock user-selection menu, users selects course 109. This constant is # defined in conftest.py - mocker.patch("pick.pick", return_value=(Option(constants["COURSE_109"], constants["COURSE_109"]), 1)) + mocker.patch("pick.pick", return_value=( + Option(constants["COURSE_109"], constants["COURSE_109"]), 1)) # Run agio runner = click.testing.CliRunner() @@ -141,8 +142,10 @@ def test_projects_empty(api_mock, mocker, constants): # Mock user-selection menu, users selects course 109, then project 1005. # These constants are defined in conftest.py mocker.patch("pick.pick", side_effect=[ - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), ]) # Run agio diff --git a/tests/test_submissions.py b/tests/test_submissions.py index 88b081f..5b8a764 100644 --- a/tests/test_submissions.py +++ b/tests/test_submissions.py @@ -127,9 +127,13 @@ def test_submissions_empty(api_mock, mocker, constants): # then submission 1128572. These are constants in conftest.py. Mock input # "awdeorio", which selects a group. mocker.patch("pick.pick", side_effect=[ - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), # First call to pick() selects course - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), # Second call selects project - (Option(constants["SUBMISSION_1128572"], constants["SUBMISSION_1128572"]), 0), # Third call selects submission + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), + # Third call selects submission + (Option(constants["SUBMISSION_1128572"], + constants["SUBMISSION_1128572"]), 0), ]) mocker.patch("builtins.input", return_value="awdeorio") From bbf8599a2b9c592f5daa7147a9d2f0b4803528b7 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Thu, 22 Sep 2022 15:33:39 -0400 Subject: [PATCH 23/27] Change --queue to --list-json --- agiocli/__main__.py | 10 +++++----- tests/test_groups.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/agiocli/__main__.py b/agiocli/__main__.py index f04085b..33fd2a3 100644 --- a/agiocli/__main__.py +++ b/agiocli/__main__.py @@ -145,14 +145,14 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30 help="Project pk, name, or shorthand.") @click.option("-l", "--list", "show_list", is_flag=True, help="List groups and exit.") -@click.option("-q", "--queue", "show_queue", is_flag=True, - help="List groups in OH Queue format and exit.") +@click.option("-j", "--list-json", "list_json", is_flag=True, + help="List groups in JSON format (2D array) and exit.") @click.option("-w", "--web", is_flag=True, help="Open group in browser.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. # We need to tell pycodestyle to ignore it. # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def groups(ctx, group_arg, project_arg, course_arg, show_list, show_queue, web): # noqa: D301 +def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): # noqa: D301 """Show group detail or list groups. GROUP_ARG is a primary key, name, or member uniqname. @@ -160,7 +160,7 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, show_queue, web): \b EXAMPLES: agio groups --list - agio groups --queue + agio groups --list-json agio groups agio groups 246965 agio groups awdeorio @@ -185,7 +185,7 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, show_queue, web): return # Handle --queue: list groups in OH Queue format and exit - if show_queue: + if list_json: project = utils.get_project_smart(project_arg, course_arg, client) group_list = utils.get_group_list(project, client) output = [utils.group_emails(group) for group in group_list] diff --git a/tests/test_groups.py b/tests/test_groups.py index 1a1581e..ffc0217 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -37,10 +37,10 @@ def test_groups_list(api_mock): assert "[246965] awdeorio" in result.output -def test_groups_queue(api_mock): +def test_groups_list_json(api_mock): """Verify agio groups queue option when project is specified. - $ agio groups --queue --project 1005 + $ agio groups --list-json --project 1005 api_mock is a shared test fixture that mocks responses to REST API requests. It is implemented in conftest.py. @@ -50,7 +50,7 @@ def test_groups_queue(api_mock): result = runner.invoke( main, [ "groups", - "--queue", + "--list-json", "--project", "1005", ], catch_exceptions=False, From 0860f1273d8e07f278501892f6a254d7c33295c0 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Thu, 22 Sep 2022 15:40:08 -0400 Subject: [PATCH 24/27] Change function name --- agiocli/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agiocli/utils.py b/agiocli/utils.py index 9f1af60..62bdf57 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -320,7 +320,7 @@ def parse_project_string(user_input): return asstype, num, subtitle -def parse_project_string_filter(user_input): +def parse_project_string_skipper(user_input): """Wrap parse_project_string to skip errors.""" try: return parse_project_string(user_input) @@ -340,7 +340,7 @@ def project_match(search, projects): # Filter for only parsable projects projects = filter( - lambda x: parse_project_string_filter(x["name"]), projects + lambda x: parse_project_string_skipper(x["name"]), projects ) # Remove projects with an assignment type mismatch (Lab vs. Project, etc.) From 1767dc4cc8da720d09ff833e29039faba0129dfd Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Sat, 24 Sep 2022 17:07:35 -0400 Subject: [PATCH 25/27] Add 485 F22 AG projects as a test case --- tests/test_matching.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_matching.py b/tests/test_matching.py index 31d8be7..411daf8 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -167,6 +167,24 @@ def test_project_match_pattern(search, expected_project_pk): assert project["pk"] == expected_project_pk +@pytest.mark.parametrize( + "search, expected_project_pk", + [ + ("p1", 1527), + ("p2", 1525), + ("p3", 1524), + ("p4", 1526), + ("p5", 1523), + ] +) +def test_project_match_pattern_include_invalid(search, expected_project_pk): + """Many supported input patterns.""" + matches = utils.project_match(search, PROJECTS_INCLUDING_INVALID) + assert len(matches) == 1 + project = matches[0] + assert project["pk"] == expected_project_pk + + @pytest.mark.parametrize( "search", [ @@ -327,3 +345,14 @@ def test_project_match_bad_num(search): {"pk": 434, "name": "Project 5 - Machine Learning"}, {"pk": 426, "name": "Lab 06 - Container ADTs"}, ] + +# These projects are from EECS 485 Fall 2022 +PROJECTS_INCLUDING_INVALID = [ + {"pk": 1527, "name": "Project 1 - Templated Static Site Generator"}, + {"pk": 1525, "name": "Project 2 - Server-side Dynamic Pages"}, + {"pk": 1524, "name": "Project 3 - Client-side Dynamic Pages"}, + {"pk": 1526, "name": "Project 4 - MapReduce"}, + {"pk": 1523, "name": "Project 5 - Search Engine"}, + {"pk": 1749, "name": "Testing JVM errors 20.04"}, + {"pk": 1748, "name": "Testing JVM errors 22.04"}, +] \ No newline at end of file From 8038baca13efcc6c651456b281e60046e19c9cd5 Mon Sep 17 00:00:00 2001 From: Justin Applefield Date: Sat, 24 Sep 2022 17:08:49 -0400 Subject: [PATCH 26/27] Lint --- tests/test_matching.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_matching.py b/tests/test_matching.py index 411daf8..3958671 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -355,4 +355,4 @@ def test_project_match_bad_num(search): {"pk": 1523, "name": "Project 5 - Search Engine"}, {"pk": 1749, "name": "Testing JVM errors 20.04"}, {"pk": 1748, "name": "Testing JVM errors 22.04"}, -] \ No newline at end of file +] From dd549c8438a464308748791f6818b073cb657f27 Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Mon, 26 Sep 2022 09:16:03 -0400 Subject: [PATCH 27/27] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 623465b..88d21e0 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ description="A command line interface to autograder.io", long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", - version="0.4.0", + version="0.5.0", author="Andrew DeOrio", author_email="awdeorio@umich.edu", url="https://github.com/eecs485staff/agio-cli/",