PK!xwccls/__init__.pyfrom .wccls import * # pylint: disable=wildcard-import from .bibliocommons import * # pylint: disable=wildcard-import Wccls = WcclsBiblioCommons __all__ = wccls.__all__ + bibliocommons.__all__ # pylint: disable=undefined-variable PK!`6wccls/bibliocommons.pyfrom datetime import datetime from os import makedirs from os.path import join from re import search from tempfile import gettempdir from requests_html import HTMLSession from .wccls import ActiveHold, CheckedOutItem, HeldItem, ShippedItem, SuspendedHold __all__ = ["BiblioCommons", "MultnomahBiblioCommons", "WcclsBiblioCommons"] class BiblioCommons: def __init__(self, subdomain, login, password, debug_=False): self._debug = debug_ self._domain = f"https://{subdomain}.bibliocommons.com" self._session = HTMLSession() self._Login(login, password) self.items = self._CheckedOut() + self._ReadyForPickup() + self._InTransit() + self._NotYetAvailable() + self._Suspended() def _Login(self, login, password): loginPage = self._session.get(f"{self._domain}/user/login") loginForm = loginPage.html.find(".loginForm", first=True) formData = {} for input_ in loginForm.find("input"): formData[input_.attrs["name"]] = input_.attrs["value"] if "value" in input_.attrs else "" formData["user_pin"] = password formData["name"] = login _ = self._session.post(loginForm.attrs["action"], data=formData) def _DumpDebugFile(self, filename, content): if not self._debug: return directory = join(gettempdir(), "log", "wccls") makedirs(directory, exist_ok=True) with open(join(directory, filename), "wb") as theFile: theFile.write(content) def _ParseItems(self, url, dumpfile, parseFunction): result = [] # if there are no items in "ready_for_pickup", for instance, it will redirect back to the holds index, which we don't want page = self._session.get(url, allow_redirects=False) self._DumpDebugFile(dumpfile, page.content) page.raise_for_status() for listItem in page.html.find(".item-row"): result.append(parseFunction(listItem)) return result def _Suspended(self): return self._ParseItems(f"{self._domain}/v2/holds/suspended", "suspended.html", _ParseSuspended) def _NotYetAvailable(self): return self._ParseItems(f"{self._domain}/v2/holds/not_yet_available", "not-yet-available.html", _ParseNotYetAvailable) def _ReadyForPickup(self): return self._ParseItems(f"{self._domain}/v2/holds/ready_for_pickup", "ready-for-pickup.html", _ParseReadyForPickup) def _InTransit(self): return self._ParseItems(f"{self._domain}/v2/holds/in_transit", "in-transit.html", _ParseInTransit) def _CheckedOut(self): return self._ParseItems(f"{self._domain}/v2/checkedout", "checked-out.html", _ParseCheckedOut) class WcclsBiblioCommons(BiblioCommons): def __init__(self, login, password, debug_=False): super().__init__(subdomain="wccls", login=login, password=password, debug_=debug_) class MultnomahBiblioCommons(BiblioCommons): def __init__(self, login, password, debug_=False): super().__init__(subdomain="multcolib", login=login, password=password, debug_=debug_) def _ParseTitle(listItem): title = listItem.find(".title-content", first=True).text subtitleElement = listItem.find(".cp-subtitle", first=True) if subtitleElement is not None: title += ": " + subtitleElement.text return title def _ParseSuspended(listItem): return SuspendedHold( title=_ParseTitle(listItem), reactivationDate=_ParseDate2(listItem)) def _ParseNotYetAvailable(listItem): holdInfo = _ParseHoldPosition(listItem) return ActiveHold( title=_ParseTitle(listItem), activationDate=_ParseDate2(listItem), # TODO - this isn't an activation date anymore - it's the expiry date queuePosition=holdInfo[0], queueSize=None, # Not shown on the initial screen anymore copies=holdInfo[1]) def _ParseReadyForPickup(listItem): return HeldItem( title=_ParseTitle(listItem), expiryDate=_ParseDate(listItem.find(".cp-short-formatted-date", first=True))) def _ParseInTransit(listItem): return ShippedItem( title=_ParseTitle(listItem), shippedDate=None) # they don't seem to show this anymore def _ParseCheckedOut(listItem): renewals = 1 # we don't know how many renewals are really left - this just means at least one if listItem.find(".cp-held-copies-count", first=True) is not None: renewals = 0 renewCountText = listItem.find(".cp-renew-count span:nth-of-type(2)", first=True) if renewCountText is not None: renewals = 4 - int(renewCountText.text[0]) isOverdrive = False if listItem.find(".cp-checked-out-reading-links", first=True) is not None: isOverdrive = True return CheckedOutItem( title=_ParseTitle(listItem), dueDate=_ParseDate(listItem.find(".cp-short-formatted-date", first=True)), renewals=renewals, # really should be a "renewable" flag isOverdrive=isOverdrive) def _ParseDate(element): return datetime.strptime(element.text, "%b %d, %Y").date() def _ParseDate2(listItem): dateAttr = listItem.find(".cp-short-formatted-date", first=True) if dateAttr is None: return None # text value here seems to have been run through some javascript return datetime.strptime(dateAttr.text, "%b %d, %Y").date() def _ParseHoldPosition(listItem): text = listItem.find(".cp-hold-position", first=True).text match = search(r"\#(\d+) on (\d+) cop", text) return (match.group(1), match.group(2)) PK!?N N wccls/wccls.pyfrom enum import Enum __all__ = ["ParseError", "StatusType", "Item", "CheckedOutItem", "SuspendedHold", "HeldItem", "ShippedItem", "ActiveHold", "CancelledHold", "PendingItem", "UnclaimedHold"] class ParseError(Exception): pass # status types StatusType = Enum("StatusType", "Held, Pending, Shipped, Active, Inactive, Cancelled, CheckedOut, Unclaimed") class Item: def __init__(self, title, status): self.title = title self.status = status def __repr__(self): return f"" class CheckedOutItem(Item): def __init__(self, title, dueDate, renewals, isOverdrive): super().__init__(title, StatusType.CheckedOut) self.dueDate = dueDate self.renewals = renewals self.isOverdrive = isOverdrive @property def renewable(self): return not self.isOverdrive and self.renewals > 0 def __repr__(self): return f"" # The same as a paused hold class SuspendedHold(Item): def __init__(self, title, reactivationDate): super().__init__(title, StatusType.Inactive) self.reactivationDate = reactivationDate def __repr__(self): return f"" # Being held at the library class HeldItem(Item): def __init__(self, title, expiryDate): super().__init__(title, StatusType.Held) self.expiryDate = expiryDate def __repr__(self): return f"" # Shipping to the library class ShippedItem(Item): def __init__(self, title, shippedDate): super().__init__(title, StatusType.Shipped) self.shippedDate = shippedDate def __repr__(self): return f"" # In the queue class ActiveHold(Item): def __init__(self, title, activationDate, queuePosition, queueSize, copies): super().__init__(title, StatusType.Active) self.activationDate = activationDate self.queuePosition = queuePosition self.queueSize = queueSize self.copies = copies def __repr__(self): return f"" # Cancelled - not sure if this shows up anymore class CancelledHold(Item): def __init__(self, title, cancellationDate): super().__init__(title, StatusType.Cancelled) self.cancellationDate = cancellationDate def __repr__(self): return f"" # This is the state after it's been requested but before it's an ActiveHold class PendingItem(Item): def __init__(self, title, reservationDate): super().__init__(title, StatusType.Pending) self.reservationDate = reservationDate def __repr__(self): return f"" # The hold expired - not sure if this shows up anymore class UnclaimedHold(Item): def __init__(self, title): super().__init__(title, StatusType.Unclaimed) PK!&ɪ77wccls-1.3.3.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2018 Rehan Khwaja Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUwccls-1.3.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hwccls-1.3.3.dist-info/METADATARMo0 W,؆s 9,X&[ϊZ-ɥtEvY'0-G= YUU) x'ZY,`uğXNDzk XiRx$GC1ǠtɴlE]Ϝ?gX-N#֍ ;$.BJ06mJ1 6S}U2l/RN7UF~SCG+6׏mwLBpރƕZQu E?| F  4X<ǃt57PK!H Xwccls-1.3.3.dist-info/RECORDur0}%i,ƢrT6 P(K( Oߎo9$$yGTTgW,zoWb,