├── .gitignore ├── LICENSE ├── README.md ├── example.py ├── mycuinfo ├── __init__.py └── cusession.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | secret.txt 58 | map.txt 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # myCUinfo-API 2 | 3 | A python wrapper for the [myCUinfo System](http://mycuinfo.colorado.edu) at the [University of Colorado Boulder](http://colorado.edu). 4 | 5 | NOTE: This project is not affiliated with the development of the myCUinfo system. This project has no endorsement by the University of Colorado. 6 | 7 | ## Getting Started 8 | 9 | First, install the [Python Requests Library](http://docs.python-requests.org/en/latest/). To install Requests use the following command: 10 | 11 | ```bash 12 | $ pip install requests 13 | ``` 14 | 15 | Then run the code with Python 2.7 or 3.x 16 | 17 | ## Functions and Output 18 | 19 | The API currently has a few functions that scrape the info off the myCUinfo site. They all use methods on an initialized CUSessions logged in user. This way the convoluted login process is only handled once. 20 | 21 | #### CUSession(username, password) [initializer] 22 | 23 | This is the initializer for the myCUinfo API class. It takes in a username & password of a myCUinfo user and returns a class object that is a logged in user. 24 | 25 | ```python 26 | import mycuinfo 27 | 28 | user = "example001" 29 | password = "secret001" 30 | loginUser = mycuinfo.CUSession(user, password) 31 | ``` 32 | 33 | #### CUSession.info() 34 | 35 | This method returns the basic user information. We will format the output with json. 36 | 37 | ```python 38 | userInfo = loginUser.info() 39 | print json.dumps(userInfo, sort_keys=True, indent=4, separators=(',', ':')) 40 | ``` 41 | 42 | ###### Example Output: 43 | 44 | ```python 45 | { 46 | "affiliation": "STUDENT", 47 | "classStanding": "UGRD", 48 | "college": "College Arts and Sciences", 49 | "eid": None, # Will include the Employee ID if the user has one 50 | "firstName": "Chip", 51 | "lastName": "Buffalo", 52 | "major": "Dance", 53 | "minor": None, # Will return a Minor if the user has one 54 | "sid": 000000001 # Student ID 55 | } 56 | ``` 57 | 58 | #### CUSession.classes(term) 59 | 60 | The method returns the class information of the user for the optional given term. We will format the output with json. 61 | 62 | ```python 63 | userClasses = loginUser.classes("Spring 2015") # we can also call loginUser.classes() and it will default to the current semester 64 | print json.dumps(userClasses, sort_keys=True, indent=4, separators=(',', ':')) 65 | ``` 66 | 67 | ###### Example Output: 68 | 69 | ```python 70 | [ 71 | { 72 | "classCode": "1010", # Primary Class Code. Ex: SPRT 1010 73 | "section": "001", # Secondary Class Code. Ex: Section 001 74 | "credits": 4, 75 | "days": "MWF", 76 | "department": "SPRT", 77 | "endTime": "10:50 AM", 78 | "grade": "A", # will not show for current semester 79 | "instructor": "Ralphie Buffalo", 80 | "startTime": "10:00 AM", 81 | "status": "Enrolled", # Will show Waitlisted or Enrolled 82 | "name": "Intro to Spirit (Lecture)" 83 | }, 84 | ..., # Will list all classes but example output has been truncated to two classes 85 | { 86 | "classCode": "1010", # Primary Class Code. Ex: SPRT 1010 87 | "section": "010", # Secondary Class Code. Ex: Section 010 88 | "credits": 0, # shows 0 credits because all credits go to Lecture class 89 | "days": "W", 90 | "department": "SPRT", 91 | "endTime": "03:50 PM", 92 | "grade": "A-", # will not show for current semester 93 | "instructor": "Ralphie Buffalo", 94 | "startTime": "03:00 PM", 95 | "status": "Enrolled", # Will show Waitlisted or Enrolled 96 | "name": "Intro to Spirit (Recitation)" 97 | }, 98 | ] 99 | ``` 100 | 101 | #### CUSession.books(department, courseNumber, section, term) 102 | 103 | The method returns the book information for a given class section for an optional given term. We will format the output with json. 104 | 105 | ```python 106 | department = "SPRT" 107 | courseNumber = "1010" 108 | section = "010" 109 | userBooks = loginUser.books(department, courseNumber, section, term="Spring2015") # term is optional, will default to current semester. 110 | print json.dumps(userBooks, sort_keys=True, indent=4, separators=(',', ':')) 111 | ``` 112 | 113 | ##### Example Output: 114 | 115 | ```python 116 | [ 117 | { 118 | "author": "BRYANT", 119 | "course": "SPRT1010-010", 120 | "isbn": "9780136102041", 121 | "new": 157.0, # price in USD 122 | "newRent": 102.25, 123 | "required": True, 124 | "title": "SCHOOL SPIRIT: A MASCOT'S PERSPECTIVE", 125 | "used": 116.5, 126 | "usedRent": 70.75 127 | } 128 | ] 129 | ``` 130 | 131 | #### CUSession.GPA() 132 | 133 | The method returns the current GPA of the logged in student 134 | 135 | ```python 136 | gpa = loginUser.GPA() 137 | print "The current GPA is " + gpa 138 | ``` 139 | 140 | ##### Example Output: 141 | 142 | ```python 143 | The current GPA is 3.991 144 | ``` 145 | 146 | ## To Do 147 | 148 | - [x] Python 2.7+ & 3.x support 149 | - [ ] Create read-only of class listings 150 | - [ ] Make API do writes 151 | 152 | ## Contribution 153 | 154 | I welcome all kinds of contribution. 155 | 156 | If you have any problem using the myCUinfo-API, please file an issue in Issues. 157 | 158 | If you'd like to contribute on the source, please upload a pull request in Pull Requests. 159 | 160 | ## License 161 | 162 | [MIT](LICENSE) 163 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf8 3 | import getpass 4 | import mycuinfo 5 | 6 | # fix for python 3.x 7 | try: 8 | raw_input = input 9 | except NameError: 10 | pass 11 | 12 | # define the username & password for the cuSession 13 | user0 = raw_input("username: ") 14 | pass0 = getpass.getpass("password: ") 15 | 16 | # create the cuLog Session 17 | cu_student = mycuinfo.CUSession(user0, pass0) 18 | 19 | # if the loggin session is a valid one 20 | if cu_student.valid: 21 | 22 | # example of how to get the books from CSCI 2700, Section 010 (Fall 2014) 23 | print(cu_student.books("CSCI", "2270", "010", term=2167)) 24 | 25 | # example of how to get the info of the user 26 | print(cu_student.info()) 27 | 28 | # example of how to get the classes of the user (Fall 2014) 29 | print(cu_student.classes()) 30 | 31 | # example of how to get the GPA of the user 32 | print(cu_student.GPA()) 33 | 34 | else: 35 | print("Bad user. Check the username/password") 36 | -------------------------------------------------------------------------------- /mycuinfo/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf8 3 | from .cusession import CUSession 4 | -------------------------------------------------------------------------------- /mycuinfo/cusession.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf8 3 | import requests 4 | import json 5 | import html 6 | 7 | 8 | # This script is downloaded when navigating to mycuinfo.colorado.edu. 9 | # It contains a map of a bunch of urls. We can convert it to a dictionary 10 | # And find the one students are interested in with the key iepprdUCB2 to 11 | # hopefully avoid needing to update the url and having the API break all the time 12 | def getURL(session): 13 | page_text = session.get('https://portal.prod.cu.edu/scripts/redirect.js').text 14 | ind0 = page_text.find('{') 15 | ind1 = page_text.find('}') 16 | url_map_string = page_text[ind0:ind1+1] 17 | lines = url_map_string.split('\n') 18 | good_lines = [] 19 | for l in lines: 20 | if len(l)<20 and l.find('{')==-1 and l.find('}')==-1: 21 | continue 22 | good_lines.append(l) 23 | url_map_string = '\n'.join(good_lines) 24 | url_map = json.loads(url_map_string) 25 | return url_map['iepprdUCB2'] 26 | 27 | # Take in some html text and the names of the inputs we need 28 | # Find the post url and the values for those inputs 29 | def parseForm(text, names): 30 | form = text[text.find('')] 31 | act0 = form.find('action="')+len('action="') 32 | act1 = form.find('"', act0+1) 33 | action = form[act0:act1] 34 | data = {} 35 | for n in names: 36 | input = form[form.find(n):] 37 | ind0 = input.find('value="')+len('value="') 38 | ind1 = input.find('"', ind0+1) 39 | data[n] = input[ind0:ind1] 40 | return html.unescape(action), data 41 | 42 | class CUSession(requests.sessions.Session): 43 | 44 | # get past the weird javascript redirects that mycuinfo uses to login 45 | def __init__(self, username, password): 46 | 47 | session = requests.Session() 48 | url = getURL(session) 49 | # get the inital page, found url by disecting js code from 50 | # mycuinfo.colorado.edu 51 | init_page = session.get(url) 52 | init_text = init_page.text 53 | resume_url, resume_data = parseForm(init_text, ['SAMLRequest', 'RelayState']) 54 | resume_page = session.post(resume_url, data=resume_data) 55 | 56 | login_url, login_data = parseForm(resume_page.text, []) 57 | login_data['j_username'] = username 58 | login_data['j_password'] = password 59 | login_data['timezoneOffset'] = '0' 60 | login_data['_eventId_proceed'] = 'Log In' 61 | login_url = 'https://fedauth.colorado.edu' + login_url 62 | login_page1 = session.post(login_url, login_data) 63 | login_url2, login_data2 = parseForm(login_page1.text, ['SAMLResponse', 'RelayState']) 64 | login_page2 = session.post(login_url2, login_data2) 65 | 66 | last_url, last_data = parseForm(login_page2.text, ['SAMLResponse', 'RelayState']) 67 | last_page = session.post(last_url, last_data) 68 | 69 | # if the user/pass was bad, the url will not be correct 70 | if last_page.url != "https://portal.prod.cu.edu/psp/epprod/UCB2/ENTP/h/?tab=DEFAULT": 71 | self.valid = False 72 | else: 73 | self.valid = True 74 | 75 | # from here on, we are a regular user who has all the logged in cookies 76 | # we can do anything that a web user could (javascript not included) 77 | self.session = session 78 | 79 | 80 | # get the basic info of the user (name, student ID, Major, College, etc.) 81 | def info(self): 82 | 83 | # if the user is not logged in, error out, else go for it 84 | if self.valid == False: 85 | return False 86 | 87 | # set the url (break it up so the line isnt huge) 88 | url0 = "https://portal.prod.cu.edu/psp/epprod/UCB2/ENTP/h/?cmd=get" 89 | url1 = "CachedPglt&pageletname=ISCRIPT_CU_PROFILE_V2" 90 | url = url0 + url1 91 | 92 | # get the page 93 | pageLoad = self.session.get(url) 94 | 95 | # get the text (encode it unicode) 96 | pageText = pageLoad.text 97 | 98 | # split the text up a few times till we just have the info 99 | splitText = pageText.split("