Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .pylintrc

This file was deleted.

11 changes: 4 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ $ pytest --cov ./agiocli --cov-report term-missing
$ pytest tests/test_courses.py::test_courses_pk
```

Lint (all four must pass; this is what CI runs):
Lint (all three must pass; this is what CI runs):
```console
$ pycodestyle agiocli tests
$ pydocstyle agiocli tests
$ pylint agiocli tests
$ ruff check agiocli tests
$ ruff format --check agiocli tests
$ check-manifest
```

Expand All @@ -36,8 +35,6 @@ Run the full lint + test suite in a clean throwaway virtualenv (mirrors CI exact
$ tox -e py3
```

Note: pydocstyle cannot glob `tests/`, so tox invokes it as `sh -c "pydocstyle agiocli tests/*"`. When running pydocstyle manually against tests, match that pattern.

## Architecture

Three modules under `agiocli/`, layered:
Expand Down Expand Up @@ -68,7 +65,7 @@ Tests are system tests driven through Click's `CliRunner` (`runner.invoke(main,

- Errors surfaced to the user are reported via `sys.exit("Error: ...")`, not exceptions, except `TokenFileNotFound` and `UnsupportedAssignmentError` which are caught by callers.
- Click docstrings use `\b` to prevent paragraph rewrapping; lines with `\b` carry a `# noqa: D301`.
- Subcommands with many params carry `# pylint: disable=too-many-arguments` because each CLI option needs a function parameter.
- Subcommands with many params carry `# noqa: PLR0913` because each CLI option needs a function parameter.
- The version string lives in `pyproject.toml` (`version =`); bumping it is a manual step in the release procedure.

## Release
Expand Down
5 changes: 2 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ $ pytest --cov ./agiocli --cov-report term-missing

Test code style
```console
$ pycodestyle agiocli tests
$ pydocstyle agiocli tests
$ pylint agiocli tests
$ ruff check agiocli tests
$ ruff format --check agiocli tests
$ check-manifest
```

Expand Down
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
include LICENSE
include MANIFEST.in
include *.md
include .pylintrc
graft tests

# Avoid dev and and binary files
Expand Down
4 changes: 2 additions & 2 deletions agiocli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Autograder.io CLI API."""

from .api_client import APIClient, TokenFileNotFound
from .utils import *
from .api_client import APIClient as APIClient
from .api_client import TokenFileNotFound as TokenFileNotFound
95 changes: 43 additions & 52 deletions agiocli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

Andrew DeOrio <awdeorio@umich.edu>
"""

import sys

import click

from agiocli import APIClient, TokenFileNotFound, utils


Expand Down Expand Up @@ -34,14 +37,13 @@ def login(ctx):

