Skip to content

Commit

Permalink
Merge 0c4c62c into c5f1426
Browse files Browse the repository at this point in the history
  • Loading branch information
fanglinfang committed Mar 7, 2022
2 parents c5f1426 + 0c4c62c commit 661d3b7
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 60 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@
Installation:

pip install UW-RestClients-MyPlan

To use this client, you'll need these settings in your application or script:

# Specifies whether requests should use live or mocked resources,
# acceptable values are 'Live' or 'Mock' (default)
RESTCLIENTS_MYPLAN_DAO_CLASS='Live'
RESTCLIENTS_MYPLAN_AUTH_DAO_CLASS='Live'
RESTCLIENTS_MYPLAN_AUTH_SECRET=''
RESTCLIENTS_MYPLAN_AUTH_HOST=''
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

import os
from setuptools import setup

Expand All @@ -20,7 +23,10 @@
author="UW-IT AXDD",
author_email="aca-it@uw.edu",
include_package_data=True,
install_requires=['UW-RestClients-Core'],
install_requires=[
'UW-RestClients-Core',
'mock',
],
license='Apache License, Version 2.0',
description=('A library for connecting to the UW MyPlan API'),
long_description=README,
Expand Down
100 changes: 61 additions & 39 deletions uw_myplan/__init__.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,83 @@
# Copyright 2021 UW-IT, University of Washington
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

"""
This is the interface for interacting with MyPlan.
https://wiki.cac.washington.edu/display/MyPlan/Plan+Resource+v1
"""

from uw_myplan.dao import MyPlan_DAO
import json
import logging
from restclients_core.exceptions import DataFailureException
from uw_myplan.dao import MyPlan_DAO
from uw_myplan.models import (
MyPlan, MyPlanTerm, MyPlanCourse, MyPlanCourseSection)
import json

logger = logging.getLogger(__name__)


class Plan(object):

def __init__(self, actas=None):
self.dao = MyPlan_DAO()

def get_plan(regid, year, quarter, terms=4):
dao = MyPlan_DAO()
url = get_plan_url(regid, year, quarter, terms)
def _get_plan_url(self, regid, year, quarter, terms):
return "/plan/v1/{year},{quarter},{terms},{uwregid}".format(
year=year, quarter=quarter, terms=terms, uwregid=regid)

response = dao.getURL(url, {"Accept": "application/json"})
if response.status != 200:
raise DataFailureException(url, response.status, str(response.data))
def _get_resource(self, regid, year, quarter, terms,
clear_cached_token=False):
if clear_cached_token:
self.dao.clear_access_token()
return self.dao.getURL(
self._get_plan_url(regid, year, quarter, terms),
{"Accept": "application/json"})

data = json.loads(response.data)
def get_plan(self, regid, year, quarter, terms=4):
response = self._get_resource(regid, year, quarter, terms)
if response.status == 200:
return self._process_data(json.loads(response.data))

plan = MyPlan()
for term_data in data:
term = MyPlanTerm()
term.year = term_data["Term"]["Year"]
term.quarter = term_data["Term"]["Quarter"]
if response.status == 401 or response.status == 403:
# clear cached access token, retry once
response = self._get_resource(
regid, year, quarter, terms, clear_cached_token=True)
if response.status == 200:
return self._process_data(json.loads(response.data))

term.course_search_href = term_data["CourseSearchHref"]
term.degree_audit_href = term_data["DegreeAuditHref"]
term.myplan_href = term_data["MyPlanHref"]
term.registration_href = term_data["RegistrationHref"]
term.registered_courses_count = int(
term_data["RegisteredCoursesCount"])
term.registered_sections_count = int(
term_data["RegisteredSectionsCount"])
raise DataFailureException(
self._get_plan_url(regid, year, quarter, terms),
response.status, str(response.data))

for course_data in term_data["Courses"]:
course = MyPlanCourse()
course.curriculum_abbr = course_data["CurriculumAbbreviation"]
course.course_number = course_data["CourseNumber"]
def _process_data(self, jdata):
plan = MyPlan()
for term_data in jdata:
term = MyPlanTerm()
term.year = term_data["Term"]["Year"]
term.quarter = term_data["Term"]["Quarter"]

