Skip to content

Commit 48463ec

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents 513d3b2 + 651e59b commit 48463ec

File tree

11 files changed

+121
-54
lines changed

11 files changed

+121
-54
lines changed

README.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,11 @@ Autograder.io CLI (`agio`) is a command line interface to [autograder.io](https:
99

1010

1111
## Quick start
12-
First, [obtain a token](#obtaining-a-token) (below).
13-
1412
```console
1513
$ pip install agiocli
1614
$ agio
1715
```
1816

19-
## Obtaining a Token
20-
1. Log in [autograder.io](https://autograder.io/) with your web browser
21-
2. Open browser developer tools
22-
3. Click on a course link
23-
4. In the developer console, click on a request, e.g., `my_roles/` or `projects/`)
24-
5. Under Request Headers, there is an Authorization entry that looks like "Token ". Copy the hex string and save it to the file `.agtoken` in your home
25-
directory.
26-
2717
## Contributing
2818
Contributions from the community are welcome! Check out the [guide for contributing](CONTRIBUTING.md).
2919

agiocli/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def courses(ctx, course_arg, show_list, web): # noqa: D301
5252
agio courses
5353
agio courses 109
5454
agio courses eecs485sp21
55+
agio courses eecs485[cur|current]
5556
5657
"""
5758
try:

agiocli/api_client.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,28 @@ def get_api_token(token_filename: str) -> str:
151151
- If token_filename is an absolute path or a relative path that contains
152152
at least one directory, that file will be opened and the token read.
153153
"""
154-
token_not_found_msg = f"Token file not found: {token_filename}"
154+
# Token filename provided and it does not exist
155155
if os.path.dirname(token_filename) and not os.path.isfile(token_filename):
156-
raise TokenFileNotFound(token_not_found_msg)
156+
raise TokenFileNotFound("Token file does not exist: {token_filename}")
157157

158158
# Make sure that we're starting in a subdir of the home directory
159-
if os.path.expanduser('~') not in os.path.abspath(os.curdir):
160-
raise TokenFileNotFound(token_not_found_msg)
159+
curdir = os.path.abspath(os.curdir)
160+
if os.path.expanduser('~') not in curdir:
161+
raise TokenFileNotFound(f"Invalid search path: {curdir}")
161162

163+
# Search, walking up the directory structure from PWD to home
162164
for dirname in walk_up_to_home_dir():
163165
filename = os.path.join(dirname, token_filename)
164166
if os.path.isfile(filename):
165167
with open(filename, encoding="utf8") as tokenfile:
166168
return tokenfile.read().strip()
167169

168-
raise TokenFileNotFound(token_not_found_msg)
170+
# Didn't find a token file
171+
raise TokenFileNotFound(
172+
f"Token file not found: {token_filename}. Download a token from "
173+
f"https://autograder.io/web/__apitoken__ and save it to "
174+
f"~/{token_filename} or ./{token_filename}"
175+
)
169176

170177

171178
def walk_up_to_home_dir() -> Iterator[str]:

agiocli/utils.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@
2727
9: 4, 10: 4, 11: 4, 12: 4, # Sep-Dec Fall
2828
}
2929

30+
# Map month number to semester name
31+
MONTH_SEMESTER_NAME = {
32+
1: "Winter",
33+
2: "Winter",
34+
3: "Winter",
35+
4: "Winter",
36+
5: "Spring",
37+
6: "Spring",
38+
7: "Summer",
39+
8: "Summer",
40+
9: "Fall",
41+
10: "Fall",
42+
11: "Fall",
43+
12: "Fall",
44+
}
45+
3046

3147
def dict_str(obj):
3248
"""Format a dictionary as an indented string."""
@@ -113,29 +129,44 @@ def parse_course_string(user_input):
113129
[\s_-]* # Optional whitespace or delimiter
114130
(?P<num>\d{3}) # 3 digit course number
115131
[\s_-]* # Optional whitespace or delimiter
116-
(?P<sem> # Semester name or abbreviation
132+
(?P<sem> # Semester name or abbreviation or empty string
117133
w|wn|winter|
118134
sp|s|spring|
119135
su|summer|
120136
sp/su|spsu|ss|spring/summer|
121-
f|fa|fall)
137+
f|fa|fall|
138+
cur|current|)
122139
[\s_-]* # Optional whitespace or delimiter
123-
(?P<year>\d{2,4}) # 2-4 digit year
140+
(?P<year>\d{2,4}|) # 2-4 digit year or empty string
124141
\s* # Optional trailing whitespace
125142
$ # Match ends at the end
126143
"""
127144
match = re.search(pattern, user_input)
128145
if not match:
129146
sys.exit(f"Error: unsupported input format: '{user_input}'")
130147

148+
# User must provide either year and semester or neither. Semester may be
149+
# "cur" or "current."
150+
year = match.group("year")
151+
semester_abbrev = match.group("sem")
152+
if bool(year) ^ (semester_abbrev not in ["", "cur", "current"]):
153+
sys.exit(f"Error: unsupported input format: '{user_input}'")
154+
155+
# Default year and semester
156+
if not year and semester_abbrev in ["", "cur", "current"]:
157+
today = dt.date.today()
158+
year = today.year
159+
semester_abbrev = MONTH_SEMESTER_NAME[today.month]
160+
131161
# Convert year to a number, handling 2-digit year as "20xx"
132-
year = int(match.group("year"))
162+
year = int(year)
133163
assert year >= 0
134164
if year < 100:
135165
year = 2000 + year
136166

137167
# Convert semester abbreviation to semester name. Make sure that the keys
138168
# match the abbreviations in the regular expression above.
169+
semester_abbrev = semester_abbrev.lower()
139170
semester_names = {
140171
"w": "Winter",
141172
"wn": "Winter",
@@ -153,7 +184,6 @@ def parse_course_string(user_input):
153184
"fa": "Fall",
154185
"fall": "Fall",
155186
}
156-
semester_abbrev = match.group("sem").lower()
157187
semester = semester_names[semester_abbrev]
158188

159189
# Course name, with department and catalog number. If no department is

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
description="A command line interface to autograder.io",
1616
long_description=LONG_DESCRIPTION,
1717
long_description_content_type="text/markdown",
18-
version="0.2.0",
18+
version="0.3.0",
1919
author="Andrew DeOrio",
2020
author_email="[email protected]",
2121
url="https://github.com/eecs485staff/agio-cli/",

tests/test_courses.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,9 @@ def test_courses_empty(api_mock, mocker):
5757
}
5858
mocker.patch("pick.pick", return_value=(course_109, 1))
5959

60-
# Run agio, mocking the date to be Jun 2021. We need to mock the date
61-
# because the prompt filters out past courses.
62-
# https://github.com/spulec/freezegun
60+
# Run agio
6361
runner = click.testing.CliRunner()
64-
with freezegun.freeze_time("2021-06-15"):
65-
result = runner.invoke(main, ["courses"], catch_exceptions=False)
62+
result = runner.invoke(main, ["courses"], catch_exceptions=False)
6663

6764
# Check output
6865
assert result.exit_code == 0, result.output
@@ -122,3 +119,26 @@ def test_courses_shortcut(api_mock):
122119
assert result.exit_code == 0, result.output
123120
output_obj = json.loads(result.output)
124121
assert output_obj["pk"] == 109
122+
123+
124+
def test_courses_default_semester(api_mock):
125+
"""Verify courses subcommand with no semester/year shortcut input.
126+
127+
$ agio courses eecs485
128+
129+
api_mock is a shared test fixture that mocks responses to REST API
130+
requests. It is implemented in conftest.py.
131+
132+
"""
133+
# Run agio, mocking the date to be Jun 2021. The current course should be
134+
# spring 2021 https://github.com/spulec/freezegun
135+
runner = click.testing.CliRunner()
136+
with freezegun.freeze_time("2021-06-15"):
137+
result = runner.invoke(
138+
main, ["courses", "eecs485"],
139+
catch_exceptions=False,
140+
)
141+
142+
assert result.exit_code == 0, result.output
143+
output_obj = json.loads(result.output)
144+
assert output_obj["pk"] == 109

tests/test_groups.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import json
77
import click
88
import click.testing
9-
import freezegun
109
from agiocli.__main__ import main
1110

1211

@@ -108,12 +107,9 @@ def test_groups_empty(api_mock, mocker, constants):
108107
])
109108
mocker.patch("builtins.input", return_value="awdeorio")
110109

111-
# Run agio, mocking the date to be Jun 2021. We need to mock the date
112-
# because the prompt filters out past courses.
113-
# https://github.com/spulec/freezegun
110+
# Run agio
114111
runner = click.testing.CliRunner()
115-
with freezegun.freeze_time("2021-06-15"):
116-
result = runner.invoke(main, ["groups"], catch_exceptions=False)
112+
result = runner.invoke(main, ["groups"], catch_exceptions=False)
117113

118114
# Check output
119115
assert result.exit_code == 0, result.output

tests/test_matching.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Unit tests for smart user input string matching."""
2+
import freezegun
23
import pytest
34
from agiocli import utils
45

@@ -91,6 +92,39 @@ def test_course_match_bad_year(search):
9192
assert not matches
9293

9394

95+
@pytest.mark.parametrize(
96+
"search, expected_course_pk",
97+
[
98+
("EECS 280", 111),
99+
("EECS 280 cur", 111),
100+
("EECS 280 current", 111),
101+
("eecs280", 111),
102+
("eecs280cur", 111),
103+
("eecs280current", 111),
104+
("eecs280-cur", 111),
105+
("eecs280-current", 111),
106+
("EECS 485", 109),
107+
("EECS 485 cur", 109),
108+
("EECS 485 current", 109),
109+
("eecs485", 109),
110+
("eecs485cur", 109),
111+
("eecs485current", 109),
112+
("eecs485-cur", 109),
113+
("eecs485-current", 109),
114+
115+
]
116+
)
117+
def test_course_match_current(search, expected_course_pk):
118+
"""Auto select current semester."""
119+
# Run course match, mocking the date to be June 2021
120+
# https://github.com/spulec/freezegun
121+
with freezegun.freeze_time("2021-06-15"):
122+
matches = utils.course_match(search, COURSES)
123+
assert len(matches) == 1
124+
course = matches[0]
125+
assert course["pk"] == expected_course_pk
126+
127+
94128
@pytest.mark.parametrize(
95129
"search, expected_project_pk",
96130
[

tests/test_projects.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import textwrap
88
import click
99
import click.testing
10-
import freezegun
1110
from agiocli.__main__ import main
1211

1312

@@ -117,15 +116,9 @@ def test_projects_no_course(api_mock, mocker, constants):
117116
# defined in conftest.py
118117
mocker.patch("pick.pick", return_value=(constants["COURSE_109"], 1))
119118

120-
# Run agio, mocking the date to be Jun 2021. We need to mock the date
121-
# because the prompt filters out past courses.
122-
# https://github.com/spulec/freezegun
119+
# Run agio
123120
runner = click.testing.CliRunner()
124-
with freezegun.freeze_time("2021-06-15"):
125-
result = runner.invoke(
126-
main, ["projects", "p1"],
127-
catch_exceptions=False,
128-
)
121+
result = runner.invoke(main, ["projects", "p1"], catch_exceptions=False)
129122

130123
# Check output
131124
assert result.exit_code == 0, result.output
@@ -149,12 +142,9 @@ def test_projects_empty(api_mock, mocker, constants):
149142
(constants["PROJECT_1005"], 0), # Second call selects project
150143
])
151144

152-
# Run agio, mocking the date to be Jun 2021. We need to mock the date
153-
# because the prompt filters out past courses.
154-
# https://github.com/spulec/freezegun
145+
# Run agio
155146
runner = click.testing.CliRunner()
156-
with freezegun.freeze_time("2021-06-15"):
157-
result = runner.invoke(main, ["projects"], catch_exceptions=False)
147+
result = runner.invoke(main, ["projects"], catch_exceptions=False)
158148

159149
# Check output
160150
assert result.exit_code == 0, result.output

tests/test_submissions.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import json
77
import click
88
import click.testing
9-
import freezegun
109
from agiocli.__main__ import main
1110

1211

@@ -133,12 +132,9 @@ def test_submissions_empty(api_mock, mocker, constants):
133132
])
134133
mocker.patch("builtins.input", return_value="awdeorio")
135134

136-
# Run agio, mocking the date to be Jun 2021. We need to mock the date
137-
# because the prompt filters out past courses.
138-
# https://github.com/spulec/freezegun
135+
# Run agio
139136
runner = click.testing.CliRunner()
140-
with freezegun.freeze_time("2021-06-15"):
141-
result = runner.invoke(main, ["submissions"], catch_exceptions=False)
137+
result = runner.invoke(main, ["submissions"], catch_exceptions=False)
142138

143139
# Check output
144140
assert result.exit_code == 0, result.output

0 commit comments

Comments
 (0)