argparser

This commit is contained in:
Daniel Tsvetkov 2020-02-12 14:20:54 +01:00
parent d78a50c7d7
commit fb953069f3
3 changed files with 229 additions and 76 deletions

View File

@ -0,0 +1,5 @@
christmas,25 december
xmas,25 december
new years eve,31 december 23:59:59
new years,31 december 23:59:59
end of workday,17:00
1 christmas 25 december
2 xmas 25 december
3 new years eve 31 december 23:59:59
4 new years 31 december 23:59:59
5 end of workday 17:00

View File

@ -1,3 +1,4 @@
import argparse
import json import json
import locale import locale
import re import re
@ -5,12 +6,15 @@ import sys
from datetime import datetime from datetime import datetime
from pygments import highlight, lexers, formatters from pygments import highlight, lexers, formatters
from scalpl import Cut
from localization import setlocale, resolve_locale from localization import setlocale, resolve_locale
from tww import ISO_FORMAT, time_to_emoji, time_ago, workday_diff, workhours_diff, td_remainders, td_totals, td_iso8601 from tww import ISO_FORMAT, time_to_emoji, time_ago, workday_diff, workhours_diff, td_remainders, td_totals, td_iso8601
from tww import resolve_timezone, dateparser_parse_dt, get_utcnow, get_s_since_epoch, get_ms_since_epoch, \ from tww import resolve_timezone, dateparser_parse_dt, get_utcnow, get_s_since_epoch, get_ms_since_epoch, \
dt_tz_translation, get_local_now, query_to_format_result dt_tz_translation, get_local_now, query_to_format_result
custom_locale = None
r_generic = re.compile('(.*)', flags=re.IGNORECASE) r_generic = re.compile('(.*)', flags=re.IGNORECASE)
r_time_in_epoch_s_now = re.compile('(?:time since epoch|seconds since epoch)', flags=re.IGNORECASE) r_time_in_epoch_s_now = re.compile('(?:time since epoch|seconds since epoch)', flags=re.IGNORECASE)
r_time_in_epoch_s2 = re.compile('(.*)?\s*(?:in|to)\s*(?:epoch|seconds since epoch|seconds)', flags=re.IGNORECASE) r_time_in_epoch_s2 = re.compile('(.*)?\s*(?:in|to)\s*(?:epoch|seconds since epoch|seconds)', flags=re.IGNORECASE)
@ -33,42 +37,40 @@ r_timezone = re.compile('(.*)?\s(?:timezone|timezones|tz)', flags=re.IGNORECASE)
r_timezone_2 = re.compile('(?:timezone in|timezones in|tz in|timezone|timezones|tz)\s(.*)?', flags=re.IGNORECASE) r_timezone_2 = re.compile('(?:timezone in|timezones in|tz in|timezone|timezones|tz)\s(.*)?', flags=re.IGNORECASE)
def handler_time_s(dt_s: str) -> int: def handler_time(dt_s):
return get_s_since_epoch(dateparser_parse_dt(dt_s)) return dateparser_parse_dt(dt_s)
def handler_time_ms(dt_s: str) -> int: def handler_time_now_local():
return get_ms_since_epoch(dateparser_parse_dt(dt_s)) return get_local_now()
def handler_time_s_now_local() -> int: def handler_time_now_utc():
return get_s_since_epoch(get_local_now()) return get_utcnow()
def handler_time_ms_now_local() -> int: def dt_normalize(start_dt, end_dt) -> (datetime, datetime):
return get_ms_since_epoch(get_local_now()) if type(start_dt) is str:
start_dt = dateparser_parse_dt(start_dt)
if type(end_dt) is str:
end_dt = dateparser_parse_dt(end_dt)
return start_dt, end_dt
def handler_time_s_now_utc() -> int: def handler_time_diff(start_dt, end_dt) -> dict:
return get_s_since_epoch(get_utcnow()) start_dt, end_dt = dt_normalize(start_dt, end_dt)
diff = start_dt - end_dt
def handler_time_ms_now_utc() -> int:
return get_ms_since_epoch(get_utcnow())
def handler_time_diff(start_dt: datetime, end_dt: datetime) -> dict:
diff = end_dt - start_dt
return dict(start=dt_pretty(start_dt), return dict(start=dt_pretty(start_dt),
end=dt_pretty(end_dt), end=dt_pretty(end_dt),
diff=td_pretty(diff)) diff=td_pretty(diff))
def handler_time_since_until(start_dt_s: str) -> dict: def handler_time_since_until(start_dt_s: str) -> dict:
return handler_time_diff(dateparser_parse_dt(start_dt_s), get_local_now()) return handler_time_diff(start_dt_s, get_local_now())
def handler_workdays_diff(start_dt: datetime, end_dt: datetime) -> dict: def handler_workdays_diff(start_dt, end_dt) -> dict:
start_dt, end_dt = dt_normalize(start_dt, end_dt)
diff = workday_diff(start_dt, end_dt) diff = workday_diff(start_dt, end_dt)
return dict(start=dt_pretty(start_dt), return dict(start=dt_pretty(start_dt),
end=dt_pretty(end_dt), end=dt_pretty(end_dt),
@ -79,7 +81,8 @@ def handler_workdays_since_until(start_dt_s: str) -> dict:
return handler_workdays_diff(dateparser_parse_dt(start_dt_s), get_local_now()) return handler_workdays_diff(dateparser_parse_dt(start_dt_s), get_local_now())
def handler_workhours_diff(start_dt: datetime, end_dt: datetime) -> dict: def handler_workhours_diff(start_dt, end_dt) -> dict:
start_dt, end_dt = dt_normalize(start_dt, end_dt)
diff = workhours_diff(start_dt, end_dt) diff = workhours_diff(start_dt, end_dt)
return dict(start=dt_pretty(start_dt), return dict(start=dt_pretty(start_dt),
end=dt_pretty(end_dt), end=dt_pretty(end_dt),
@ -91,6 +94,8 @@ def handler_workhours_since_until(start_dt_s: str) -> dict:
def handler_timezone_translation(dt_s: str, timezone_like_s: str) -> dict: def handler_timezone_translation(dt_s: str, timezone_like_s: str) -> dict:
if dt_s.lower().strip() == "time":
dt_s = "now"
src_dt = dateparser_parse_dt(dt_s) src_dt = dateparser_parse_dt(dt_s)
tz = resolve_timezone(timezone_like_s) tz = resolve_timezone(timezone_like_s)
if not tz: if not tz:
@ -102,7 +107,7 @@ def handler_timezone_translation(dt_s: str, timezone_like_s: str) -> dict:
def handler_generic_parser(dt_s: str) -> datetime: def handler_generic_parser(dt_s: str) -> datetime:
return query_to_format_result(dt_s, None) return dateparser_parse_dt(dt_s)
def handler_time_in_parser(dt_s: str) -> datetime: def handler_time_in_parser(dt_s: str) -> datetime:
@ -118,29 +123,38 @@ QUERY_TYPE_DT = "datetime_details"
QUERY_TYPE_TZ = "timezone" QUERY_TYPE_TZ = "timezone"
QUERY_TYPE_TD = "timedelta" QUERY_TYPE_TD = "timedelta"
h_default = ''
h_unix_s = 'dt->unix_s'
h_unix_ms = 'dt->unix_ms'
h_tz_offset = 'tz->tz_offset'
h_time_in = 'dt->hh:mm'
h_translation = 'dst_dt->iso8601_full'
h_default_dt = 'dt->iso8601_full'
h_default_td = 'diff->duration_iso8601'
regex_handlers = [ regex_handlers = [
(r_time_in_epoch_s_now, handler_time_s_now_local, QUERY_TYPE_DT), (r_time_in_epoch_s_now, handler_time_now_local, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_s_now, handler_time_s_now_utc, QUERY_TYPE_DT), (r_time_in_epoch_s_now, handler_time_now_utc, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_s2, handler_time_s, QUERY_TYPE_DT), (r_time_in_epoch_s2, handler_time, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_s3, handler_time_s, QUERY_TYPE_DT), (r_time_in_epoch_s3, handler_time, QUERY_TYPE_DT, h_unix_s),
(r_time_in_epoch_ms_now, handler_time_ms_now_local, QUERY_TYPE_DT), (r_time_in_epoch_ms_now, handler_time_now_local, QUERY_TYPE_DT, h_unix_ms),
(r_time_in_epoch_ms_now, handler_time_ms_now_utc, QUERY_TYPE_DT), (r_time_in_epoch_ms_now, handler_time_now_utc, QUERY_TYPE_DT, h_unix_ms),
(r_time_in_epoch_ms2, handler_time_ms, QUERY_TYPE_DT), (r_time_in_epoch_ms2, handler_time, QUERY_TYPE_DT, h_unix_ms),
(r_time_in_epoch_ms3, handler_time_ms, QUERY_TYPE_DT), (r_time_in_epoch_ms3, handler_time, QUERY_TYPE_DT, h_unix_ms),
(r_timezone_translation, handler_timezone_translation, QUERY_TYPE_DT_TR), (r_timezone_translation, handler_timezone_translation, QUERY_TYPE_DT_TR, h_translation),
(r_time_since, handler_time_since_until, QUERY_TYPE_TD), (r_time_since, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
(r_time_until, handler_time_since_until, QUERY_TYPE_TD), (r_time_until, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
(r_time_between, handler_time_diff, QUERY_TYPE_TD), (r_time_between, handler_time_diff, QUERY_TYPE_TD, h_default_td),
(r_workdays_since, handler_workdays_since_until, QUERY_TYPE_TD), (r_workdays_since, handler_workdays_since_until, QUERY_TYPE_TD, h_default_td),
(r_workdays_until, handler_workdays_since_until, QUERY_TYPE_TD), (r_workdays_until, handler_workdays_since_until, QUERY_TYPE_TD, h_default_td),
(r_workdays_between, handler_workdays_diff, QUERY_TYPE_TD), (r_workdays_between, handler_workdays_diff, QUERY_TYPE_TD, h_default_td),
(r_workhours_since, handler_workhours_since_until, QUERY_TYPE_TD), (r_workhours_since, handler_workhours_since_until, QUERY_TYPE_TD, h_default_td),
(r_workhours_until, handler_workhours_since_until, QUERY_TYPE_TD), (r_workhours_until, handler_workhours_since_until, QUERY_TYPE_TD, h_default_td),
(r_workhours_between, handler_workhours_diff, QUERY_TYPE_TD), (r_workhours_between, handler_workhours_diff, QUERY_TYPE_TD, h_default_td),
(r_time_in, handler_time_in_parser, QUERY_TYPE_DT), (r_time_in, handler_time_in_parser, QUERY_TYPE_DT, h_time_in),
(r_timezone, handler_timezone, QUERY_TYPE_TZ), (r_timezone, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
(r_timezone_2, handler_timezone, QUERY_TYPE_TZ), (r_timezone_2, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
(r_generic, handler_generic_parser, QUERY_TYPE_DT), (r_generic, handler_generic_parser, QUERY_TYPE_DT, h_default_dt),
] ]
@ -156,7 +170,7 @@ def try_regex(r, s):
def tokenize(s): def tokenize(s):
solutions = [] solutions = []
for r, h, t in regex_handlers: for r, h, t, hi in regex_handlers:
g = try_regex(r, s) g = try_regex(r, s)
if g is not None: if g is not None:
try: try:
@ -164,7 +178,7 @@ def tokenize(s):
except Exception as e: except Exception as e:
continue continue
if result is not None: if result is not None:
solutions.append((h.__name__, result, t)) solutions.append((h.__name__, result, t, hi))
return solutions return solutions
@ -174,6 +188,20 @@ def pretty_print_dict(obj):
print(colorful_json) print(colorful_json)
def show_magic_results(obj, args):
for solution in obj['solutions']:
entry_proxy = Cut(solution, sep='->')
highlight_entry = solution["highlight"]
try:
highlight_result = entry_proxy[highlight_entry]
except Exception as e:
continue
if args.handlers:
print("{} -> {}".format(solution['handler'], highlight_result))
else:
print(highlight_result)
def dt_pretty(dt): def dt_pretty(dt):
rv = {} rv = {}
global custom_locale global custom_locale
@ -200,7 +228,7 @@ def dt_pretty(dt):
def td_pretty(td): def td_pretty(td):
rv = { rv = {
"sign": '-' if td.days < 0 else '+', "sign": '-' if td.days > 0 else '+',
"in_the": 'future' if td.days < 0 else 'past', "in_the": 'future' if td.days < 0 else 'past',
"time_ago": time_ago(td), "time_ago": time_ago(td),
"duration_iso8601": td_iso8601(td), "duration_iso8601": td_iso8601(td),
@ -214,7 +242,7 @@ def resolve_query_type(query):
solutions = tokenize(query) solutions = tokenize(query)
if not solutions: if not solutions:
dt = get_local_now() dt = get_local_now()
return [["now", dt, QUERY_TYPE_DT]] return [["now", dt, QUERY_TYPE_DT, h_default]]
return solutions return solutions
@ -226,9 +254,10 @@ def resolve_query(query):
solutions = resolve_query_type(query) solutions = resolve_query_type(query)
for solution in solutions: for solution in solutions:
element = {} element = {}
handler, results, query_type = solution handler, results, query_type, hi = solution
element["handler"] = handler element["handler"] = handler
element["query_type"] = query_type element["query_type"] = query_type
element["highlight"] = hi
try: try:
if query_type == QUERY_TYPE_DT: if query_type == QUERY_TYPE_DT:
element["dt"] = dt_pretty(results) element["dt"] = dt_pretty(results)
@ -241,8 +270,8 @@ def resolve_query(query):
elif query_type == QUERY_TYPE_TD: elif query_type == QUERY_TYPE_TD:
element["timedelta"] = results element["timedelta"] = results
rv["solutions"].append(element) rv["solutions"].append(element)
except Exception: except Exception as e:
... continue
return rv return rv
@ -273,10 +302,27 @@ def test():
print("{} -> {}".format(s, resolve_query(s))) print("{} -> {}".format(s, resolve_query(s)))
if __name__ == "__main__": def parse_args():
custom_locale = "" parser = argparse.ArgumentParser()
custom_locale = resolve_locale(custom_locale) parser.add_argument('query', nargs='*', default="now", help="freeform")
query = ' '.join(sys.argv[1:]) parser.add_argument('--locale', dest='locale')
# query = "workhours until 2/12/2020 12:00" parser.add_argument('--handlers', dest='handlers', action='store_true')
parser.add_argument('--full', dest='full', action='store_true')
args = parser.parse_args()
return args
def main(args):
global custom_locale
custom_locale = resolve_locale(args.locale)
query = ' '.join(args.query)
query = "time in sofia"
result = resolve_query(query) result = resolve_query(query)
pretty_print_dict(result) if args.full:
pretty_print_dict(result)
show_magic_results(result, args)
if __name__ == "__main__":
args = parse_args()
main(args)

View File

@ -4,9 +4,11 @@ Find time now, in the past or future in any timezone or location.
""" """
import argparse import argparse
import csv
import datetime import datetime
import logging import logging
import os import os
import re
from collections import defaultdict from collections import defaultdict
import dateparser import dateparser
@ -18,6 +20,7 @@ from datetimerange import DateTimeRange
from dateutil.parser import parse as dutil_parse from dateutil.parser import parse as dutil_parse
from dateparser.timezone_parser import StaticTzInfo from dateparser.timezone_parser import StaticTzInfo
from dateutil.tz import gettz, tzlocal from dateutil.tz import gettz, tzlocal
from fuzzywuzzy import fuzz
from pytz import timezone from pytz import timezone
from pytz.exceptions import UnknownTimeZoneError from pytz.exceptions import UnknownTimeZoneError
@ -134,8 +137,6 @@ def create_if_not_exists(fname):
def write_to_cache(query, location): def write_to_cache(query, location):
import csv
logger.debug("Writing location to cache") logger.debug("Writing location to cache")
with open(os.path.join(basepath, "data", ".cache.csv"), 'a+') as wf: with open(os.path.join(basepath, "data", ".cache.csv"), 'a+') as wf:
cachewriter = csv.writer(wf) cachewriter = csv.writer(wf)
@ -156,9 +157,7 @@ def resolve_location_local(query):
""" """
Find a location by searching in local db of countries and cities Find a location by searching in local db of countries and cities
""" """
import csv
from heapq import heappush, heappop from heapq import heappush, heappop
from fuzzywuzzy import fuzz
query = query.lower() query = query.lower()
create_if_not_exists(os.path.join(basepath, "data", ".cache.csv")) create_if_not_exists(os.path.join(basepath, "data", ".cache.csv"))
@ -473,10 +472,113 @@ def tzinfo_from_offset(offset: str) -> pytz.timezone:
return None, [] return None, []
def dateparser_parse_dt(s: str): def custom_dt_parse(query):
# print("Dateparser query: {}".format(s)) with open(os.path.join(basepath, "data", "custom_dt.csv")) as f:
cfile = csv.reader(f)
for row in cfile:
entry = row[0]
fuzz_query = query.lower().strip()
fuzz_ratio = fuzz.ratio(fuzz_query, entry)
if fuzz_ratio >= 95:
return row[1]
return query
r_next = re.compile('(?:next)?\s*(.*)', flags=re.IGNORECASE)
r_prev = re.compile('(?:last|prev|previous)?\s*(.*)', flags=re.IGNORECASE)
r_this = re.compile('(?:this|that)?\s*(.*)', flags=re.IGNORECASE)
def get_local_now_parsed(s):
now = get_local_now()
now = now.replace(hour=0, minute=0, second=0, microsecond=0)
parsed = parse_dt(s)
parsed = parsed.replace(tzinfo=now.tzinfo)
return now, parsed
def get_week_start_end(dt):
start = dt - timedelta(days=dt.weekday())
end = start + timedelta(days=6)
return start, end
def handler_next_weekday(s):
now, parsed = get_local_now_parsed(s)
week_start, week_end = get_week_start_end(now)
if parsed > week_end:
# parsed is in next week
return str(parsed)
if week_start <= parsed <= week_end:
# parsed is in this week
return str(parsed + timedelta(days=7))
else:
# parsed is in previous week
return str(parsed + timedelta(days=14))
def handler_prev_weekday(s):
now, parsed = get_local_now_parsed(s)
week_start, week_end = get_week_start_end(now)
if parsed > week_end:
# parsed is in next week
return str(parsed - timedelta(days=14))
if week_start <= parsed <= week_end:
# parsed is in this week
return str(parsed - timedelta(days=7))
else:
# parsed is in previous week
return str(parsed)
def handler_this_weekday(s):
now, parsed = get_local_now_parsed(s)
week_start, week_end = get_week_start_end(now)
if parsed > week_end:
# parsed is in next week
return str(parsed - timedelta(days=7))
if week_start <= parsed <= week_end:
# parsed is in this week
return str(parsed)
else:
# parsed is in previous week
return str(parsed + timedelta(days=7))
def try_regex(r, s):
try:
m = re.match(r, s)
except:
return None
if m:
groups = m.groups()
return groups
regex_handlers = [
(r_next, handler_next_weekday),
(r_prev, handler_prev_weekday),
(r_this, handler_this_weekday),
]
def regex_parsers(s):
for r, h in regex_handlers:
g = try_regex(r, s)
if g is not None:
try:
result = h(*g)
except Exception as e:
continue
if result is not None:
return result
return s
def dateparser_parse_dt(s: str):
s = custom_dt_parse(s)
s = regex_parsers(s)
parsed = parse_dt(s) parsed = parse_dt(s)
# print("Dateparser parsed query: {}".format(parsed))
if not parsed: if not parsed:
parsed = dutil_parse(s) parsed = dutil_parse(s)
if not parsed: if not parsed:
@ -643,11 +745,11 @@ def workhours_diff(start, end, workhour_begin="09:00", workhour_end="17:00", wor
def td_remainders(td): def td_remainders(td):
# split seconds to larger units # split seconds to larger units
seconds = td.total_seconds() seconds = td.total_seconds()
minutes, seconds = divmod(seconds, 60) minutes, seconds = divmod(abs(int(seconds)), 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(abs(int(minutes)), 60)
days, hours = divmod(hours, 24) days, hours = divmod(abs(int(hours)), 24)
months, days = divmod(days, 30.42) months, days = divmod(abs(int(days)), 30.42)
years, months = divmod(months, 12) years, months = divmod(abs(int(months)), 12)
years, months, days, hours, minutes, seconds = map(int, (years, months, days, hours, minutes, seconds)) years, months, days, hours, minutes, seconds = map(int, (years, months, days, hours, minutes, seconds))
years, months, days, hours, minutes, seconds = map(abs, (years, months, days, hours, minutes, seconds)) years, months, days, hours, minutes, seconds = map(abs, (years, months, days, hours, minutes, seconds))
return dict( return dict(
@ -662,12 +764,12 @@ def td_remainders(td):
def td_totals(td): def td_totals(td):
seconds = td.total_seconds() seconds = td.total_seconds()
minutes = seconds // 60 minutes = seconds / 60
hours = seconds // (60 * 60) hours = seconds / (60 * 60)
days = seconds // (24 * 60 * 60) days = seconds / (24 * 60 * 60)
weeks = seconds // (7 * 24 * 60 * 60) weeks = seconds / (7 * 24 * 60 * 60)
months = seconds // (30 * 24 * 60 * 60) months = seconds / (30 * 24 * 60 * 60)
years = seconds // (365 * 24 * 60 * 60) years = seconds / (365 * 24 * 60 * 60)
years, months, weeks, days, hours, minutes, seconds = map(abs, years, months, weeks, days, hours, minutes, seconds = map(abs,
(years, months, weeks, days, hours, minutes, seconds)) (years, months, weeks, days, hours, minutes, seconds))
return dict( return dict(
@ -694,4 +796,4 @@ def td_iso8601(td):
for short, timeframe in hms: for short, timeframe in hms:
if rem[timeframe]: if rem[timeframe]:
fmt += "{}{}".format(rem[timeframe], short) fmt += "{}{}".format(rem[timeframe], short)
return fmt return fmt