diff --git a/.gitignore b/.gitignore index 43fa68f..7012b83 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ MANIFEST docs/_build .pypirc .idea/ +*.swp +*.swo diff --git a/.travis.yml b/.travis.yml index 279d0a9..9273425 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,11 @@ python: - "2.7" - "3.4" - "3.5" -script: nosetests +install: + - pip install flake8 -r requirements.txt +script: + - flake8 --max-line-length=150 --exclude=venv,docs,build --ignore=F401,F403,F405 . + - nosetests deploy: provider: pypi user: 'pennappslabs' diff --git a/penn/__init__.py b/penn/__init__.py index 5c5f399..0ee73ee 100644 --- a/penn/__init__.py +++ b/penn/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.6.6' +__version__ = '1.6.7' from .registrar import Registrar from .directory import Directory diff --git a/penn/calendar3year.py b/penn/calendar3year.py index d1e9b52..c8d7fe2 100644 --- a/penn/calendar3year.py +++ b/penn/calendar3year.py @@ -21,9 +21,9 @@ def pull_3year(self): """ events = [] r = requests.get(BASE_URL).text - l = r.split("\r\n") + lines = r.split("\r\n") d = {} - for line in l: + for line in lines: if line == "BEGIN:VEVENT": d = {} elif line[:7] == "DTSTART": @@ -32,7 +32,7 @@ def pull_3year(self): d['start'] = start_date.strftime('%Y-%m-%d') elif line[:5] == "DTEND": raw_date = line.split(":")[1] - end_date = datetime.datetime.strptime(raw_date,'%Y%m%d').date() + end_date = datetime.datetime.strptime(raw_date, '%Y%m%d').date() d['end'] = end_date.strftime('%Y-%m-%d') elif line[:7] == "SUMMARY": name = line.split(":")[1] diff --git a/penn/dining.py b/penn/dining.py index 6c98285..e918502 100644 --- a/penn/dining.py +++ b/penn/dining.py @@ -81,6 +81,7 @@ def get_meals(v2_response, building_id): meals.append({"tblStation": stations, "txtDayPartDescription": meal["label"]}) return meals + class DiningV2(WrapperBase): """The client for the Registrar. Used to make requests to the API. @@ -141,6 +142,7 @@ def item(self, item_id): response = self._request(V2_ENDPOINTS['ITEMS'] + item_id) return response + class Dining(WrapperBase): """The client for the Registrar. Used to make requests to the API. diff --git a/penn/directory.py b/penn/directory.py index 11bbfc5..bee4ea9 100644 --- a/penn/directory.py +++ b/penn/directory.py @@ -38,7 +38,6 @@ def standardize(res): res['list_affiliation'] = res['list_affiliation'].replace('Faculty - ', '') return res - def search(self, params, standardize=False): """Get a list of person objects for the given search params. diff --git a/penn/studyspaces.py b/penn/studyspaces.py index d63361a..2475400 100644 --- a/penn/studyspaces.py +++ b/penn/studyspaces.py @@ -1,93 +1,109 @@ -from bs4 import BeautifulSoup import requests +import datetime +import json +import pytz +import re +import six + +from bs4 import BeautifulSoup -BASE_URL = "http://libcal.library.upenn.edu" +BASE_URL = "https://libcal.library.upenn.edu" class StudySpaces(object): + """Used for interacting with the UPenn library GSR booking system. + + Usage:: + + >>> from penn import StudySpaces + >>> s = StudySpaces() + """ + def __init__(self): pass + def get_buildings(self): + """Returns a list of building IDs, building names, and services.""" + + soup = BeautifulSoup(requests.get("{}/spaces".format(BASE_URL)).content, "html5lib") + options = soup.find("select", {"id": "lid"}).find_all("option") + return [{"id": int(opt["value"]), "name": str(opt.text), "service": "libcal"} for opt in options] + + @staticmethod + def parse_date(date): + """Converts library system dates into timezone aware Python datetime objects.""" + + date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + return pytz.timezone("US/Eastern").localize(date) + @staticmethod - def date_parse(original): - """Parses the date to dashed format. - - :param original: string with date in the format MM/DD/YYYY. - """ - l = original.split("-") - final = [l[1], l[2], l[0]] - return '-'.join(final) - - def get_id_json(self): - """Makes JSON with each element associating URL, ID, and building - name. - """ - group_study_codes = [] - url = BASE_URL + "/booking/vpdlc" - soup = BeautifulSoup(requests.get(url).text, 'html5lib') - l = soup.find('select', {'id': 'lid'}).find_all('option') - for element in l: - if element['value'] != '0': - url2 = "{}/spaces?lid={}".format(BASE_URL, str(element['value'])) - new_dict = {} - new_dict['id'] = int(str(element['value'])) - new_dict['name'] = str(element.contents[0]) - new_dict['url'] = url2 - group_study_codes.append(new_dict) - return group_study_codes - - def get_id_dict(self): - """Extracts the ID's of the room into a dictionary. Used as a - helper for the extract_times method. - """ - group_study_codes = {} - url = BASE_URL + "/booking/vpdlc" - soup = BeautifulSoup(requests.get(url).text, 'html5lib') - options = soup.find('select', {'id': 'lid'}).find_all('option') - for element in options: - if element['value'] != '0': - group_study_codes[int(str(element['value']))] = str(element.contents[0]) - return group_study_codes - - def extract_times(self, id, date, name): - """Scrapes the avaiable rooms with the given ID and date. - - :param id: the ID of the building - :param date: the date to acquire available rooms from - :param name: the name of the building; obtained via get_id_dict - """ - url = BASE_URL + "/rooms_acc.php?gid=%s&d=%s&cap=0" % (int(id), date) - soup = BeautifulSoup(requests.get(url).text, 'html5lib') - - time_slots = soup.find_all('form') - unparsed_rooms = time_slots[1].contents[2:-2] - - roomTimes = [] - - for i in unparsed_rooms: - room = BeautifulSoup(str(i), 'html5lib') - try: - # extract room names - roomName = room.fieldset.legend.h2.contents[0] - except AttributeError: - # in case the contents aren't a list - continue - newRoom = str(roomName)[:-1] - times = [] - - filtered = room.fieldset.find_all('label') - - for t in filtered: - # getting the individual times for each room - dict_item = {} - dict_item['room_name'] = newRoom - time = str(t).split("\t\t\t\t\t")[2][1:-1] - times.append(time) - startAndEnd = time.split(" - ") - dict_item['start_time'] = startAndEnd[0].upper() - dict_item['end_time'] = startAndEnd[1].upper() - roomTimes.append(dict_item) - dict_item['date'] = self.date_parse(date) - dict_item['building'] = name - return roomTimes + def get_room_id_name_mapping(building): + """ Returns a dictionary mapping id to name, thumbnail, and capacity. """ + + data = requests.get("{}/spaces?lid={}".format(BASE_URL, building)).content.decode("utf8") + # find all of the javascript room definitions + out = {} + for item in re.findall(r"resources.push\(((?s).*?)\);", data, re.MULTILINE): + # parse all of the room attributes + items = {k: v for k, v in re.findall(r'(\w+?):\s*(.*?),', item)} + + # room name formatting + title = items["title"][1:-1] + title = title.encode().decode("unicode_escape" if six.PY3 else "string_escape") + title = re.sub(r" \(Capacity [0-9]+\)", r"", title) + + # turn thumbnail into proper url + thumbnail = items["thumbnail"][1:-1] + if thumbnail: + thumbnail = "https:" + thumbnail + + room_id = int(items["eid"]) + out[room_id] = { + "name": title, + "thumbnail": thumbnail or None, + "capacity": int(items["capacity"]) + } + return out + + def get_rooms(self, building, start, end): + """Returns a dictionary matching all rooms given a building id and a date range.""" + + if start.tzinfo is None: + start = pytz.timezone("US/Eastern").localize(start) + if end.tzinfo is None: + end = pytz.timezone("US/Eastern").localize(end) + + mapping = self.get_room_id_name_mapping(building) + room_endpoint = "{}/process_equip_p_availability.php".format(BASE_URL) + data = { + "lid": building, + "gid": 0, + "start": start.strftime("%Y-%m-%d"), + "end": (end + datetime.timedelta(days=1)).strftime("%Y-%m-%d"), + "bookings": [] + } + resp = requests.post(room_endpoint, data=json.dumps(data), headers={'Referer': "{}/spaces?lid={}".format(BASE_URL, building)}) + rooms = {} + for row in resp.json(): + room_id = int(row["resourceId"][4:]) + if room_id not in rooms: + rooms[room_id] = [] + room_start = self.parse_date(row["start"]) + room_end = self.parse_date(row["end"]) + if start <= room_start <= end: + rooms[room_id].append({ + "start": room_start.isoformat(), + "end": room_end.isoformat(), + "available": row["status"] == 0 + }) + out = [] + for k, v in rooms.items(): + item = { + "room_id": k, + "times": v + } + if k in mapping: + item.update(mapping[k]) + out.append(item) + return out diff --git a/requirements.txt b/requirements.txt index 30f7ee3..cb7d83d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -nose==1.3.4 -requests==2.4.3 -beautifulsoup4==4.4.1 +beautifulsoup4==4.6.0 html5lib==0.999 mock==2.0.0 nameparser==0.4.0 +nose==1.3.7 +pytz==2017.3 +requests==2.4.3 +six==1.11.0 diff --git a/setup.py b/setup.py index cc9a657..dc3b58b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ url='https://github.com/pennlabs/penn-sdk-python', author='Penn Labs', author_email='admin@pennlabs.org', - version='1.6.6', + version='1.6.7', packages=['penn'], license='MIT', package_data={ diff --git a/tests/calendar_test.py b/tests/calendar_test.py index 175d060..267ad88 100644 --- a/tests/calendar_test.py +++ b/tests/calendar_test.py @@ -9,15 +9,15 @@ def setUp(self): self.calendar = Calendar() def test_pull(self): - l = self.calendar.pull_3year() - ok_(len(l) > 0) - for event in l: + cal = self.calendar.pull_3year() + ok_(len(cal) > 0) + for event in cal: ok_(len(event) == 3) def test_date(self): - l = self.calendar.pull_3year() - ok_(len(l) > 0) - for event in l: + cal = self.calendar.pull_3year() + ok_(len(cal) > 0) + for event in cal: ok_(isinstance(event['name'], str)) if event['name'] == "Independence Day Observed (no classes)": independence = event['start'] @@ -25,18 +25,18 @@ def test_date(self): ok_(d.month == 7) def test_name(self): - l = self.calendar.pull_3year() - ok_(len(l) > 0) - for event in l: + cal = self.calendar.pull_3year() + ok_(len(cal) > 0) + for event in cal: ok_(isinstance(event['name'], str)) start = datetime.datetime.strptime(event['start'], '%Y-%m-%d').date() end = datetime.datetime.strptime(event['end'], '%Y-%m-%d').date() ok_((end - start).total_seconds() >= 0) def test_chrono(self): - l = self.calendar.pull_3year() - ok_(len(l) > 0) - for i, event in enumerate(l[:-1]): + cal = self.calendar.pull_3year() + ok_(len(cal) > 0) + for i, event in enumerate(cal[:-1]): start = datetime.datetime.strptime(event['start'], '%Y-%m-%d').date() - nextstart = datetime.datetime.strptime(l[i]['start'], '%Y-%m-%d').date() + nextstart = datetime.datetime.strptime(cal[i]['start'], '%Y-%m-%d').date() ok_((nextstart - start).total_seconds() >= 0) diff --git a/tests/dining_test.py b/tests/dining_test.py index 7754df5..44500bc 100644 --- a/tests/dining_test.py +++ b/tests/dining_test.py @@ -18,8 +18,7 @@ def test_dining(self): self.assertTrue(len(venues) > 0) id = str(venues["id"]) data = self.din.menu_daily(id) - self.assertTrue( - len(data["result_data"]["Document"]["tblMenu"]["tblDayPart"][0]) >= 2) + self.assertTrue(len(data["result_data"]["Document"]["tblMenu"]["tblDayPart"][0]) >= 2) def test_dining_normalization(self): data = self.din.menu_daily("593") diff --git a/tests/studyspaces_test.py b/tests/studyspaces_test.py index 983562e..b0b35d4 100644 --- a/tests/studyspaces_test.py +++ b/tests/studyspaces_test.py @@ -1,6 +1,8 @@ +import datetime +import pytz + from nose.tools import ok_ from penn import StudySpaces -import datetime class TestStudySpaces(): @@ -8,23 +10,15 @@ class TestStudySpaces(): def setUp(self): self.studyspaces = StudySpaces() - def test_json(self): - json_id = self.studyspaces.get_id_json() - ok_(len(json_id) > 0) - for i in json_id: - ok_(i['id'] > 0) - ok_(i['name'] != '') - ok_(i['url'] != '') + def test_buildings(self): + buildings = self.studyspaces.get_buildings() + ok_(len(buildings) > 0) + + def test_room_name_mapping(self): + mapping = self.studyspaces.get_room_id_name_mapping(2683) + ok_(len(mapping) > 0) - def test_extraction(self): - dict_id = self.studyspaces.get_id_dict() - ok_(len(dict_id) > 0) - d = datetime.datetime.now() + datetime.timedelta(days=1) - next_date = d.strftime("%Y-%m-%d") - s = self.studyspaces.extract_times(1799, next_date, "Van Pelt-Dietrich Library Center Group Study Rooms") - for i in s: - ok_("building" in i) - ok_("start_time" in i) - ok_("end_time" in i) - ok_("date" in i) - ok_("room_name" in i) + def test_rooms(self): + now = pytz.timezone("US/Eastern").localize(datetime.datetime.now()) + rooms = self.studyspaces.get_rooms(2683, now, now + datetime.timedelta(days=3)) + ok_(len(rooms) > 0)