is_available = course_data["RegistrationAvailable"]
course.registrations_available = is_available
term.course_search_href = term_data["CourseSearchHref"]
term.degree_audit_href = term_data["DegreeAuditHref"]
term.myplan_href = term_data["MyPlanHref"]
term.registration_href = term_data["RegistrationHref"]
term.registered_courses_count = int(
term_data["RegisteredCoursesCount"])
term.registered_sections_count = int(
term_data["RegisteredSectionsCount"])

for section_data in course_data["Sections"]:
section = MyPlanCourseSection()
section.section_id = section_data["SectionId"]
course.sections.append(section)
for course_data in term_data["Courses"]:
course = MyPlanCourse()
course.curriculum_abbr = course_data["CurriculumAbbreviation"]
course.course_number = course_data["CourseNumber"]

term.courses.append(course)
plan.terms.append(term)
return plan
is_available = course_data["RegistrationAvailable"]
course.registrations_available = is_available

for section_data in course_data["Sections"]:
section = MyPlanCourseSection()
section.section_id = section_data["SectionId"]
course.sections.append(section)

def get_plan_url(regid, year, quarter, terms=4):
return "/student/api/plan/v1/{year},{quarter},{terms},{uwregid}".format(
year=year, quarter=quarter, terms=terms, uwregid=regid)
term.courses.append(course)
plan.terms.append(term)
return plan
66 changes: 65 additions & 1 deletion uw_myplan/dao.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
# Copyright 2021 UW-IT, University of Washington
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

import json
import logging
import os
from os.path import abspath, dirname
from restclients_core.dao import DAO
from restclients_core.exceptions import DataFailureException

logger = logging.getLogger(__name__)
myplan_access_token_url = "/oauth2/token"


class MyPlan_Auth_DAO(DAO):
def service_name(self):
return "myplan_auth"

def _is_cacheable(self, method, url, headers, body=None):
return True

def clear_token_from_cache(self):
self.clear_cached_response(myplan_access_token_url)

def get_auth_token(self, secret):
headers = {"Authorization": "Basic {}".format(secret),
"Content-type": "application/x-www-form-urlencoded"}

response = self.postURL(
myplan_access_token_url, headers, "grant_type=client_credentials")
if response.status != 200:
logger.error(
{'url': myplan_access_token_url,
'status': response.status,
'data': response.data})
raise DataFailureException(
myplan_access_token_url, response.status, response.data)

data = json.loads(response.data)
return data.get("access_token", "")

def service_mock_paths(self):
return [abspath(os.path.join(dirname(__file__), "resources"))]

def _edit_mock_response(self, method, url, headers, body, response):
if response.status == 404 and method != "GET":
alternative_url = "{0}.{1}".format(url, method)
backend = self.get_implementation()
new_resp = backend.load(method, alternative_url, headers, body)
response.status = new_resp.status
response.data = new_resp.data
logger.debug(
{'url': alternative_url,
'status': response.status,
'data': response.data})


class MyPlan_DAO(DAO):

def __init__(self):
self.auth_dao = MyPlan_Auth_DAO()
return super(MyPlan_DAO, self).__init__()

def service_name(self):
return 'myplan'

def service_mock_paths(self):
return [abspath(os.path.join(dirname(__file__), "resources"))]

def _custom_headers(self, method, url, headers, body):
if not headers:
headers = {}
secret = self.get_service_setting("AUTH_SECRET", "")
if secret:
headers["Authorization"] = self.auth_dao.get_auth_token(secret)
return headers

def clear_access_token(self):
self.auth_dao.clear_token_from_cache()
2 changes: 1 addition & 1 deletion uw_myplan/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 UW-IT, University of Washington
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from restclients_core import models
Expand Down
5 changes: 5 additions & 0 deletions uw_myplan/resources/myplan_auth/file/oauth2/token.POST
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"access_token":"eyJraWQiOmYmEwwvOQ",
"expires_in":3600,
"token_type":"Bearer"
}
2 changes: 1 addition & 1 deletion uw_myplan/test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2021 UW-IT, University of Washington
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

# This is just a test runner for coverage
Expand Down
46 changes: 46 additions & 0 deletions uw_myplan/tests/test_dao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from unittest import TestCase
import mock
from commonconf import override_settings
from restclients_core.exceptions import DataFailureException
from restclients_core.models import MockHTTP
from uw_myplan.dao import MyPlan_Auth_DAO, MyPlan_DAO
from uw_myplan.utils import (
fdao_myplan_override, fdao_myplan_auth_override)


@fdao_myplan_auth_override
@fdao_myplan_override
class TestMyPlanAuth(TestCase):

