Guest User

Untitled

a guest
Nov 26th, 2017
96
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.32 KB | None | 0 0
  1. import requests
  2. from bs4 import BeautifulSoup, SoupStrainer
  3.  
  4.  
  5. class BrowserError(Exception):
  6. pass
  7.  
  8.  
  9. class ParsingError(BrowserError):
  10. pass
  11.  
  12.  
  13. class NoWebsiteLoadedError(BrowserError):
  14. pass
  15.  
  16.  
  17. class SimpleBrowser:
  18. """Low-level HTTP browser to simplify interacting with websites.
  19.  
  20. Attributes:
  21. parser: Used in website parsing, defaults to `lxml`.
  22. session: A reusable TCP connection, useful for making requests to the
  23. same website and managing cookies.
  24. <http://docs.python-requests.org/en/master/user/advanced/#session-objects>
  25. url: Full URL of currently loaded website.
  26. response: Response of currently loaded website.
  27. """
  28. def __init__(self, parser='lxml'):
  29. self.parser = parser
  30. self.session = requests.Session()
  31. self._url = None
  32. self._response = None
  33.  
  34. @property
  35. def url(self):
  36. """Return the URL of currently loaded website."""
  37. return self._url
  38.  
  39. @property
  40. def response(self):
  41. """Return the `Response` object of currently loaded website."""
  42. return self._response
  43.  
  44. @property
  45. def cookies(self):
  46. """Return the CookieJar instance of the current `Session`."""
  47. return self.session.cookies
  48.  
  49. def soup(self, *args, **kwargs):
  50. """Parse the currently loaded website.
  51.  
  52. Optionally, SoupStrainer can be used to only parse relevant
  53. parts of the page. This can be particularly useful if the website is
  54. complex or perfomance is a factor.
  55. <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#soupstrainer>
  56.  
  57. Args:
  58. *args: Optional positional arguments that `SoupStrainer` takes.
  59. **kwargs: Optional keyword argument that `SoupStrainer` takes.
  60.  
  61. Returns:
  62. A `BeautifulSoup` object.
  63.  
  64. Raises:
  65. NoWebsiteLoadedError: If no website is currently loaded.
  66. ParsingError: If the current response isn't supported by `bs4`
  67. """
  68. if self._url is None:
  69. raise NoWebsiteLoadedError('website parsing requires a loaded website')
  70.  
  71. content_type = self._response.headers.get('Content-Type', '')
  72. if not any(markup in content_type for markup in ('html', 'xml')):
  73. raise ParsingError('unsupported content type '{}''.format(content_type))
  74.  
  75. strainer = SoupStrainer(*args, **kwargs)
  76. return BeautifulSoup(self._response.content, self.parser, parse_only=strainer)
  77.  
  78. def get(self, url, **kwargs):
  79. """Send a GET request to the specified URL.
  80.  
  81. Method directly wraps around `Session.get` and updates browser
  82. attributes.
  83. <http://docs.python-requests.org/en/master/api/#requests.get>
  84.  
  85. Args:
  86. url: URL for the new `Request` object.
  87. **kwargs: Optional arguments that `Request` takes.
  88.  
  89. Returns:
  90. `Response` object of a successful request.
  91. """
  92. response = self.session.get(url, **kwargs)
  93. self._url = response.url
  94. self._response = response
  95. return response
  96.  
  97. def post(self, **kwargs):
  98. """Send a POST request to the currently loaded website's URL.
  99.  
  100. The browser will automatically fill out the form. If `data` dict has
  101. been passed into ``kwargs``, the contained input values will override
  102. the automatically filled out values.
  103.  
  104. Returns:
  105. `Response` object of a successful request.
  106.  
  107. Raises:
  108. NoWebsiteLoadedError: If no website is currently loaded.
  109. """
  110. if self._url is None:
  111. raise NoWebsiteLoadedError('request submission requires a loaded website')
  112.  
  113. data = kwargs.get('data', {})
  114. for i in self.soup('form').select('input[name]'):
  115. if i.get('name') not in data:
  116. data[i.get('name')] = i.get('value', '')
  117. kwargs['data'] = data
  118.  
  119. response = self.session.post(self._url, **kwargs)
  120. self._url = response.url
  121. self._response = response
  122. return response
  123.  
  124. import re
  125.  
  126. from browser import SimpleBrowser
  127.  
  128.  
  129. class LeapError(Exception):
  130. pass
  131.  
  132.  
  133. class LoginError(LeapError):
  134. pass
  135.  
  136.  
  137. class Leap:
  138. """Interface class for automated access to the Leapcard website.
  139.  
  140. Attributes:
  141. browser: An instance of `SimpleBrowser`
  142. """
  143.  
  144. BASE_URL = 'https://www.leapcard.ie/en/'
  145. LOGIN_URL = BASE_URL + 'login.aspx'
  146. TABLE_URL = BASE_URL + 'SelfServices/CardServices/ViewJourneyHistory.aspx'
  147.  
  148. def __init__(self):
  149. self.browser = SimpleBrowser()
  150.  
  151. @property
  152. def login_cookie(self):
  153. """Return True if user authentication is successful."""
  154. return any('ASPXFORMSAUTH' in c.name for c in self.browser.cookies)
  155.  
  156. def login(self, username, password):
  157. """Authenticate a user account to access user information.
  158.  
  159. Args:
  160. username: Leapcard.ie account username
  161. password: Leapcard.ie account password
  162.  
  163. Raises:
  164. LoginError: If user authentication fails.
  165. """
  166. self.browser.get(self.LOGIN_URL)
  167. data = {
  168. 'ctl00$ContentPlaceHolder1$UserName': username,
  169. 'ctl00$ContentPlaceHolder1$Password': password,
  170. 'ctl00$ContentPlaceHolder1$btnlogin': 'Login'
  171. }
  172. self.browser.post(data=data)
  173. if self.login_cookie is False:
  174. raise LoginError('user login failure')
  175.  
  176. def select_card(self, card_number):
  177. """Select the requested card number from the dropdown menu.
  178.  
  179. In case of an account with multiple cards registered, this method
  180. will ensure that the correct card has been selected.
  181.  
  182. Args:
  183. card_number: Unique Leap card number
  184.  
  185. Raises:
  186. LeapError: If requested card is not registered in user account.
  187. """
  188. cards = self.browser.soup().select_one('select[id*=CardsList]')
  189. registered_cards = {c.text.split()[0]: c.get('value') for c in cards.select('option[value]')}
  190. if card_number not in registered_cards:
  191. raise LeapError('requested card not registered: {}'.format(card_number))
  192. data = {cards.get('name'): registered_cards.get(card_number)}
  193. self.browser.post(data=data)
  194.  
  195. @property
  196. def balance(self):
  197. """Fetch dictionary with last known travel credit balance.
  198.  
  199. Returns:
  200. A dictionary containing date and time of the last transaction
  201. made with a Leap card and the balance after the transaction.
  202. """
  203. self.browser.get(self.TABLE_URL)
  204. table = self.browser.soup().select_one('table[id*=CardJourney]')
  205. date = table.find_next(text=re.compile(r'd{2}/d{2}/d{4}'))
  206. time = table.find_next(text=re.compile(r'd{1,2}:d{2} wM'))
  207. balance = table.find_next(text=re.compile(r'€-?d{1,3}.d{2}')).next_element.text.strip('€')
  208. return {'date': date, 'time': time, 'balance': balance}
  209.  
  210. <table class="table" cellspacing="0" cellpadding="3" rules="all" align="left" rules="none" id="gvCardJourney" style="border-width:1px;border-style:solid;width:100%;border-collapse:collapse;">
  211. <caption>
  212. Travel Credit History Information
  213. </caption><tr class="grid-header" align="left" style="color:White;background-color:#008033;">
  214. <th scope="col" abbr="Date">Date</th><th scope="col" abbr="Time">Time</th><th scope="col" abbr="ParticipantShortNameDescription">Source</th><th scope="col" abbr="TransactionTypeDescription">Transaction Type</th><th scope="col" abbr="TransactionAmountEuro">Amount</th><th scope="col" abbr="PurseAmountEuro">Balance</th>
  215. </tr><tr style="background-color:#EDEDED;">
  216. <td align="center">24/11/2017</td><td align="center" style="white-space:nowrap;">12:41 PM</td><td align="center">Luas</td><td align="center">Travel Credit Returned</td><td align="center">€2.13</td><td align="center">€6.49</td>
  217. </tr><tr style="background-color:#F2F1F1;">
  218. <td align="center">24/11/2017</td><td align="center" style="white-space:nowrap;">12:31 PM</td><td align="center">Luas</td><td align="center">Travel Credit Deduction</td><td align="center">€-2.13</td><td align="center">€4.36</td>
  219. </tr>
Add Comment
Please, Sign In to add comment