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.

220 lines
6.8 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, make_key
  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. # sort based on column number
  61. k = displays[sort]
  62. display_data = {obj[k]: obj[data] for obj in objs}
  63. srt_keys = sorted(display_data.keys(), key=key)
  64. display = sorted(
  65. [
  66. [obj[d] for d in displays] for obj in objs
  67. ],
  68. key=lambda row: key(row[sort]) if key else row[sort]
  69. )
  70. if num_pic:
  71. display = [[i] + data for i, data in enumerate(display)]
  72. table = create_table(display, DATETIME_FORMAT)
  73. render_table(table)
  74. if num_pic:
  75. which = None
  76. while not which:
  77. try:
  78. which = input('Which one?: ')
  79. except KeyboardInterrupt:
  80. quit()
  81. try:
  82. which = srt_keys[int(which)]
  83. except (ValueError, IndexError):
  84. which = None
  85. return display_data[which]
  86. else:
  87. ret = None
  88. while not ret:
  89. try:
  90. ret = display_data[input('Which one?: ')]
  91. except KeyError:
  92. pass
  93. return ret
  94. config = '''\
  95. {route} - {end} ({direction})
  96. {nm}, stop {stop_id}
  97. {delta} ({t})\
  98. '''
  99. def show(data, rt_filter=None, _clear=False):
  100. times = data['prd']
  101. today = datetime.datetime.now(CHICAGO_TZ)
  102. arrivals = sorted(times, key=lambda t: t['prdtm'])
  103. if rt_filter is not None:
  104. arrivals = filter(lambda arrival: arrival['rt'] == rt_filter, arrivals)
  105. if _clear:
  106. clearscr()
  107. for bustime in arrivals:
  108. before = date_parse(bustime['prdtm'])
  109. arrival = before.replace(tzinfo=CHICAGO_TZ)
  110. if arrival > today:
  111. stop_id = bustime['stpid']
  112. delta = pprint_delta(arrival-today)
  113. t = arrival.strftime('%H:%M:%S')
  114. route = bustime['rt']
  115. direction = bustime['rtdir']
  116. end = bustime['des']
  117. nm = bustime['stpnm'].rstrip()
  118. print(
  119. config.format(**locals()), end='\n'*2
  120. )
  121. print("="*36)
  122. def main(args):
  123. args.arg = ' '.join(args.arg)
  124. if not args.arg.isdecimal():
  125. # save on import time slightly
  126. from search import Search, StopSearch
  127. # routes
  128. if not args.route:
  129. data = ctabus.get_routes()['routes']
  130. route = gen_list(data, 'rt', 'rt', 'rtnm',
  131. num_pic=False, key=numb_sort)
  132. else:
  133. route = args.route
  134. data = ctabus.get_directions(route)['directions']
  135. # direction
  136. if not args.direction:
  137. for direction_obj in data:
  138. friendly_name = ctabus.get_name_from_direction(
  139. route, direction_obj['dir'])
  140. direction_obj['friendly_name'] = friendly_name
  141. direction = gen_list(data, 'dir', 'dir', 'friendly_name')
  142. else:
  143. s = Search(args.direction)
  144. direction = sorted((obj['dir'] for obj in data), key=s)[0]
  145. # direction
  146. stops = ctabus.get_stops(route, direction)['stops']
  147. s = StopSearch(args.arg)
  148. if args.lucky:
  149. stop_id = sorted(stops, key=lambda stop: s(stop['stpnm']))[
  150. 0]['stpid']
  151. else:
  152. stop_id = gen_list(stops, 'stpid', 'stpnm', key=s)
  153. else:
  154. stop_id = args.arg
  155. data = ctabus.get_times(stop_id)
  156. info = data['prd'][0]
  157. key = make_key(info['rt'], info['rtdir'], ctabus.api, None)
  158. if key not in ctabus.get_name_from_direction.cache.keys():
  159. ctabus.get_name_from_direction.cache[key] = info['des']
  160. ctabus.get_name_from_direction.fresh = True
  161. if args.periodic is not None:
  162. _done = False
  163. while not _done:
  164. try:
  165. show(data, args.route, True)
  166. s = time.perf_counter()
  167. timeout = 1
  168. if args.periodic > timeout:
  169. timeout = args.periodic
  170. data = ctabus.get_times(stop_id, timeout=timeout)
  171. e = time.perf_counter() - s
  172. except KeyboardInterrupt:
  173. _done = True
  174. except (urllib.error.URLError, socket.timeout):
  175. e = time.perf_counter() - s
  176. print("Error fetching times")
  177. if e < args.periodic:
  178. time.sleep(args.periodic-e)
  179. else:
  180. show(data, args.route)
  181. if __name__ == '__main__':
  182. parser = argparse.ArgumentParser(prog='ctabus')
  183. parser.add_argument('-l', '--lucky', action='store_true',
  184. help='picks first result')
  185. parser.add_argument('-p', '--periodic', metavar='SEC',
  186. type=int, help='checks periodically')
  187. parser.add_argument('-r', '--route', default=None)
  188. parser.add_argument('-d', '--direction', default=None)
  189. parser.add_argument('-k', '--kill-cache', action="store_true")
  190. parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)')
  191. args = parser.parse_args()
  192. sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w')
  193. if args.kill_cache:
  194. for cache_obj in disk_cache.caches:
  195. cache_obj.delete_cache()
  196. main(args)
  197. for cache_obj in disk_cache.caches:
  198. if cache_obj.fresh:
  199. cache_obj.save_cache()