@main.command()
@click.argument("course_arg", required=False)
@click.option("-l", "--list", "show_list", is_flag=True,
help="List courses and exit.")
@click.option("-l", "--list", "show_list", is_flag=True, help="List courses and exit.")
@click.option("-w", "--web", is_flag=True, help="Open course 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.
# We need to tell ruff to ignore it (D301).
# https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping
def courses(ctx, course_arg, show_list, web): # noqa: D301
def courses(ctx, course_arg, show_list, web):
"""Show course detail or list courses.

COURSE_ARG is a primary key, name, or shorthand.
Expand All @@ -54,7 +56,7 @@ def courses(ctx, course_arg, show_list, web): # noqa: D301
agio courses eecs485sp21
agio courses eecs485[cur|current]

"""
""" # noqa: D301
try:
client = APIClient.make_default(debug=ctx.obj["DEBUG"])
except TokenFileNotFound as err:
Expand All @@ -77,17 +79,15 @@ def courses(ctx, course_arg, show_list, web): # noqa: D301

@main.command()
@click.argument("project_arg", required=False)
@click.option("-c", "--course", "course_arg",
help="Course pk, name, or shorthand.")
@click.option("-l", "--list", "show_list", is_flag=True,
help="List projects and exit.")
@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.")
@click.option("-l", "--list", "show_list", is_flag=True, help="List projects and exit.")
@click.option("-w", "--web", is_flag=True, help="Open project in browser.")
@click.option("--config", is_flag=True, help="Get test suite config.")
@click.pass_context
# The \b character in the docstring prevents Click from rewraping a paragraph.
# We need to tell pycodestyle to ignore it.
# We need to tell ruff to ignore it (D301).
# https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping
def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D301
def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: PLR0913
"""Show project detail or list projects.

PROJECT_ARG is a primary key, name, or shorthand.
Expand All @@ -102,8 +102,7 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30
agio projects p1
agio projects --course eecs485sp21 p1 --config

"""
# pylint: disable=too-many-arguments,too-many-positional-arguments
""" # noqa: D301
try:
client = APIClient.make_default(debug=ctx.obj["DEBUG"])
except TokenFileNotFound as err:
Expand All @@ -122,9 +121,7 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30

# Print test suite config if --config flag
if config:
config_json = client.get(
f"/api/projects/{project['pk']}/ag_test_suites/"
)
config_json = client.get(f"/api/projects/{project['pk']}/ag_test_suites/")
print(utils.dict_str(config_json))
return

Expand All @@ -139,20 +136,22 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30

@main.command()
@click.argument("group_arg", required=False)
@click.option("-c", "--course", "course_arg",
help="Course pk, name, or shorthand.")
@click.option("-p", "--project", "project_arg",
help="Project pk, name, or shorthand.")
@click.option("-l", "--list", "show_list", is_flag=True,
help="List groups and exit.")
@click.option("-j", "--list-json", "list_json", is_flag=True,
help="List groups in JSON format (2D array) and exit.")
@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.")
@click.option("-p", "--project", "project_arg", help="Project pk, name, or shorthand.")
@click.option("-l", "--list", "show_list", is_flag=True, help="List groups 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.
# We need to tell ruff to ignore it (D301).
# https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping
def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): # noqa: D301
def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): # noqa: PLR0913
"""Show group detail or list groups.

GROUP_ARG is a primary key, name, or member uniqname.
Expand All @@ -167,10 +166,7 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web):
agio groups awdeorio --project 1005
agio groups awdeorio --course eecs485sp21 --project p1

"""
# We must have an function argument for each CLI argument or option
# pylint: disable=too-many-arguments,too-many-positional-arguments

""" # noqa: D301
try:
client = APIClient.make_default(debug=ctx.obj["DEBUG"])
except TokenFileNotFound as err:
Expand Down Expand Up @@ -207,22 +203,24 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web):

@main.command()
@click.argument("submission_arg", required=False)
@click.option("-c", "--course", "course_arg",
help="Course pk, name, or shorthand.")
@click.option("-p", "--project", "project_arg",
help="Project pk, name, or shorthand.")
@click.option("-g", "--group", "group_arg",
help="Group pk or member uniqname.")
@click.option("-l", "--list", "show_list", is_flag=True,
help="List groups and exit.")
@click.option("-d", "--download", is_flag=True,
help="Download submission files.")
@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.")
@click.option("-p", "--project", "project_arg", help="Project pk, name, or shorthand.")
@click.option("-g", "--group", "group_arg", help="Group pk or member uniqname.")
@click.option("-l", "--list", "show_list", is_flag=True, help="List groups and exit.")
@click.option("-d", "--download", is_flag=True, help="Download submission files.")
@click.pass_context
# The \b character in the docstring prevents Click from rewraping a paragraph.
# We need to tell pycodestyle to ignore it.
# We need to tell ruff to ignore it (D301).
# https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping
def submissions(ctx, submission_arg, group_arg,
project_arg, course_arg, show_list, download): # noqa: D301
def submissions( # noqa: PLR0913
ctx,
submission_arg,
group_arg,
project_arg,
course_arg,
show_list,
download,
):
"""Show submission detail or list submissions.

