Source code for penn.studyspaces

import requests
import datetime
import json
import pytz
import re
import six

from bs4 import BeautifulSoup


BASE_URL = "https://libcal.library.upenn.edu"


[docs]class StudySpaces(object): """Used for interacting with the UPenn library GSR booking system. Usage:: >>> from penn import StudySpaces >>> s = StudySpaces() """ def __init__(self): pass
[docs] 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 if int(opt["value"]) > 0]
[docs] def book_room(self, building, room, start, end, firstname, lastname, email, groupname, phone, size, fake=False): """Books a room given the required information. :param building: The ID of the building the room is in. :type building: int :param room: The ID of the room to book. :type room: int :param start: The start time range of when to book the room. :type start: datetime :param end: The end time range of when to book the room. :type end: datetime :param fake: If this is set to true, don't actually book the room. Default is false. :type fake: bool :returns: Boolean indicating whether the booking succeeded or not. :raises ValueError: If one of the fields is missing or incorrectly formatted, or if the server fails to book the room. """ data = { "formData[fname]": firstname, "formData[lname]": lastname, "formData[email]": email, "formData[nick]": groupname, "formData[q2533]": phone, "formData[q2555]": size, "forcedEmail": "" } try: room_obj = self.get_room_id_name_mapping(building)[room] except KeyError: raise ValueError("No room with id {} found in building with id {}!".format(room, building)) if room_obj["lid"] != building: raise ValueError("Mismatch between building IDs! (expected {}, got {})".format(building, room_obj["lid"])) room_data = { "id": 1, "eid": room, "gid": room_obj["gid"], "lid": room_obj["lid"], "start": start.strftime("%Y-%m-%d %H:%M"), "end": end.strftime("%Y-%m-%d %H:%M") } for key, val in room_data.items(): data["bookings[0][{}]".format(key)] = val if fake: return True resp = requests.post("{}/ajax/space/book".format(BASE_URL), data) resp_data = resp.json() if resp_data.get("success"): return True else: if "error" in resp_data: raise ValueError(resp_data["error"]) else: raise ValueError(re.sub('<.*?>', '', resp_data.get("msg").strip()).strip())
[docs] @staticmethod def parse_date(date): """Converts library system dates into timezone aware Python datetime objects. :param date: A library system date in the format '2018-01-25 12:30:00'. :type date: datetime :returns: A timezone aware python datetime object. """ date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S") return pytz.timezone("US/Eastern").localize(date)
[docs] @staticmethod def get_room_id_name_mapping(building): """Returns a dictionary mapping id to name, thumbnail, and capacity. The dictionary also contains information about the lid and gid, which are used in the booking process. :param building: The ID of the building to fetch rooms for. :type building: int :returns: A list of rooms, with each item being a dictionary that contains the room id and available times. """ 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"]) room_gid = int(items["gid"]) room_lid = int(items["lid"]) out[room_id] = { "name": title, "gid": room_gid, "lid": room_lid, "thumbnail": thumbnail or None, "capacity": int(items["capacity"]) } return out
[docs] def get_rooms(self, building, start, end): """Returns a dictionary matching all rooms given a building id and a date range. The resulting dictionary contains both rooms that are available and rooms that already have been booked. :param building: The ID of the building to fetch rooms for. :type building: int :param start: The start date of the range used to filter available rooms. :type start: datetime :param end: The end date of the range used to filter available rooms. :type end: datetime """ 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