You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

209 lines
6.3 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. #!/usr/bin/python3
  2. from dateutil.parser import parse as date_parse
  3. from dateutil import tz
  4. from disk_cache import disk_cache
  5. import argparse
  6. import ctabus
  7. import datetime
  8. import os
  9. import socket
  10. import re
  11. import time
  12. import urllib
  13. # for logging
  14. import os.path as osp
  15. import sys
  16. CHICAGO_TZ = tz.gettz("America/Chicago")
  17. DATETIME_FORMAT = "%A, %B %e, %Y %H:%M:%S"
  18. # https://stackoverflow.com/a/5967539
  19. def atoi(text):
  20. return int(text) if text.isdigit() else text
  21. def numb_sort(text):
  22. '''
  23. alist.sort(key=natural_keys) sorts in human order
  24. http://nedbatchelder.com/blog/200712/human_sorting.html
  25. (See Toothy's implementation in the comments)
  26. '''
  27. return [atoi(c) for c in re.split(r'(\d+)', text)]
  28. def clearscr():
  29. os.system('cls' if os.name == 'nt' else 'clear')
  30. def pprint_delta(delta):
  31. delta = str(delta)
  32. days = None
  33. s1 = delta.split(', ')
  34. if len(s1) > 1:
  35. days, time = s1
  36. else:
  37. time = s1[0]
  38. time = time.split('.')[0]
  39. hour, minute, second = map(int, time.split(':'))
  40. time = ''
  41. if hour:
  42. time += '{hour} hour'.format(hour=hour) + ('s' if hour != 1 else '')
  43. if minute:
  44. if time and not time.endswith(', '):
  45. time += ', '
  46. time += '{minute} minute'.format(minute=minute) + \
  47. ('s' if minute != 1 else '')
  48. if second:
  49. if time and not time.endswith(', '):
  50. time += ', '
  51. time += '{second} second'.format(second=second) + \
  52. ('s' if second != 1 else '')
  53. ret = ''
  54. if days:
  55. ret = days + ', ' if time else ''
  56. ret += time
  57. return ret
  58. def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True):
  59. from print2d import create_table, render_table
  60. k = displays[sort]
  61. display_data = {obj[k]: obj[data] for obj in objs}
  62. srt_keys = sorted(display_data.keys(), key=key)
  63. display = sorted(
  64. [
  65. [obj[d] for d in displays] for obj in objs
  66. ],
  67. key=lambda row: key(row[sort]) if key else row[sort]
  68. )
  69. if num_pic:
  70. display = [[i] + data for i, data in enumerate(display)]
  71. table = create_table(display, DATETIME_FORMAT)
  72. render_table(table)
  73. if num_pic:
  74. which = None
  75. while not which:
  76. try:
  77. which = input('Which one?: ')
  78. except KeyboardInterrupt:
  79. quit()
  80. try:
  81. which = srt_keys[int(which)]
  82. except (ValueError, IndexError):
  83. which = None
  84. return display_data[which]
  85. else:
  86. ret = None
  87. while not ret:
  88. try:
  89. ret = display_data[input('Which one?: ')]
  90. except KeyError:
  91. pass
  92. return ret
  93. config = '''\
  94. {route} - {end} ({direction})
  95. {nm}, stop {stop_id}
  96. {delta} ({t})\
  97. '''
  98. def show(data, rt_filter=None, _clear=False):
  99. times = data['prd']
  100. today = datetime.datetime.now(CHICAGO_TZ)
  101. arrivals = sorted(times, key=lambda t: t['prdtm'])
  102. if rt_filter is not None:
  103. arrivals = filter(lambda arrival: arrival['rt'] == rt_filter, arrivals)
  104. if _clear:
  105. clearscr()
  106. for bustime in arrivals:
  107. before = date_parse(bustime['prdtm'])
  108. arrival = before.replace(tzinfo=CHICAGO_TZ)
  109. if arrival > today:
  110. stop_id = bustime['stpid']
  111. delta = pprint_delta(arrival-today)
  112. t = arrival.strftime('%H:%M:%S')
  113. route = bustime['rt']
  114. direction = bustime['rtdir']
  115. end = bustime['des']
  116. nm = bustime['stpnm'].rstrip()
  117. print(
  118. config.format(**locals()), end='\n'*2
  119. )
  120. print("="*36)
  121. def main(args):
  122. args.arg = ' '.join(args.arg)
  123. if not args.arg.isdecimal():
  124. # save on import time slightly
  125. from search import Search, StopSearch
  126. # routes
  127. if not args.route:
  128. data = ctabus.get_routes()['routes']
  129. route = gen_list(data, 'rt', 'rt', 'rtnm',
  130. num_pic=False, key=numb_sort)
  131. else:
  132. route = args.route
  133. data = ctabus.get_directions(route)['directions']
  134. # direction
  135. if not args.direction:
  136. direction = gen_list(data, 'dir', 'dir')
  137. else:
  138. s = Search(args.direction)
  139. direction = sorted((obj['dir'] for obj in data), key=s)[0]
  140. # direction
  141. stops = ctabus.get_stops(route, direction)['stops']
  142. s = StopSearch(args.arg)
  143. if args.lucky:
  144. stop_id = sorted(stops, key=lambda stop: s(stop['stpnm']))[
  145. 0]['stpid']
  146. else:
  147. stop_id = gen_list(stops, 'stpid', 'stpnm', key=s)
  148. else:
  149. stop_id = args.arg
  150. data = ctabus.get_times(stop_id)
  151. if args.periodic is not None:
  152. _done = False
  153. while not _done:
  154. try:
  155. show(data, args.route, True)
  156. s = time.perf_counter()
  157. timeout = 1
  158. if args.periodic > timeout:
  159. timeout = args.periodic
  160. data = ctabus.get_times(stop_id, timeout=timeout)
  161. e = time.perf_counter() - s
  162. if e < args.periodic:
  163. time.sleep(args.periodic-e)
  164. except KeyboardInterrupt:
  165. _done = True
  166. except (urllib.error.URLError, socket.timeout):
  167. print("Error fetching times")
  168. else:
  169. show(data, args.route)
  170. if __name__ == '__main__':
  171. parser = argparse.ArgumentParser(prog='ctabus')
  172. parser.add_argument('-l', '--lucky', action='store_true',
  173. help='picks first result')
  174. parser.add_argument('-p', '--periodic', metavar='SEC',
  175. type=int, help='checks periodically')
  176. parser.add_argument('-r', '--route', default=None)
  177. parser.add_argument('-d', '--direction', default=None)
  178. parser.add_argument('-k', '--kill-cache', action="store_true")
  179. parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)')
  180. args = parser.parse_args()
  181. sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w')
  182. if args.kill_cache:
  183. for cache_obj in disk_cache.caches:
  184. cache_obj.kill_cache()
  185. main(args)
  186. for cache_obj in disk_cache.caches:
  187. if cache_obj.fresh:
  188. cache_obj.save_cache()