SUBMISSION_ARG is a primary key, 'best', or 'last'
Expand All @@ -236,20 +234,15 @@ def submissions(ctx, submission_arg, group_arg,
agio submissions [...] best
agio submissions [...] last
agio submissions [...] --download
"""
# We must have an function argument for each CLI argument or option
# pylint: disable=too-many-arguments,too-many-positional-arguments

""" # noqa: D301
try:
client = APIClient.make_default(debug=ctx.obj["DEBUG"])
except TokenFileNotFound as err:
sys.exit(err)

# Handle --list: list submissions and exit
if show_list:
group = utils.get_group_smart(
group_arg, project_arg, course_arg, client
)
group = utils.get_group_smart(group_arg, project_arg, course_arg, client)
submission_list = utils.get_submission_list(group, client)
for i in submission_list:
print(utils.submission_str(i))
Expand All @@ -270,6 +263,4 @@ def submissions(ctx, submission_arg, group_arg,


if __name__ == "__main__":
# These errors are endemic to click
# pylint: disable=no-value-for-parameter,unexpected-keyword-arg
main(obj={})
26 changes: 13 additions & 13 deletions agiocli/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
Based on HTTPClient by James Perretta
https://github.com/eecs-autograder/autograder-contrib/
"""

import copy
import os
import json
import os
import sys
from typing import Iterator
from collections.abc import Iterator
from urllib.parse import urljoin

import requests


Expand All @@ -28,9 +30,7 @@ class APIClient:

@staticmethod
def make_default(
token_filename='.agtoken',
base_url='https://autograder.io/',
debug=False
token_filename=".agtoken", base_url="https://autograder.io/", debug=False
):
"""Create an APIClient instance with API token found in token_filename.

Expand Down Expand Up @@ -69,7 +69,7 @@ def get_paginated(self, path, *args, **kwargs):
assert "results" in response.json()
assert "next" in response.json()
yield from response.json()["results"]
page_url = response.json()['next']
page_url = response.json()["next"]

def post(self, path, *args, **kwargs):
"""Call requests.post with authentication headers and base URL."""
Expand Down Expand Up @@ -105,8 +105,8 @@ def do_request(self, method_func, path, *args, **kwargs):
print(f"{method} {url}")

# Call the underlying requests library function
headers = copy.deepcopy(kwargs.pop('headers', {}))
headers['Authorization'] = f'Token {self.api_token}'
headers = copy.deepcopy(kwargs.pop("headers", {}))
headers["Authorization"] = f"Token {self.api_token}"
response = method_func(url, *args, headers=headers, **kwargs)

# Print the response
Expand All @@ -123,15 +123,15 @@ def do_request(self, method_func, path, *args, **kwargs):
# Decode JSON
if "Content-Type" not in response.headers:
sys.exit(f"Error: no Content-Type from: {response.url}")
if 'application/json' in response.headers['Content-Type']:
if "application/json" in response.headers["Content-Type"]:
try:
return response.json()
except json.JSONDecodeError:
sys.exit(
f"Error: JSON decoding failed for url {response.url}\n"
f"{response.text}"
)
elif 'application/octet-stream' in response.headers['Content-Type']:
elif "application/octet-stream" in response.headers["Content-Type"]:
return response.content
else:
sys.exit(
Expand All @@ -156,7 +156,7 @@ def get_api_token(token_filename: str) -> str:

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

# Search, walking up the directory structure from PWD to home
Expand All @@ -177,7 +177,7 @@ def get_api_token(token_filename: str) -> str:
def walk_up_to_home_dir() -> Iterator[str]:
"""Iterate up the directory structure from pwd to home directory."""
current_dir = os.path.abspath(os.curdir)
home_dir = os.path.expanduser('~')
home_dir = os.path.expanduser("~")

while current_dir != home_dir:
yield current_dir
Expand All @@ -197,5 +197,5 @@ def print_response(response):
print(formatted)


class TokenFileNotFound(Exception):
class TokenFileNotFound(Exception): # noqa: N818 -- public API, don't rename
"""Exception type indicating failure to locate user token file."""
Loading
Loading