Skip to content

Commit

Permalink
Merge pull request #42 from nyuoss/feat-configurable-gradescope-url
Browse files Browse the repository at this point in the history
feat: add support for configurable gradescope url #40
  • Loading branch information
calvinatian authored Jan 26, 2025
2 parents ae1aa9e + 2639fb1 commit 397e25b
Show file tree
Hide file tree
Showing 14 changed files with 84 additions and 56 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ jobs:
# - name: Lint code
# run: pdm lint

- name: Start project
run: pdm run start

- name: Run Tests
run: pdm test
env:
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ Implemented Features Include:
* Upload submissions to assignments
* API server to interact with library without Python

## Demo

## Demo
To get a feel for how the API works, we have provided a demo video of the features in-use: [link](https://youtu.be/eK9m4nVjU1A?si=6GTevv23Vym0Mu8V)

Note that we only demo interacting with the API server, you can alternatively use the Python library directly.

Note that we only demo interacting with the API server, you can alternatively use the Python library directly.

## Setup

Expand All @@ -39,6 +38,7 @@ pip install gradescopeapi
For additional methods of installation, refer to the [install guide](docs/INSTALL.md)

## Usage

The project is designed to be simple and easy to use. As such, we have provided users with two different options for using this project.

### Option 1: FastAPI
Expand All @@ -51,10 +51,10 @@ To run the API server locally on your machine, open the project repository on yo

1. Navigate to the `src.gradescopeapi.api` directory
2. Run the command: `uvicorn api:app --reload` to run the server locally
3. In a web browser, navigate to `localhost:8000/docs`, to see the auto-generated FastAPI docs

3. In a web browser, navigate to `localhost:8000/docs`, to see the auto-generated FastAPI docs

### Option 2: Python

Alternatively, you can use Python to use the library directly. We have provided some sample scripts of common tasks one might do:

```python
Expand Down Expand Up @@ -93,7 +93,7 @@ For more examples of features not covered here such as changing extensions, uplo

## Testing

For information on how to run your own tests using ```gradescopeapi```, refer to [TESTING.md](docs/TESTING.md)
For information on how to run your own tests using `gradescopeapi`, refer to [TESTING.md](docs/TESTING.md)

## Contributing Guidelines

Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ maintainers = [
name = "gradescopeapi"
readme = "README.md"
requires-python = ">=3.10"
version = "1.3.1"
version = "1.4.0"

[project.license]
text = "MIT"
Expand All @@ -59,7 +59,6 @@ format = "ruff format src tests"
format-test = "ruff format --check src tests"
lint = "ruff check src tests"
lint-fix = "ruff check --fix src tests"
start = "python -m gradescopeapi"
test = "pytest tests"
test-cov-generate-html = "coverage html"
test-cov-generate-report = "coverage run --source=gradescopeapi --module pytest tests"
Expand Down
2 changes: 1 addition & 1 deletion src/gradescopeapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# empty on purpose
DEFAULT_GRADESCOPE_BASE_URL = "https://www.gradescope.com"
6 changes: 0 additions & 6 deletions src/gradescopeapi/__main__.py

This file was deleted.

11 changes: 9 additions & 2 deletions src/gradescopeapi/classes/_helpers/_assignment_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dateutil.parser
import requests

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL
from gradescopeapi.classes.assignments import Assignment


Expand Down Expand Up @@ -159,9 +160,15 @@ def get_assignments_student_view(coursepage_soup):
return assignment_info_list


def get_submission_files(session, course_id, assignment_id, submission_id):
def get_submission_files(
session,
course_id,
assignment_id,
submission_id,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
):
ASSIGNMENT_ENDPOINT = (
f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}"
f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}"
)

file_info_link = f"{ASSIGNMENT_ENDPOINT}/submissions/{submission_id}.json?content=react&only_keys[]=text_files&only_keys[]=file_comments"
Expand Down
19 changes: 13 additions & 6 deletions src/gradescopeapi/classes/_helpers/_login_helpers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import requests
from bs4 import BeautifulSoup

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL

def get_auth_token_init_gradescope_session(session: requests.Session) -> str:

def get_auth_token_init_gradescope_session(
session: requests.Session,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
) -> str:
"""
Go to homepage to parse hidden authenticity token and to set initial "_gradescope_session" cookie
"""
GS_LANDING_ENDPOINT = "https://gradescope.com"

# go to homepage and set initial "_gradescope_session" cookie
homepage_resp = session.get(GS_LANDING_ENDPOINT)
homepage_resp = session.get(gradescope_base_url)
homepage_soup = BeautifulSoup(homepage_resp.text, "html.parser")

# Find the authenticity token using CSS selectors
Expand All @@ -20,9 +23,13 @@ def get_auth_token_init_gradescope_session(session: requests.Session) -> str:


def login_set_session_cookies(
session: requests.Session, email: str, password: str, auth_token: str
session: requests.Session,
email: str,
password: str,
auth_token: str,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
) -> bool:
GS_LOGIN_ENDPOINT = "https://www.gradescope.com/login"
GS_LOGIN_ENDPOINT = f"{gradescope_base_url}/login"

# populate params for post request to login endpoint
login_data = {
Expand Down
20 changes: 13 additions & 7 deletions src/gradescopeapi/classes/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from bs4 import BeautifulSoup

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL
from gradescopeapi.classes._helpers._assignment_helpers import (
check_page_auth,
get_assignments_instructor_view,
Expand All @@ -17,8 +18,13 @@


class Account:
def __init__(self, session):
def __init__(
self,
session,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
):
self.session = session
self.gradescope_base_url = gradescope_base_url

def get_courses(self) -> dict:
"""
Expand All @@ -44,7 +50,7 @@ def get_courses(self) -> dict:
RuntimeError: If request to account page fails.
"""

endpoint = "https://www.gradescope.com/account"
endpoint = f"{self.gradescope_base_url}/account"

# get main page
response = self.session.get(endpoint)
Expand Down Expand Up @@ -92,7 +98,7 @@ def get_course_users(self, course_id: str) -> list[Member]:
"""

membership_endpoint = (
f"https://www.gradescope.com/courses/{course_id}/memberships"
f"{self.gradescope_base_url}/courses/{course_id}/memberships"
)

# check that course_id is valid (not empty)
Expand Down Expand Up @@ -124,7 +130,7 @@ def get_assignments(self, course_id: str) -> list[Assignment]:
"You are not authorized to access this page.": if logged in user is unable to access submissions
"You must be logged in to access this page.": if no user is logged in
"""
course_endpoint = f"https://www.gradescope.com/courses/{course_id}"
course_endpoint = f"{self.gradescope_base_url}/courses/{course_id}"
# check that course_id is valid (not empty)
if not course_id:
raise Exception("Invalid Course ID")
Expand Down Expand Up @@ -170,7 +176,7 @@ def get_assignment_submissions(
2. Not recommended for use, since this makes a GET request for every submission -> very slow!
3. so far only accessible for teachers, not for students to get submissions to an assignment
"""
ASSIGNMENT_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}"
ASSIGNMENT_ENDPOINT = f"{self.gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}"
ASSIGNMENT_SUBMISSIONS_ENDPOINT = f"{ASSIGNMENT_ENDPOINT}/review_grades"
if not course_id or not assignment_id:
raise Exception("One or more invalid parameters")
Expand Down Expand Up @@ -216,7 +222,7 @@ def get_assignment_submission(
NOTE: so far only accessible for teachers, not for students to get their own submission
"""
# fetch submission id
ASSIGNMENT_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}"
ASSIGNMENT_ENDPOINT = f"{self.gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}"
ASSIGNMENT_SUBMISSIONS_ENDPOINT = f"{ASSIGNMENT_ENDPOINT}/review_grades"
if not (student_email and course_id and assignment_id):
raise Exception("One or more invalid parameters")
Expand Down Expand Up @@ -262,7 +268,7 @@ def get_assignment_graders(self, course_id: str, question_id: str) -> set[str]:
"Page not Found": When link is invalid: change in url, invalid course_if or assignment id
"""
QUESTION_ENDPOINT = (
f"https://www.gradescope.com/courses/{course_id}/questions/{question_id}"
f"{self.gradescope_base_url}/courses/{course_id}/questions/{question_id}"
)
ASSIGNMENT_SUBMISSIONS_ENDPOINT = f"{QUESTION_ENDPOINT}/submissions"
if not course_id or not question_id:
Expand Down
9 changes: 7 additions & 2 deletions src/gradescopeapi/classes/assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from bs4 import BeautifulSoup
from requests_toolbelt.multipart.encoder import MultipartEncoder

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL


@dataclass
class Assignment:
Expand All @@ -27,6 +29,7 @@ def update_assignment_date(
release_date: datetime.datetime | None = None,
due_date: datetime.datetime | None = None,
late_due_date: datetime.datetime | None = None,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
):
"""Update the dates of an assignment on Gradescope.
Expand All @@ -45,9 +48,11 @@ def update_assignment_date(
Returns:
bool: True if the assignment dates were successfully updated, False otherwise.
"""
GS_EDIT_ASSIGNMENT_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}/edit"
GS_EDIT_ASSIGNMENT_ENDPOINT = (
f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/edit"
)
GS_POST_ASSIGNMENT_ENDPOINT = (
f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}"
f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}"
)

# Get auth token
Expand Down
12 changes: 8 additions & 4 deletions src/gradescopeapi/classes/connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import requests

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL
from gradescopeapi.classes._helpers._login_helpers import (
get_auth_token_init_gradescope_session,
login_set_session_cookies,
Expand All @@ -8,21 +9,24 @@


class GSConnection:
def __init__(self):
def __init__(self, gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL):
self.session = requests.Session()
self.gradescope_base_url = gradescope_base_url
self.logged_in = False
self.account = None

def login(self, email, password):
# go to homepage to parse hidden authenticity token and to set initial "_gradescope_session" cookie
auth_token = get_auth_token_init_gradescope_session(self.session)
auth_token = get_auth_token_init_gradescope_session(
self.session, self.gradescope_base_url
)

# login and set cookies in session. Result bool on whether login was success
login_success = login_set_session_cookies(
self.session, email, password, auth_token
self.session, email, password, auth_token, self.gradescope_base_url
)
if login_success:
self.logged_in = True
self.account = Account(self.session)
self.account = Account(self.session, self.gradescope_base_url)
else:
raise ValueError("Invalid credentials.")
12 changes: 9 additions & 3 deletions src/gradescopeapi/classes/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import requests
from bs4 import BeautifulSoup

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL


@dataclass
class Extension:
Expand All @@ -30,7 +32,10 @@ class Extension:


def get_extensions(
session: requests.Session, course_id: str, assignment_id: str
session: requests.Session,
course_id: str,
assignment_id: str,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
) -> dict:
"""Get all extensions for an assignment.
Expand All @@ -52,7 +57,7 @@ def get_extensions(
RuntimeError: If the request to get extensions fails.
"""

GS_EXTENSIONS_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}/extensions"
GS_EXTENSIONS_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/extensions"
GS_EXTENSIONS_TABLE_CSS_CLASSES = (
"table js-overridesTable" # Table containing extensions
)
Expand Down Expand Up @@ -139,6 +144,7 @@ def update_student_extension(
release_date: datetime.datetime | None = None,
due_date: datetime.datetime | None = None,
late_due_date: datetime.datetime | None = None,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
) -> bool:
"""Updates the extension for a student on an assignment.
Expand Down Expand Up @@ -212,7 +218,7 @@ def add_to_body(extension_name: str, extension_datetime: datetime.datetime):

# send the request
resp = session.post(
f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}/extensions",
f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/extensions",
json=body,
)
return resp.status_code == 200
Expand Down
7 changes: 5 additions & 2 deletions src/gradescopeapi/classes/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
from bs4 import BeautifulSoup
from requests_toolbelt.multipart.encoder import MultipartEncoder

from gradescopeapi import DEFAULT_GRADESCOPE_BASE_URL


def upload_assignment(
session: requests.Session,
course_id: str,
assignment_id: str,
*files: io.TextIOWrapper,
leaderboard_name: str | None = None,
gradescope_base_url: str = DEFAULT_GRADESCOPE_BASE_URL,
) -> str | None:
"""Uploads given file objects to the specified assignment on Gradescope.
Expand All @@ -28,8 +31,8 @@ def upload_assignment(
Returns:
str | None: Link to submission if successful or None if unsuccessful.
"""
GS_COURSE_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}"
GS_UPLOAD_ENDPOINT = f"https://www.gradescope.com/courses/{course_id}/assignments/{assignment_id}/submissions"
GS_COURSE_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}"
GS_UPLOAD_ENDPOINT = f"{gradescope_base_url}/courses/{course_id}/assignments/{assignment_id}/submissions"

# Get auth token
# TODO: Refactor to helper function since it is needed in multiple places
Expand Down
6 changes: 3 additions & 3 deletions tests/test_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ def test_get_extensions(create_session):
assignment_id = "4330410"

extensions = get_extensions(test_session, course_id, assignment_id)
assert (
len(extensions) > 0
), f"Got 0 extensions for course {course_id} and assignment {assignment_id}"
assert len(extensions) > 0, (
f"Got 0 extensions for course {course_id} and assignment {assignment_id}"
)


def test_valid_change_extension(create_session):
Expand Down
Loading

0 comments on commit 397e25b

Please sign in to comment.