From e96e8dcce320bdddc152293694dbe6f891cf3108 Mon Sep 17 00:00:00 2001 From: Raphael Roberts Date: Wed, 13 Mar 2019 16:57:38 -0500 Subject: [PATCH 1/5] Created a main function and catches timeout errors now --- main.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index a75a0b3..2aeb893 100755 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ import argparse import ctabus import datetime import os +import socket import re import time import urllib @@ -139,17 +140,7 @@ def show(data, rt_filter=None, _clear=False): print("="*36) -if __name__ == '__main__': - parser = argparse.ArgumentParser(prog='ctabus') - parser.add_argument('-l', '--lucky', action='store_true', - help='picks first result') - parser.add_argument('-p', '--periodic', metavar='SEC', - type=int, help='checks periodically') - parser.add_argument('-r', '--route', default=None) - parser.add_argument('-d', '--direction', default=None) - parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)') - args = parser.parse_args() - sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w') +def main(args): args.arg = ' '.join(args.arg) if not args.arg.isdecimal(): @@ -190,13 +181,27 @@ if __name__ == '__main__': timeout = 1 if args.periodic > timeout: timeout = args.periodic - data = ctabus.get_times(stop_id,timeout=timeout) + data = ctabus.get_times(stop_id, timeout=timeout) e = time.perf_counter() - s if e < args.periodic: time.sleep(args.periodic-e) except KeyboardInterrupt: _done = True - except urllib.error.URLError: + except (urllib.error.URLError, socket.timeout): print("Error fetching times") else: show(data, args.route) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog='ctabus') + parser.add_argument('-l', '--lucky', action='store_true', + help='picks first result') + parser.add_argument('-p', '--periodic', metavar='SEC', + type=int, help='checks periodically') + parser.add_argument('-r', '--route', default=None) + parser.add_argument('-d', '--direction', default=None) + parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)') + args = parser.parse_args() + sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w') + main(args) From f0dfa4375121987b140afae3cbb32a676de658b3 Mon Sep 17 00:00:00 2001 From: Raphael Roberts Date: Sat, 30 Mar 2019 02:34:45 -0500 Subject: [PATCH 2/5] Switched to using terminaltables package; catches both value and index error for selection --- main.py | 16 +++----- print2d.py | 102 ++++++++++++++++++++++++++++------------------- requirements.txt | 3 +- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/main.py b/main.py index 2aeb893..e5e41fe 100755 --- a/main.py +++ b/main.py @@ -13,6 +13,7 @@ import urllib import os.path as osp import sys CHICAGO_TZ = tz.gettz("America/Chicago") +DATETIME_FORMAT = "%A, %B %e, %Y %H:%M:%S" # https://stackoverflow.com/a/5967539 @@ -64,6 +65,7 @@ def pprint_delta(delta): def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): + from print2d import create_table, render_table k = displays[sort] display_data = {obj[k]: obj[data] for obj in objs} srt_keys = sorted(display_data.keys(), key=key) @@ -77,15 +79,8 @@ def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): if num_pic: display = [[i] + data for i, data in enumerate(display)] - opts = { - 'spacer': ' ', - 'seperator': ' ', - 'interactive': True, - 'bottom': '=', - 'l_end': '<', - 'r_end': '>', - } - print2d(display, **opts) + table = create_table(display, DATETIME_FORMAT) + render_table(table) if num_pic: which = None while not which: @@ -95,7 +90,7 @@ def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): quit() try: which = srt_keys[int(which)] - except ValueError: + except (ValueError, IndexError): which = None return display_data[which] else: @@ -145,7 +140,6 @@ def main(args): if not args.arg.isdecimal(): # save on import time slightly - from print2d import print2d from search import Search, StopSearch # routes if not args.route: diff --git a/print2d.py b/print2d.py index 100b993..ce702dd 100644 --- a/print2d.py +++ b/print2d.py @@ -1,5 +1,38 @@ import datetime -from pydoc import pager +import os +import sys +import pydoc +from pydoc import pager, pipepager, tempfilepager, plainpager, plain +from textwrap import fill +from terminaltables import AsciiTable +from terminaltables.terminal_io import terminal_size + + +def getpager(): + """Decide what method to use for paging through text.""" + if not hasattr(sys.stdin, "isatty"): + return plainpager + if not hasattr(sys.stdout, "isatty"): + return plainpager + if not sys.stdin.isatty() or not sys.stdout.isatty(): + return plainpager + use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER') + if use_pager: + if sys.platform == 'win32': # pipes completely broken in Windows + return lambda text: tempfilepager(plain(text), use_pager) + elif os.environ.get('TERM') in ('dumb', 'emacs'): + return lambda text: pipepager(plain(text), use_pager) + else: + return lambda text: pipepager(text, use_pager) + if os.environ.get('TERM') in ('dumb', 'emacs'): + return plainpager + if sys.platform == 'win32': + return lambda text: tempfilepager(plain(text), 'more <') + if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: + return lambda text: pipepager(text, 'less -X') + + +pydoc.getpager = getpager def str_coerce(s, **kwargs): @@ -9,48 +42,33 @@ def str_coerce(s, **kwargs): return str(s) -def print2d(list_param, - datetime_format="%A, %B %e, %Y %H:%M:%S", - seperator=' | ', - spacer=' ', - bottom='=', - l_end='|', r_end='|', - interactive=False - ): - list_param = [[str_coerce(s, datetime_format=datetime_format) - for s in row] for row in list_param] - - max_col = [] +def create_table(list_param, datetime_format): + rows = [] for row in list_param: - for i, col in enumerate(row): - try: - max_col[i] = max(max_col[i], len(col)) - except IndexError: - max_col.append(len(col)) - - fmt_row = '{content}' - if l_end: - fmt_row = '{} {}'.format(l_end, fmt_row) - if r_end: - fmt_row = '{} {}'.format(fmt_row, r_end) - - done = [] - for row in list_param: - content = seperator.join(col.ljust(max_col[i], spacer if i < len( - row)-1 or r_end else ' ') for i, col in enumerate(row)) - done.append(fmt_row.format(content=content)) + rows.append([]) + for item in row: + rows[-1].append(str_coerce(item, datetime_format=datetime_format)) + return AsciiTable(rows) - if bottom: - bottom = bottom*len(done[0]) - row_sep = ('\n'+bottom+'\n') - else: - row_sep = '\n' - final = row_sep.join(done) - if bottom: - final = '\n'.join((bottom, final, bottom)) + +def render_table(table: AsciiTable, interactive=True): + '''Do all wrapping to make the table fit in screen''' + table.inner_row_border = True + data = table.table_data + terminal_width = terminal_size()[0] + n_cols = len(data[0]) + even_distribution = terminal_width // n_cols + for row_num, row in enumerate(data): + for col_num, col_data in enumerate(row): + if len(col_data) > even_distribution: + if col_num != n_cols - 1: + data[row_num][col_num] = fill(col_data, even_distribution) + else: + data[row_num][col_num] = '' + data[row_num][col_num] = fill( + col_data, table.column_max_width(col_num)) if interactive: - if not bottom: - final += '\n' - pager(final) + pager(table.table) else: - return final + print(table.table) + diff --git a/requirements.txt b/requirements.txt index db25db4..0c78596 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ edlib -python-dateutil \ No newline at end of file +python-dateutil +terminaltables From 5a3b5144a0a283a4cb4b3a521de9602763ec7734 Mon Sep 17 00:00:00 2001 From: Raphael Roberts Date: Mon, 1 Apr 2019 13:20:34 -0500 Subject: [PATCH 3/5] added and fixed disk cache start from toast --- disk_cache.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ test_cache.py | 14 ++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 disk_cache.py create mode 100755 test_cache.py diff --git a/disk_cache.py b/disk_cache.py new file mode 100644 index 0000000..8e97d54 --- /dev/null +++ b/disk_cache.py @@ -0,0 +1,48 @@ +import pickle +import os +import lzma +cache_path = os.path.abspath(os.path.join(__file__, "..", "__pycache__")) +if not os.path.exists(cache_path): + os.mkdir(cache_path) + + +def make_key(*args, **kwargs): + + return args, tuple(sorted( + kwargs.items(), key=lambda item: item[0])) + + +class disk_cache: + def __init__(self, func): + self.fname = "{}.{}.dc".format(func.__module__, func.__name__) + self.fname = os.path.join(cache_path, self.fname) + self.func = func + self.load_cache() + + def __call__(self, *args, **kwargs): + key = make_key(*args, **kwargs) + try: + return self.cache[key] + except KeyError: + res = self.func(*args, **kwargs) + self.cache[key] = res + return res + + def load_cache(self): + try: + with lzma.open(self.fname, 'rb') as file: + cache = pickle.load(file) + self.fresh = False + except FileNotFoundError: + cache = {} + self.fresh = True + self.cache = cache + + def save_cache(self): + with lzma.open(self.fname, 'wb') as file: + pickle.dump(self.cache, file) + + def delete_cache(self): + os.remove(self.fname) + self.cache = {} + self.fresh = True diff --git a/test_cache.py b/test_cache.py new file mode 100755 index 0000000..c618e90 --- /dev/null +++ b/test_cache.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +test = False +test = True +from disk_cache import * +@disk_cache +def func(n): + t = 1 + for i in range(1,n+1): + t *= i + return t +if test: + for i in range(0,10**9,1000): + print(i) + func.save_cache() From 5437b03cf90c61514e919d902f24182532f0ff8d Mon Sep 17 00:00:00 2001 From: Raphael Roberts Date: Mon, 1 Apr 2019 13:57:59 -0500 Subject: [PATCH 4/5] Added disk cache and -k option to refresh --- ctabus.py | 6 +++++- disk_cache.py | 6 +++++- main.py | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ctabus.py b/ctabus.py index 4ef42ea..8491553 100644 --- a/ctabus.py +++ b/ctabus.py @@ -1,5 +1,6 @@ from urllib.parse import urlencode from urllib.request import urlopen +from disk_cache import disk_cache import json from sensitive import api @@ -10,7 +11,7 @@ def get_data(type, api_key=api, timeout=None, **args): args['format'] = 'json' url = base_url.format(type=type, query=urlencode(args)) if timeout is not None: - response = urlopen(url,timeout = timeout) + response = urlopen(url, timeout=timeout) else: response = urlopen(url) data = json.load(response)['bustime-response'] @@ -25,13 +26,16 @@ def get_times(stop_id, api_key=api, timeout=None): return get_data('getpredictions', api_key, stpid=stop_id, timeout=timeout) +@disk_cache def get_routes(api_key=api, timeout=None): return get_data('getroutes', api_key, timeout=timeout) +@disk_cache def get_directions(route, api_key=api, timeout=None): return get_data('getdirections', api_key, rt=route, timeout=timeout) +@disk_cache def get_stops(route, direction, api_key=api, timeout=None): return get_data('getstops', api_key, rt=route, dir=direction, timeout=timeout) diff --git a/disk_cache.py b/disk_cache.py index 8e97d54..a5e84d4 100644 --- a/disk_cache.py +++ b/disk_cache.py @@ -13,11 +13,15 @@ def make_key(*args, **kwargs): class disk_cache: + caches = [] + """Decorator to make a function with lru_cache that can be written to disk""" + def __init__(self, func): self.fname = "{}.{}.dc".format(func.__module__, func.__name__) self.fname = os.path.join(cache_path, self.fname) self.func = func self.load_cache() + disk_cache.caches.append(self) def __call__(self, *args, **kwargs): key = make_key(*args, **kwargs) @@ -40,7 +44,7 @@ class disk_cache: def save_cache(self): with lzma.open(self.fname, 'wb') as file: - pickle.dump(self.cache, file) + pickle.dump(self.cache, file, pickle.HIGHEST_PROTOCOL) def delete_cache(self): os.remove(self.fname) diff --git a/main.py b/main.py index e5e41fe..d867493 100755 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 from dateutil.parser import parse as date_parse from dateutil import tz +from disk_cache import disk_cache import argparse import ctabus import datetime @@ -195,7 +196,14 @@ if __name__ == '__main__': type=int, help='checks periodically') parser.add_argument('-r', '--route', default=None) parser.add_argument('-d', '--direction', default=None) + parser.add_argument('-k', '--kill-cache', action="store_true") parser.add_argument('arg', nargs='+', metavar='(stop-id | cross streets)') args = parser.parse_args() sys.stderr = open(osp.join(osp.dirname(__file__), 'stderr.log'), 'w') + if args.kill_cache: + for cache_obj in disk_cache.caches: + cache_obj.kill_cache() main(args) + for cache_obj in disk_cache.caches: + if cache_obj.fresh: + cache_obj.save_cache() From d5dae075d38074cd8ff949fdae535ea7eda6befd Mon Sep 17 00:00:00 2001 From: Raphael Roberts Date: Tue, 2 Apr 2019 20:09:37 -0500 Subject: [PATCH 5/5] Extensive testing of disk_cache and new terminal tables --- ctabus.py | 10 +++++++++- disk_cache.py | 6 ++++-- main.py | 13 +++++++++++-- print2d.py | 16 ++++++---------- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/ctabus.py b/ctabus.py index 8491553..6e968cd 100644 --- a/ctabus.py +++ b/ctabus.py @@ -38,4 +38,12 @@ def get_directions(route, api_key=api, timeout=None): @disk_cache def get_stops(route, direction, api_key=api, timeout=None): - return get_data('getstops', api_key, rt=route, dir=direction, timeout=timeout) + return get_data('getstops', api_key, rt=route, dir=direction, + timeout=timeout) + + +@disk_cache +def get_name_from_direction(route, direction, api_key=api, timeout=None): + test_stop = get_stops(route, direction, api_key=api_key, + timeout=timeout)['stops'][0]['stpid'] + return get_times(test_stop, api_key=api, timeout=timeout)['prd'][0]['des'] diff --git a/disk_cache.py b/disk_cache.py index a5e84d4..b5a9b3c 100644 --- a/disk_cache.py +++ b/disk_cache.py @@ -13,8 +13,8 @@ def make_key(*args, **kwargs): class disk_cache: + """Decorator to make persistent cache""" caches = [] - """Decorator to make a function with lru_cache that can be written to disk""" def __init__(self, func): self.fname = "{}.{}.dc".format(func.__module__, func.__name__) @@ -26,8 +26,10 @@ class disk_cache: def __call__(self, *args, **kwargs): key = make_key(*args, **kwargs) try: - return self.cache[key] + res = self.cache[key] + return res except KeyError: + self.fresh = True res = self.func(*args, **kwargs) self.cache[key] = res return res diff --git a/main.py b/main.py index d867493..e73b9af 100755 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from dateutil.parser import parse as date_parse from dateutil import tz -from disk_cache import disk_cache +from disk_cache import disk_cache, make_key import argparse import ctabus import datetime @@ -67,6 +67,7 @@ def pprint_delta(delta): def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): from print2d import create_table, render_table + # sort based on column number k = displays[sort] display_data = {obj[k]: obj[data] for obj in objs} srt_keys = sorted(display_data.keys(), key=key) @@ -152,7 +153,11 @@ def main(args): data = ctabus.get_directions(route)['directions'] # direction if not args.direction: - direction = gen_list(data, 'dir', 'dir') + for direction_obj in data: + friendly_name = ctabus.get_name_from_direction( + route, direction_obj['dir']) + direction_obj['friendly_name'] = friendly_name + direction = gen_list(data, 'dir', 'dir', 'friendly_name') else: s = Search(args.direction) direction = sorted((obj['dir'] for obj in data), key=s)[0] @@ -167,6 +172,10 @@ def main(args): else: stop_id = args.arg data = ctabus.get_times(stop_id) + info = data['prd'][0] + key = make_key(info['rt'], info['rtdir'], ctabus.api, None) + if key not in ctabus.get_name_from_direction.cache.keys(): + ctabus.get_name_from_direction.cache[key] = info['des'] if args.periodic is not None: _done = False while not _done: diff --git a/print2d.py b/print2d.py index ce702dd..67cf1ce 100644 --- a/print2d.py +++ b/print2d.py @@ -1,11 +1,10 @@ +from terminaltables.terminal_io import terminal_size +from terminaltables import AsciiTable +from textwrap import fill +from pydoc import pipepager, tempfilepager, plainpager, plain import datetime import os import sys -import pydoc -from pydoc import pager, pipepager, tempfilepager, plainpager, plain -from textwrap import fill -from terminaltables import AsciiTable -from terminaltables.terminal_io import terminal_size def getpager(): @@ -32,9 +31,6 @@ def getpager(): return lambda text: pipepager(text, 'less -X') -pydoc.getpager = getpager - - def str_coerce(s, **kwargs): if isinstance(s, datetime.datetime): return s.strftime(kwargs['datetime_format']) @@ -68,7 +64,7 @@ def render_table(table: AsciiTable, interactive=True): data[row_num][col_num] = fill( col_data, table.column_max_width(col_num)) if interactive: + pager = getpager() pager(table.table) else: - print(table.table) - + print(table.table