def test_is_cacheable(self):
auth = MyPlan_Auth_DAO()
self.assertTrue(auth._is_cacheable("POST", "/", {}, ""))

def test_get_auth_token(self):
self.assertIsNotNone(
MyPlan_Auth_DAO().get_auth_token("test1"))

@mock.patch.object(MyPlan_Auth_DAO, "postURL")
def test_get_auth_token_error(self, mock):
response = MockHTTP()
response.status = 404
response.data = "Not Found"
mock.return_value = response
self.assertRaises(
DataFailureException,
MyPlan_Auth_DAO().get_auth_token, "test1")

def test_no_auth_header(self):
headers = MyPlan_DAO()._custom_headers("GET", "/", {}, "")
self.assertFalse("Authorization" in headers)

@override_settings(RESTCLIENTS_MYPLAN_AUTH_SECRET="test1")
@mock.patch.object(MyPlan_Auth_DAO, "get_auth_token")
def test_auth_header(self, mock_get_auth_token):
mock_get_auth_token.return_value = "abcdef"
headers = MyPlan_DAO()._custom_headers("GET", "/", {}, "")
self.assertTrue("Authorization" in headers)
self.assertEqual(headers["Authorization"], "abcdef")
53 changes: 36 additions & 17 deletions uw_myplan/tests/test_myplan.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
# Copyright 2021 UW-IT, University of Washington
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from unittest import TestCase
from uw_myplan import get_plan, get_plan_url
import mock
from restclients_core.models import MockHTTP
from restclients_core.exceptions import DataFailureException
from uw_myplan import Plan


class MyPlanTestData(TestCase):
class PlanTest(TestCase):
def test_plan_url(self):
self.assertEquals(
get_plan_url(
Plan()._get_plan_url(
"9136CCB8F66711D5BE060004AC494FFE", 2013, "spring", 2), (
"/student/api/plan/v1/2013,spring,2,"
"/plan/v1/2013,spring,2,"
"9136CCB8F66711D5BE060004AC494FFE"))

self.assertEquals(
get_plan_url(
"9136CCB8F66711D5BE060004AC494FFE", 2012, "summer"), (
"/student/api/plan/v1/2012,summer,4,"
"9136CCB8F66711D5BE060004AC494FFE"))
@mock.patch.object(Plan, "_get_resource")
def test_error_401(self, mock):
response = MockHTTP()
response.status = 403
response.data = "Not Authorized"
mock.return_value = response
self.assertRaises(
DataFailureException,
Plan().get_plan,
"9136CCB8F66711D5BE060004AC494FFE",
2013,
"spring",
terms=4)

def test_javerage(self):
plan = get_plan(regid="9136CCB8F66711D5BE060004AC494FFE",
year=2013,
quarter="spring",
terms=4)
plan = Plan().get_plan(
regid="9136CCB8F66711D5BE060004AC494FFE",
year=2013,
quarter="spring",
terms=4)
self.assertEquals(len(plan.terms), 4)

self.assertEquals(plan.terms[0].year, 2013)
Expand Down Expand Up @@ -67,10 +79,17 @@ def test_javerage(self):
self.assertEquals(term_data.courses[0].sections[1].section_id, 'AA')
self.assertEquals(term_data.courses[0].sections[2].section_id, 'AB')

resp = Plan()._get_resource(
"9136CCB8F66711D5BE060004AC494FFE", 2013, "spring",
4, clear_cached_token=True)
self.assertIsNotNone(resp)

def test_json(self):
plan = get_plan(regid="9136CCB8F66711D5BE060004AC494FFE",
year=2013, quarter="spring",
terms=4)
plan = Plan().get_plan(
regid="9136CCB8F66711D5BE060004AC494FFE",
year=2013,
quarter="spring",
terms=4)
json_data = plan.json_data()
term_data = json_data["terms"][0]
self.assertEquals(
Expand Down
9 changes: 9 additions & 0 deletions uw_myplan/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2022 UW-IT, University of Washington
# SPDX-License-Identifier: Apache-2.0

from commonconf import override_settings

fdao_myplan_override = override_settings(
RESTCLIENTS_MYPLAN_DAO_CLASS='Mock')
fdao_myplan_auth_override = override_settings(
RESTCLIENTS_MYPLAN_AUTH_DAO_CLASS='Mock')

0 comments on commit 661d3b7

Please sign in to comment.