From 29d0d36bde08c4e5e83577045c5e097e56c9b322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= Date: Mon, 25 Dec 2023 23:19:20 +0300 Subject: [PATCH] Add timewarrior config --- .config/timewarrior/extensions/totals.py | 192 ++++++++++++++++++++ .config/timewarrior/holidays/README.md | 35 ++++ .config/timewarrior/holidays/holidays.tr-TR | 34 ++++ .config/timewarrior/holidays/refresh | 144 +++++++++++++++ .config/timewarrior/timewarrior.cfg | 0 5 files changed, 405 insertions(+) create mode 100755 .config/timewarrior/extensions/totals.py create mode 100644 .config/timewarrior/holidays/README.md create mode 100644 .config/timewarrior/holidays/holidays.tr-TR create mode 100755 .config/timewarrior/holidays/refresh create mode 100644 .config/timewarrior/timewarrior.cfg diff --git a/.config/timewarrior/extensions/totals.py b/.config/timewarrior/extensions/totals.py new file mode 100755 index 0000000..5675150 --- /dev/null +++ b/.config/timewarrior/extensions/totals.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +############################################################################### +# +# Copyright 2016 - 2023, Thomas Lauf, Paul Beckingham, Federico Hernandez. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# https://www.opensource.org/licenses/mit-license.php +# +############################################################################### + +import datetime +import json +import sys + +from dateutil import tz + +DATEFORMAT = "%Y%m%dT%H%M%SZ" + + +def format_seconds(seconds): + """Convert seconds to a formatted string + + Convert seconds: 3661 + To formatted: " 1:01:01" + """ + hours = seconds // 3600 + minutes = seconds % 3600 // 60 + seconds = seconds % 60 + return "{:4d}:{:02d}:{:02d}".format(hours, minutes, seconds) + + +def calculate_totals(input_stream): + from_zone = tz.tzutc() + to_zone = tz.tzlocal() + + # Extract the configuration settings. + header = 1 + configuration = dict() + body = "" + + for line in input_stream: + if header: + if line == "\n": + header = 0 + else: + fields = line.strip().split(": ", 2) + if len(fields) == 2: + configuration[fields[0]] = fields[1] + else: + configuration[fields[0]] = "" + else: + body += line + + j = json.loads(body) + + if "temp.report.start" in configuration: + report_start_utc = datetime.datetime.strptime(configuration["temp.report.start"], DATEFORMAT) + report_start_utc = report_start_utc.replace(tzinfo=from_zone) + report_start = report_start_utc.astimezone(tz=to_zone) + else: + report_start_utc = None + report_start = None + + if "temp.report.end" in configuration: + report_end_utc = datetime.datetime.strptime(configuration["temp.report.end"], DATEFORMAT) + report_end_utc = report_end_utc.replace(tzinfo=from_zone) + report_end = report_end_utc.astimezone(tz=to_zone) + else: + report_end_utc = None + report_end = None + + if len(j) == 0: + if report_start is not None and report_end is not None: + return ["No data in the range {:%Y-%m-%d %H:%M:%S} - {:%Y-%m-%d %H:%M:%S}".format(report_start, report_end)] + elif report_start is None and report_end is not None: + return ["No data in the range until {:%Y-%m-%d %H:%M:%S}".format(report_end)] + elif report_start is not None and report_end is None: + return ["No data in the range since {:%Y-%m-%d %H:%M:%S}".format(report_start)] + else: + return ["No data to display"] + + if "start" in j[0]: + if report_start_utc is not None: + j[0]["start"] = max(report_start_utc, datetime.datetime.strptime(j[0]["start"], DATEFORMAT).replace(tzinfo=from_zone)).strftime(DATEFORMAT) + else: + report_start_utc = datetime.datetime.strptime(j[0]["start"], DATEFORMAT).replace(tzinfo=from_zone) + report_start = report_start_utc.astimezone(tz=to_zone) + else: + return ["Cannot display an past open range"] + + if "end" in j[-1]: + if report_end_utc is not None: + j[-1]["end"] = min(report_end_utc, datetime.datetime.strptime(j[-1]["end"], DATEFORMAT).replace(tzinfo=from_zone)).strftime(DATEFORMAT) + else: + report_end_utc = datetime.datetime.strptime(j[-1]["end"], DATEFORMAT).replace(tzinfo=from_zone) + report_end = report_end_utc.astimezone(tz=to_zone) + else: + if report_end_utc is not None: + j[-1]["end"] = min(report_end_utc, datetime.datetime.now(tz=from_zone)).strftime(DATEFORMAT) + else: + j[-1]["end"] = datetime.datetime.now(tz=from_zone).strftime(DATEFORMAT) + report_end = datetime.datetime.now(tz=to_zone) + + # Sum the seconds tracked by tag. + totals = dict() + untagged = None + + for object in j: + start = datetime.datetime.strptime(object["start"], DATEFORMAT).replace(tzinfo=from_zone) + end = datetime.datetime.strptime(object["end"], DATEFORMAT).replace(tzinfo=from_zone) + + tracked = end - start + + if "tags" not in object or object["tags"] == []: + if untagged is None: + untagged = tracked + else: + untagged += tracked + else: + for tag in object["tags"]: + if tag in totals: + totals[tag] += tracked + else: + totals[tag] = tracked + + # Determine largest tag width. + max_width = len("Total") + for tag in totals: + if len(tag) > max_width: + max_width = len(tag) + + # Compose report header. + output = [ + "", + "Total by Tag, for {:%Y-%m-%d %H:%M:%S} - {:%Y-%m-%d %H:%M:%S}".format(report_start, report_end), + "" + ] + + # Compose table header. + if configuration["color"] == "on": + output.append("{:{width}} {:>10}".format("Tag", "Total", width=max_width)) + else: + output.append("{:{width}} {:>10}".format("Tag", "Total", width=max_width)) + output.append("{} {}".format("-" * max_width, "----------")) + + # Compose table rows. + grand_total = 0 + for tag in sorted(totals): + seconds = int(totals[tag].total_seconds()) + formatted = format_seconds(seconds) + grand_total += seconds + output.append("{:{width}} {:10}".format(tag, formatted, width=max_width)) + + if untagged is not None: + seconds = int(untagged.total_seconds()) + formatted = format_seconds(seconds) + grand_total += seconds + output.append("{:{width}} {:10}".format("", formatted, width=max_width)) + + # Compose total. + if configuration["color"] == "on": + output.append("{} {}".format(" " * max_width, " ")) + else: + output.append("{} {}".format(" " * max_width, "----------")) + + output.append("{:{width}} {:10}".format("Total", format_seconds(grand_total), width=max_width)) + output.append("") + + return output + + +if __name__ == "__main__": + for line in calculate_totals(sys.stdin): + print(line) diff --git a/.config/timewarrior/holidays/README.md b/.config/timewarrior/holidays/README.md new file mode 100644 index 0000000..4afc2d7 --- /dev/null +++ b/.config/timewarrior/holidays/README.md @@ -0,0 +1,35 @@ +# Timewarrior Holiday Files + +The holiday files were created by the `refresh` script using data from [holidata.net](https://holidata.net). +They can be updated using the following command: + +```shell +$ ./refresh +``` + +This updates all present holiday files with holiday data for the current and the following year (default). + +If you need another locale (for example `sv-SE`), do this: + +```shell +$ ./refresh --locale sv-SE +``` + +This creates a file `holidays.sv-SE` containing holiday data for the current and following year. +The id for the locale is composed of the [ISO 639-1 language code](https://en.wikipedia.org/wiki/ISO_639-1) and the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). + +If you need a specific locale region, do this: + +```shell +$ ./refresh --locale de-CH --region BE +``` + +For regions use the corresponding [ISO 3166-2 code for principal subdivisions](https://en.wikipedia.org/wiki/ISO_3166-2). + +To specify a set of years to update, do this: + +```shell +$ ./refresh --locale en-US --year 2020 2021 2022 +``` + +If the locale is not yet supported by [holidata.net](https://holidata.net), or there is no data available for the requested year, you will see an error. diff --git a/.config/timewarrior/holidays/holidays.tr-TR b/.config/timewarrior/holidays/holidays.tr-TR new file mode 100644 index 0000000..fc2e7d9 --- /dev/null +++ b/.config/timewarrior/holidays/holidays.tr-TR @@ -0,0 +1,34 @@ +# Holiday data provided by holidata.net +# Generated 2023-12-25T12:05:27 + +define holidays: + tr-TR: + 2023_01_01 = Yılbaşı + 2023_04_21 = Ramazan Bayramı (1. Gün) + 2023_04_22 = Ramazan Bayramı (2. Gün) + 2023_04_23 = Ulusal Egemenlik ve Çocuk Bayramı + 2023_05_01 = Emek ve Dayanışma Günü + 2023_05_19 = Atatürk'ü Anma, Gençlik ve Spor Bayramı + 2023_06_28 = Kurban Bayramı (1. Gün) + 2023_06_29 = Kurban Bayramı (2. Gün) + 2023_06_30 = Kurban Bayramı (3. Gün) + 2023_07_01 = Kurban Bayramı (4. Gün) + 2023_07_15 = Demokrasi ve Milli Birlik Günü + 2023_08_30 = Zafer Bayramı + 2023_10_29 = Cumhuriyet Bayramı + + 2024_01_01 = Yılbaşı + 2024_04_10 = Ramazan Bayramı (1. Gün) + 2024_04_11 = Ramazan Bayramı (2. Gün) + 2024_04_12 = Ramazan Bayramı (3. Gün) + 2024_04_23 = Ulusal Egemenlik ve Çocuk Bayramı + 2024_05_01 = Emek ve Dayanışma Günü + 2024_05_19 = Atatürk'ü Anma, Gençlik ve Spor Bayramı + 2024_06_16 = Kurban Bayramı (1. Gün) + 2024_06_17 = Kurban Bayramı (2. Gün) + 2024_06_18 = Kurban Bayramı (3. Gün) + 2024_06_19 = Kurban Bayramı (4. Gün) + 2024_07_15 = Demokrasi ve Milli Birlik Günü + 2024_08_30 = Zafer Bayramı + 2024_10_29 = Cumhuriyet Bayramı + diff --git a/.config/timewarrior/holidays/refresh b/.config/timewarrior/holidays/refresh new file mode 100755 index 0000000..a53e577 --- /dev/null +++ b/.config/timewarrior/holidays/refresh @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +############################################################################### +# +# Copyright 2016, 2018 - 2022, Gothenburg Bit Factory +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# https://www.opensource.org/licenses/mit-license.php +# +############################################################################### + +import argparse +import datetime +import json +import os +import re +from textwrap import dedent +from urllib.error import HTTPError +from urllib.request import urlopen + + +def gather_locale_files(path): + """Enumerate all holiday files in the current directory.""" + + locale_file_map = {} + re_holiday_file = re.compile(r"/holidays.([a-z]{2}-[A-Z]{2})$") + + for file in enumerate(path): + result = re_holiday_file.search(file) + if result: + # Extract the locale name. + locale_file_map[result.group(1)] = file + + return locale_file_map + + +def enumerate(path): + if not os.path.exists(path): + raise Exception(f"Directory '{path}' does not exist") + + found = [] + + for path, dirs, files in os.walk(path, topdown=True, onerror=None, followlinks=False): + found.extend([os.path.join(path, x) for x in files]) + + return found + + +def create_locale_files(path, locales): + locale_file_map = {} + + for locale in locales: + locale_file_map[locale] = os.path.join(path, f"holidays.{locale}") + + return locale_file_map + + +def update_locale_files(locales, regions, years): + now = datetime.datetime.now() + + if not years: + years = [now.year, now.year + 1] + + for locale, file in locales.items(): + with open(file, "w") as fh: + fh.write(dedent(f"""\ + # Holiday data provided by holidata.net + # Generated {now:%Y-%m-%dT%H:%M:%S} + + define holidays: + {locale}: + """)) + + for year in years: + try: + holidays = get_holidata(locale, regions, year) + + for date, desc in holidays.items(): + fh.write(f" {date} = {desc}\n") + + fh.write("\n") + + except HTTPError as e: + if e.code == 404: + print(f"holidata.net does not have data for {locale}, for {year}.") + else: + print(e.code, e.read()) + + +def get_holidata(locale, regions, year): + url = f"https://holidata.net/{locale}/{year}.json" + print(url) + holidays = dict() + lines = urlopen(url).read().decode("utf-8") + + for line in lines.split("\n"): + if line: + j = json.loads(line) + + if not j["region"] or not regions or j["region"] in regions: + day = j["date"].replace("-", "_") + desc = j["description"] + holidays[day] = desc + + return holidays + + +def main(args): + locale_files = create_locale_files(args.path, args.locale) if args.locale else gather_locale_files(args.path) + update_locale_files(locale_files, args.region, args.year) + + +if __name__ == "__main__": + usage = """See https://holidata.net for details of supported locales and regions.""" + parser = argparse.ArgumentParser( + description="Update holiday data files. Simply run 'refresh' to update all of them.", + usage="refresh [-h] [path] [--locale LOCALE [LOCALE ...]] [--region REGION [REGION ...]] [--year YEAR [YEAR ...]]" + ) + parser.add_argument("--locale", nargs="+", help="specify locale to update") + parser.add_argument("--region", nargs="+", help="specify locale region to update", default=[]) + parser.add_argument("--year", nargs="+", help="specify year to fetch (defaults to current and next year)", type=int, default=[]) + parser.add_argument("path", nargs="?", help="base path to search for locales (defaults to current directory)", default=".") + + try: + main(parser.parse_args()) + except Exception as msg: + print("Error:", msg) diff --git a/.config/timewarrior/timewarrior.cfg b/.config/timewarrior/timewarrior.cfg new file mode 100644 index 0000000..e69de29