9 changed files with 366 additions and 364 deletions
-
362ctabus/__init__.py
-
196ctabus/__main__.py
-
5ctabus/internal/config.py
-
0ctabus/ui/__init__.py
-
10ctabus/ui/notification.py
-
133ctabus/ui/picker.py
-
11ctabus/ui/toast.py
-
11ctabus/ui/util.py
-
2setup.py
@ -1,363 +1 @@ |
|||||
#!/usr/bin/python3 |
|
||||
__version__ = "2.4.1" |
__version__ = "2.4.1" |
||||
from dateutil import tz |
|
||||
from dateutil.parser import parse as date_parse |
|
||||
from threading import Event |
|
||||
|
|
||||
import argparse |
|
||||
import datetime |
|
||||
import os |
|
||||
import os.path as osp |
|
||||
import re |
|
||||
import shutil |
|
||||
import socket |
|
||||
import subprocess |
|
||||
import sys |
|
||||
import time |
|
||||
import urllib |
|
||||
|
|
||||
import lazy_import |
|
||||
|
|
||||
|
|
||||
from ctabus.internal.config import log_dir, recent_list |
|
||||
from ctabus.internal.notification import HAS_NOTIFICATION |
|
||||
|
|
||||
fetch = lazy_import.lazy_module("ctabus.fetch") |
|
||||
|
|
||||
disk_cache = lazy_import.lazy_class("ctabus.internal.disk_cache.disk_cache") |
|
||||
make_key = lazy_import.lazy_function("ctabus.internal.disk_cache.make_key") |
|
||||
|
|
||||
|
|
||||
NotificationManager = lazy_import.lazy_class( |
|
||||
"ctabus.internal.notification.NotificationManager" |
|
||||
) |
|
||||
|
|
||||
create_table = lazy_import.lazy_function("ctabus.internal.print2d.create_table") |
|
||||
render_table = lazy_import.lazy_function("ctabus.internal.print2d.render_table") |
|
||||
|
|
||||
Search = lazy_import.lazy_class("ctabus.internal.search.Search") |
|
||||
StopSearch = lazy_import.lazy_class("ctabus.internal.search.StopSearch") |
|
||||
|
|
||||
HAS_TOAST = shutil.which("termux-toast") is not None |
|
||||
CHICAGO_TZ = tz.gettz("America/Chicago") |
|
||||
DATETIME_FORMAT = "%A, %B %e, %Y %H:%M:%S" |
|
||||
EXIT = Event() |
|
||||
# https://stackoverflow.com/a/5967539 |
|
||||
|
|
||||
parser = argparse.ArgumentParser(prog="ctabus") |
|
||||
parser.add_argument( |
|
||||
"-v", "--version", action="version", version="%(prog)s {}".format(__version__) |
|
||||
) |
|
||||
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("-t", "--toast", action="store_true") |
|
||||
parser.add_argument("-n", "--notifications", action="store_true") |
|
||||
parser.add_argument("-k", "--kill-cache", action="store_true") |
|
||||
parser.add_argument( |
|
||||
"arg", |
|
||||
nargs="*" if len(recent_list.elements) > 0 else "+", |
|
||||
metavar="(stop-id | cross streets)", |
|
||||
default=[":recents:"], |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def toast(text): |
|
||||
read, write = os.pipe() |
|
||||
os.write(write, text.encode()) |
|
||||
os.close(write) |
|
||||
subprocess.Popen( |
|
||||
["termux-toast", "-s", "-g", "top", "-c", "white", "-b", "black"], stdin=read |
|
||||
) |
|
||||
|
|
||||
|
|
||||
def atoi(text): |
|
||||
return int(text) if text.isdigit() else text |
|
||||
|
|
||||
|
|
||||
def numb_sort(text): |
|
||||
""" |
|
||||
alist.sort(key=natural_keys) sorts in human order |
|
||||
http://nedbatchelder.com/blog/200712/human_sorting.html |
|
||||
(See Toothy's implementation in the comments) |
|
||||
""" |
|
||||
return [atoi(c) for c in re.split(r"(\d+)", text)] |
|
||||
|
|
||||
|
|
||||
def clearscr(): |
|
||||
os.system("cls" if os.name == "nt" else "clear") |
|
||||
|
|
||||
|
|
||||
def pprint_delta(delta): |
|
||||
delta = str(delta) |
|
||||
days = None |
|
||||
s1 = delta.split(", ") |
|
||||
if len(s1) > 1: |
|
||||
days, time = s1 |
|
||||
else: |
|
||||
time = s1[0] |
|
||||
time = time.split(".")[0] |
|
||||
hour, minute, second = map(int, time.split(":")) |
|
||||
time = "" |
|
||||
if hour: |
|
||||
time += "{hour} hour".format(hour=hour) + ("s" if hour != 1 else "") |
|
||||
if minute: |
|
||||
if time and not time.endswith(", "): |
|
||||
time += ", " |
|
||||
time += "{minute} minute".format(minute=minute) + ("s" if minute != 1 else "") |
|
||||
if second: |
|
||||
if time and not time.endswith(", "): |
|
||||
time += ", " |
|
||||
time += "{second} second".format(second=second) + ("s" if second != 1 else "") |
|
||||
ret = "" |
|
||||
if days: |
|
||||
ret = days + ", " if time else "" |
|
||||
ret += time |
|
||||
return ret |
|
||||
|
|
||||
|
|
||||
class Table: |
|
||||
def __init__(self, dicts): |
|
||||
self.dicts = dicts |
|
||||
self.display = None |
|
||||
|
|
||||
def sort_by(self, key_name, key_function): |
|
||||
if key_function is None: |
|
||||
|
|
||||
def key(obj): |
|
||||
return obj[key_name] |
|
||||
|
|
||||
else: |
|
||||
|
|
||||
def key(obj): |
|
||||
return key_function(obj[key_name]) |
|
||||
|
|
||||
self.dicts = sorted(self.dicts, key=key) |
|
||||
|
|
||||
def set_display_categories(self, categories): |
|
||||
self.display = [ |
|
||||
[_dict[column_name] for column_name in categories] for _dict in self.dicts |
|
||||
] |
|
||||
|
|
||||
def get_index_interactive(self): |
|
||||
|
|
||||
if self.display is None: |
|
||||
raise Exception("Display columns not set") |
|
||||
display = [[i] + row for i, row in enumerate(self.display)] |
|
||||
table = create_table(display, DATETIME_FORMAT) |
|
||||
render_table(table) |
|
||||
which = None |
|
||||
while which is None: |
|
||||
try: |
|
||||
which = int(input("Which one?: ")) |
|
||||
if which >= len(self.dicts): |
|
||||
print("Max index is {}".format(len(self.dicts) - 1)) |
|
||||
|
|
||||
except ValueError: |
|
||||
which = None |
|
||||
return which |
|
||||
|
|
||||
def get_name_interactive(self, data_column): |
|
||||
|
|
||||
if self.display is None: |
|
||||
raise Exception("Display columns not set") |
|
||||
display = self.display |
|
||||
table = create_table(display, DATETIME_FORMAT) |
|
||||
render_table(table) |
|
||||
which = None |
|
||||
valid = set(_dict[data_column] for _dict in self.dicts) |
|
||||
while which is None: |
|
||||
which = input("Which one?: ") |
|
||||
if which not in valid: |
|
||||
which = None |
|
||||
return which |
|
||||
|
|
||||
|
|
||||
class CTABUSNotifictaionManager(NotificationManager): |
|
||||
def on_done(self): |
|
||||
EXIT.set() |
|
||||
|
|
||||
|
|
||||
def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): |
|
||||
"""Returns an item from objs[data] based on name or index |
|
||||
|
|
||||
|
|
||||
:param objs: list of dicts that contain data and display |
|
||||
:param data: name of dictionary key to return |
|
||||
:param displays: things to show the user |
|
||||
:param key: function to use for sorting |
|
||||
:param sort: which column of the displays should be used for sorting |
|
||||
:param num_pic: indicate whether or not to use indices to pick value |
|
||||
""" |
|
||||
display_table = Table(objs) |
|
||||
display_table.sort_by(displays[sort], key) |
|
||||
display_table.set_display_categories(displays) |
|
||||
try: |
|
||||
if num_pic: |
|
||||
which = display_table.get_index_interactive() |
|
||||
return display_table.dicts[which][data] |
|
||||
else: |
|
||||
which = display_table.get_name_interactive(data) |
|
||||
return which |
|
||||
except KeyboardInterrupt: |
|
||||
quit(0) |
|
||||
|
|
||||
|
|
||||
config = """\ |
|
||||
{route} - {end} ({direction}) |
|
||||
{nm}, stop {stop_id} |
|
||||
{delta} ({t})\ |
|
||||
""" |
|
||||
|
|
||||
if HAS_NOTIFICATION: |
|
||||
nm = CTABUSNotifictaionManager() |
|
||||
|
|
||||
|
|
||||
def show( |
|
||||
data, rt_filter=None, _clear=False, enable_toast=False, enable_notifications=False |
|
||||
): |
|
||||
times = data["prd"] |
|
||||
now = datetime.datetime.now(CHICAGO_TZ) |
|
||||
arrivals = sorted(times, key=lambda t: t["prdtm"]) |
|
||||
if rt_filter is not None: |
|
||||
arrivals = filter(lambda arrival: arrival["rt"] == rt_filter, arrivals) |
|
||||
if _clear: |
|
||||
clearscr() |
|
||||
do_gui = True |
|
||||
for bustime in arrivals: |
|
||||
before = date_parse(bustime["prdtm"]) |
|
||||
arrival = before.replace(tzinfo=CHICAGO_TZ) |
|
||||
if arrival > now: |
|
||||
format_dict = { |
|
||||
"stop_id": bustime["stpid"], |
|
||||
"delta": pprint_delta(arrival - now), |
|
||||
"t": arrival.strftime("%H:%M:%S"), |
|
||||
"route": bustime["rt"], |
|
||||
"direction": bustime["rtdir"], |
|
||||
"end": bustime["des"], |
|
||||
"nm": bustime["stpnm"].rstrip(), |
|
||||
} |
|
||||
text_to_show = config.format(**format_dict) |
|
||||
if do_gui and enable_notifications: |
|
||||
nm.set_title("Bustimes") |
|
||||
nm.say(text_to_show) |
|
||||
|
|
||||
if do_gui and enable_toast: |
|
||||
toast(text_to_show) |
|
||||
|
|
||||
do_gui = False |
|
||||
|
|
||||
print(text_to_show, end="\n" * 2) |
|
||||
print("=" * 36) |
|
||||
|
|
||||
|
|
||||
def _picker(args): |
|
||||
|
|
||||
# routes |
|
||||
if not args.route: |
|
||||
data = fetch.get_routes()["routes"] |
|
||||
route = gen_list(data, "rt", "rt", "rtnm", num_pic=False, key=numb_sort) |
|
||||
else: |
|
||||
route = args.route |
|
||||
data = fetch.get_directions(route)["directions"] |
|
||||
# direction |
|
||||
if not args.direction: |
|
||||
for direction_obj in data: |
|
||||
friendly_name = fetch.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] |
|
||||
# direction |
|
||||
stops = fetch.get_stops(route, direction)["stops"] |
|
||||
s = StopSearch(args.arg) |
|
||||
if args.lucky: |
|
||||
stop_id = sorted(stops, key=lambda stop: s(stop["stpnm"]))[0]["stpid"] |
|
||||
else: |
|
||||
stop_id = gen_list(stops, "stpid", "stpnm", key=s) |
|
||||
return stop_id |
|
||||
|
|
||||
|
|
||||
def _picker_recent(args): |
|
||||
recent_stops = [] |
|
||||
for stop in recent_list.elements: |
|
||||
info = fetch.get_data_from_stop_id(stop) |
|
||||
recent_stops.append(info) |
|
||||
display_table = Table(recent_stops) |
|
||||
display_table.set_display_categories( |
|
||||
["stop_id", "stop_name", "route_number", "route_direction", "route_name"] |
|
||||
) |
|
||||
index = display_table.get_index_interactive() |
|
||||
return recent_list.get(index) |
|
||||
|
|
||||
|
|
||||
def _main_periodic(args, stop_id, init_data): |
|
||||
_done = False |
|
||||
data = init_data |
|
||||
while not _done: |
|
||||
try: |
|
||||
try: |
|
||||
s = time.perf_counter() |
|
||||
show( |
|
||||
data, |
|
||||
args.route, |
|
||||
True, |
|
||||
args.toast and HAS_TOAST, |
|
||||
args.notifications and HAS_NOTIFICATION, |
|
||||
) |
|
||||
timeout = 1 |
|
||||
if args.periodic > timeout: |
|
||||
timeout = args.periodic |
|
||||
data = fetch.get_times(stop_id, timeout=timeout) |
|
||||
e = time.perf_counter() - s |
|
||||
except (urllib.error.URLError, socket.timeout): |
|
||||
e = time.perf_counter() - s |
|
||||
print("Error fetching times") |
|
||||
if e < args.periodic: |
|
||||
EXIT.wait(args.periodic - e) |
|
||||
if EXIT.is_set(): |
|
||||
quit(0) |
|
||||
except KeyboardInterrupt: |
|
||||
_done = True |
|
||||
|
|
||||
|
|
||||
def main(args=None): |
|
||||
if args is None: |
|
||||
args = parser.parse_args() |
|
||||
sys.stderr = open(osp.join(log_dir, "stderr.log"), "w") |
|
||||
if args.kill_cache: |
|
||||
for cache_obj in disk_cache.caches: |
|
||||
cache_obj.delete_cache() |
|
||||
args.arg = " ".join(args.arg) |
|
||||
from_recent = False |
|
||||
if args.arg.isdecimal(): |
|
||||
stop_id = args.arg |
|
||||
else: |
|
||||
if args.arg == ":recents:": |
|
||||
stop_id = _picker_recent(args) |
|
||||
from_recent = True |
|
||||
else: |
|
||||
stop_id = _picker(args) |
|
||||
if not from_recent: |
|
||||
recent_list.add(stop_id) |
|
||||
data = fetch.get_times(stop_id) |
|
||||
fetch.get_data_from_stop_id(stop_id, __setup__=data) |
|
||||
info = data["prd"][0] |
|
||||
key = make_key(info["rt"], info["rtdir"], fetch.api, None) |
|
||||
if key not in fetch.get_name_from_direction.cache.keys(): |
|
||||
fetch.get_name_from_direction.cache[key] = info["des"] |
|
||||
fetch.get_name_from_direction.fresh = True |
|
||||
|
|
||||
if args.periodic is not None: |
|
||||
_main_periodic(args, stop_id, data) |
|
||||
else: |
|
||||
show(data, args.route) |
|
||||
|
|
||||
|
|
||||
if __name__ == "__main__": |
|
||||
args = parser.parse_args() |
|
||||
main(args) |
|
||||
@ -1,4 +1,198 @@ |
|||||
from ctabus import main |
|
||||
|
#!/usr/bin/python3 |
||||
|
from dateutil.parser import parse as date_parse |
||||
|
from threading import Event |
||||
|
|
||||
|
import argparse |
||||
|
import datetime |
||||
|
import os |
||||
|
import os.path as osp |
||||
|
import socket |
||||
|
import sys |
||||
|
import time |
||||
|
import urllib |
||||
|
|
||||
|
import lazy_import |
||||
|
|
||||
|
from ctabus import __version__ |
||||
|
from ctabus.internal.config import log_dir, recent_list, CHICAGO_TZ |
||||
|
|
||||
|
notification_module = lazy_import.lazy_module("ctabus.ui.notification") |
||||
|
toast_module = lazy_import.lazy_module("ctabus.ui.toast") |
||||
|
|
||||
|
disk_cache = lazy_import.lazy_class("ctabus.internal.disk_cache.disk_cache") |
||||
|
make_key = lazy_import.lazy_function("ctabus.internal.disk_cache.make_key") |
||||
|
fetch = lazy_import.lazy_module("ctabus.fetch") |
||||
|
|
||||
|
_picker = lazy_import.lazy_function("ctabus.ui.picker._picker") |
||||
|
_picker_recent = lazy_import.lazy_function("ctabus.ui.picker._picker_recent") |
||||
|
|
||||
|
|
||||
|
toast = lazy_import.lazy_function("ctabus.ui.toast") |
||||
|
|
||||
|
|
||||
|
EXIT = Event() |
||||
|
|
||||
|
|
||||
|
parser = argparse.ArgumentParser(prog="ctabus") |
||||
|
parser.add_argument( |
||||
|
"-v", "--version", action="version", version="%(prog)s {}".format(__version__) |
||||
|
) |
||||
|
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("-t", "--toast", action="store_true") |
||||
|
parser.add_argument("-n", "--notifications", action="store_true") |
||||
|
parser.add_argument("-k", "--kill-cache", action="store_true") |
||||
|
parser.add_argument( |
||||
|
"arg", |
||||
|
nargs="*" if len(recent_list.elements) > 0 else "+", |
||||
|
metavar="(stop-id | cross streets)", |
||||
|
default=[":recents:"], |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def clearscr(): |
||||
|
os.system("cls" if os.name == "nt" else "clear") |
||||
|
|
||||
|
|
||||
|
def pprint_delta(delta): |
||||
|
delta = str(delta) |
||||
|
days = None |
||||
|
s1 = delta.split(", ") |
||||
|
if len(s1) > 1: |
||||
|
days, time = s1 |
||||
|
else: |
||||
|
time = s1[0] |
||||
|
time = time.split(".")[0] |
||||
|
hour, minute, second = map(int, time.split(":")) |
||||
|
time = "" |
||||
|
if hour: |
||||
|
time += "{hour} hour".format(hour=hour) + ("s" if hour != 1 else "") |
||||
|
if minute: |
||||
|
if time and not time.endswith(", "): |
||||
|
time += ", " |
||||
|
time += "{minute} minute".format(minute=minute) + ("s" if minute != 1 else "") |
||||
|
if second: |
||||
|
if time and not time.endswith(", "): |
||||
|
time += ", " |
||||
|
time += "{second} second".format(second=second) + ("s" if second != 1 else "") |
||||
|
ret = "" |
||||
|
if days: |
||||
|
ret = days + ", " if time else "" |
||||
|
ret += time |
||||
|
return ret |
||||
|
|
||||
|
|
||||
|
config = """\ |
||||
|
{route} - {end} ({direction}) |
||||
|
{nm}, stop {stop_id} |
||||
|
{delta} ({t})\ |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
def show( |
||||
|
data, rt_filter=None, _clear=False, enable_toast=False, enable_notifications=False |
||||
|
): |
||||
|
times = data["prd"] |
||||
|
now = datetime.datetime.now(CHICAGO_TZ) |
||||
|
arrivals = sorted(times, key=lambda t: t["prdtm"]) |
||||
|
if rt_filter is not None: |
||||
|
arrivals = filter(lambda arrival: arrival["rt"] == rt_filter, arrivals) |
||||
|
if _clear: |
||||
|
clearscr() |
||||
|
do_gui = True |
||||
|
for bustime in arrivals: |
||||
|
before = date_parse(bustime["prdtm"]) |
||||
|
arrival = before.replace(tzinfo=CHICAGO_TZ) |
||||
|
if arrival > now: |
||||
|
format_dict = { |
||||
|
"stop_id": bustime["stpid"], |
||||
|
"delta": pprint_delta(arrival - now), |
||||
|
"t": arrival.strftime("%H:%M:%S"), |
||||
|
"route": bustime["rt"], |
||||
|
"direction": bustime["rtdir"], |
||||
|
"end": bustime["des"], |
||||
|
"nm": bustime["stpnm"].rstrip(), |
||||
|
} |
||||
|
text_to_show = config.format(**format_dict) |
||||
|
if do_gui and enable_notifications: |
||||
|
notification_module.nm.set_title("Bustimes") |
||||
|
notification_module.nm.say(text_to_show) |
||||
|
|
||||
|
if do_gui and enable_toast: |
||||
|
toast(text_to_show) |
||||
|
|
||||
|
do_gui = False |
||||
|
|
||||
|
print(text_to_show, end="\n" * 2) |
||||
|
print("=" * 36) |
||||
|
|
||||
|
|
||||
|
def _main_periodic(args, stop_id, init_data): |
||||
|
_done = False |
||||
|
data = init_data |
||||
|
while not _done: |
||||
|
try: |
||||
|
try: |
||||
|
s = time.perf_counter() |
||||
|
show( |
||||
|
data, |
||||
|
args.route, |
||||
|
True, |
||||
|
args.toast and toast_module.HAS_TOAST, |
||||
|
args.notifications and notification_module.HAS_NOTIFICATION, |
||||
|
) |
||||
|
timeout = 1 |
||||
|
if args.periodic > timeout: |
||||
|
timeout = args.periodic |
||||
|
data = fetch.get_times(stop_id, timeout=timeout) |
||||
|
e = time.perf_counter() - s |
||||
|
except (urllib.error.URLError, socket.timeout): |
||||
|
e = time.perf_counter() - s |
||||
|
print("Error fetching times") |
||||
|
if e < args.periodic: |
||||
|
EXIT.wait(args.periodic - e) |
||||
|
if EXIT.is_set(): |
||||
|
quit(0) |
||||
|
except KeyboardInterrupt: |
||||
|
_done = True |
||||
|
|
||||
|
|
||||
|
def main(args=None): |
||||
|
if args is None: |
||||
|
args = parser.parse_args() |
||||
|
sys.stderr = open(osp.join(log_dir, "stderr.log"), "w") |
||||
|
if args.kill_cache: |
||||
|
for cache_obj in disk_cache.caches: |
||||
|
cache_obj.delete_cache() |
||||
|
args.arg = " ".join(args.arg) |
||||
|
from_recent = False |
||||
|
if args.arg.isdecimal(): |
||||
|
stop_id = args.arg |
||||
|
else: |
||||
|
if args.arg == ":recents:": |
||||
|
stop_id = _picker_recent(args) |
||||
|
from_recent = True |
||||
|
else: |
||||
|
stop_id = _picker(args) |
||||
|
if not from_recent: |
||||
|
recent_list.add(stop_id) |
||||
|
data = fetch.get_times(stop_id) |
||||
|
fetch.get_data_from_stop_id(stop_id, __setup__=data) |
||||
|
info = data["prd"][0] |
||||
|
key = make_key(info["rt"], info["rtdir"], fetch.api, None) |
||||
|
if key not in fetch.get_name_from_direction.cache.keys(): |
||||
|
fetch.get_name_from_direction.cache[key] = info["des"] |
||||
|
fetch.get_name_from_direction.fresh = True |
||||
|
|
||||
|
if args.periodic is not None: |
||||
|
_main_periodic(args, stop_id, data) |
||||
|
else: |
||||
|
show(data, args.route) |
||||
|
|
||||
|
|
||||
if __name__ == "__main__": |
if __name__ == "__main__": |
||||
main() |
main() |
||||
@ -0,0 +1,10 @@ |
|||||
|
from ctabus.internal.notification import NotificationManager, HAS_NOTIFICATION |
||||
|
|
||||
|
|
||||
|
class CTABUSNotifictaionManager(NotificationManager): |
||||
|
def on_done(self): |
||||
|
EXIT.set() |
||||
|
|
||||
|
|
||||
|
if HAS_NOTIFICATION: |
||||
|
nm = CTABUSNotifictaionManager() |
||||
@ -0,0 +1,133 @@ |
|||||
|
import lazy_import |
||||
|
|
||||
|
from ctabus.internal.config import DATETIME_FORMAT, recent_list |
||||
|
from ctabus.ui.util import numb_sort |
||||
|
|
||||
|
fetch = lazy_import.lazy_module("ctabus.fetch") |
||||
|
create_table = lazy_import.lazy_function("ctabus.internal.print2d.create_table") |
||||
|
StopSearch = lazy_import.lazy_class("ctabus.internal.search.StopSearch") |
||||
|
Search = lazy_import.lazy_class("ctabus.internal.search.Search") |
||||
|
render_table = lazy_import.lazy_function("ctabus.internal.print2d.render_table") |
||||
|
|
||||
|
|
||||
|
class Table: |
||||
|
def __init__(self, dicts): |
||||
|
self.dicts = dicts |
||||
|
self.display = None |
||||
|
|
||||
|
def sort_by(self, key_name, key_function): |
||||
|
if key_function is None: |
||||
|
|
||||
|
def key(obj): |
||||
|
return obj[key_name] |
||||
|
|
||||
|
else: |
||||
|
|
||||
|
def key(obj): |
||||
|
return key_function(obj[key_name]) |
||||
|
|
||||
|
self.dicts = sorted(self.dicts, key=key) |
||||
|
|
||||
|
def set_display_categories(self, categories): |
||||
|
self.display = [ |
||||
|
[_dict[column_name] for column_name in categories] for _dict in self.dicts |
||||
|
] |
||||
|
|
||||
|
def get_index_interactive(self): |
||||
|
|
||||
|
if self.display is None: |
||||
|
raise Exception("Display columns not set") |
||||
|
display = [[i] + row for i, row in enumerate(self.display)] |
||||
|
table = create_table(display, DATETIME_FORMAT) |
||||
|
render_table(table) |
||||
|
which = None |
||||
|
while which is None: |
||||
|
try: |
||||
|
which = int(input("Which one?: ")) |
||||
|
if which >= len(self.dicts): |
||||
|
print("Max index is {}".format(len(self.dicts) - 1)) |
||||
|
|
||||
|
except ValueError: |
||||
|
which = None |
||||
|
return which |
||||
|
|
||||
|
def get_name_interactive(self, data_column): |
||||
|
|
||||
|
if self.display is None: |
||||
|
raise Exception("Display columns not set") |
||||
|
display = self.display |
||||
|
table = create_table(display, DATETIME_FORMAT) |
||||
|
render_table(table) |
||||
|
which = None |
||||
|
valid = set(_dict[data_column] for _dict in self.dicts) |
||||
|
while which is None: |
||||
|
which = input("Which one?: ") |
||||
|
if which not in valid: |
||||
|
which = None |
||||
|
return which |
||||
|
|
||||
|
|
||||
|
def gen_list(objs, data, *displays, key=None, sort=0, num_pic=True): |
||||
|
"""Returns an item from objs[data] based on name or index |
||||
|
|
||||
|
|
||||
|
:param objs: list of dicts that contain data and display |
||||
|
:param data: name of dictionary key to return |
||||
|
:param displays: things to show the user |
||||
|
:param key: function to use for sorting |
||||
|
:param sort: which column of the displays should be used for sorting |
||||
|
:param num_pic: indicate whether or not to use indices to pick value |
||||
|
""" |
||||
|
display_table = Table(objs) |
||||
|
display_table.sort_by(displays[sort], key) |
||||
|
display_table.set_display_categories(displays) |
||||
|
try: |
||||
|
if num_pic: |
||||
|
which = display_table.get_index_interactive() |
||||
|
return display_table.dicts[which][data] |
||||
|
else: |
||||
|
which = display_table.get_name_interactive(data) |
||||
|
return which |
||||
|
except KeyboardInterrupt: |
||||
|
quit(0) |
||||
|
|
||||
|
|
||||
|
def _picker(args): |
||||
|
|
||||
|
# routes |
||||
|
if not args.route: |
||||
|
data = fetch.get_routes()["routes"] |
||||
|
route = gen_list(data, "rt", "rt", "rtnm", num_pic=False, key=numb_sort) |
||||
|
else: |
||||
|
route = args.route |
||||
|
data = fetch.get_directions(route)["directions"] |
||||
|
# direction |
||||
|
if not args.direction: |
||||
|
for direction_obj in data: |
||||
|
friendly_name = fetch.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] |
||||
|
# direction |
||||
|
stops = fetch.get_stops(route, direction)["stops"] |
||||
|
s = StopSearch(args.arg) |
||||
|
if args.lucky: |
||||
|
stop_id = sorted(stops, key=lambda stop: s(stop["stpnm"]))[0]["stpid"] |
||||
|
else: |
||||
|
stop_id = gen_list(stops, "stpid", "stpnm", key=s) |
||||
|
return stop_id |
||||
|
|
||||
|
|
||||
|
def _picker_recent(args): |
||||
|
recent_stops = [] |
||||
|
for stop in recent_list.elements: |
||||
|
info = fetch.get_data_from_stop_id(stop) |
||||
|
recent_stops.append(info) |
||||
|
display_table = Table(recent_stops) |
||||
|
display_table.set_display_categories( |
||||
|
["stop_id", "stop_name", "route_number", "route_direction", "route_name"] |
||||
|
) |
||||
|
index = display_table.get_index_interactive() |
||||
|
return recent_list.get(index) |
||||
@ -0,0 +1,11 @@ |
|||||
|
import os |
||||
|
import subprocess |
||||
|
|
||||
|
|
||||
|
def toast(text): |
||||
|
read, write = os.pipe() |
||||
|
os.write(write, text.encode()) |
||||
|
os.close(write) |
||||
|
subprocess.Popen( |
||||
|
["termux-toast", "-s", "-g", "top", "-c", "white", "-b", "black"], stdin=read |
||||
|
) |
||||
@ -0,0 +1,11 @@ |
|||||
|
def atoi(text): |
||||
|
return int(text) if text.isdigit() else text |
||||
|
|
||||
|
|
||||
|
def numb_sort(text): |
||||
|
""" |
||||
|
alist.sort(key=natural_keys) sorts in human order |
||||
|
http://nedbatchelder.com/blog/200712/human_sorting.html |
||||
|
(See Toothy's implementation in the comments) |
||||
|
""" |
||||
|
return [atoi(c) for c in re.split(r"(\d+)", text)] |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue