diff options
Diffstat (limited to 'sjdbmk')
-rw-r--r-- | sjdbmk/cal.py | 19 | ||||
-rw-r--r-- | sjdbmk/daily.py | 259 | ||||
-rwxr-xr-x | sjdbmk/pack.py | 80 | ||||
-rw-r--r-- | sjdbmk/twa.py | 19 |
4 files changed, 377 insertions, 0 deletions
diff --git a/sjdbmk/cal.py b/sjdbmk/cal.py index 92f1044..0d57711 100644 --- a/sjdbmk/cal.py +++ b/sjdbmk/cal.py @@ -1,3 +1,22 @@ +#!/usr/bin/env python3 +# +# Calendar Interpretation in the Songjiang Daily Bulletin Build System +# Copyright (C) 2024 Runxi Yu <https://runxiyu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + from typing import Any import datetime diff --git a/sjdbmk/daily.py b/sjdbmk/daily.py new file mode 100644 index 0000000..9bf7529 --- /dev/null +++ b/sjdbmk/daily.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# +# Daily script to prepare the YK Pao School Daily Bulletin's JSON data files +# Copyright (C) 2024 Runxi Yu <https://runxiyu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +from __future__ import annotations +from configparser import ConfigParser +import json +import argparse +import logging +import datetime +import zoneinfo +import os +import base64 +import mimetypes +import typing + +# import legacy_wikipedia + +logger = logging.getLogger(__name__) + +DAYNAMES = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + "Monday", +] +DAYNAMES_CHINESE = ["周一", "周二", "周三", "周四", "周五", "周六", "周日", "周一"] +DAYNAMES_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"] + + +def main() -> None: + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser(description="Daily script for the Daily Bulletin") + parser.add_argument( + "--date", + default=None, + help="the day to generate for, in local time, in YYYY-MM-DD; defaults to tomorrow", + # TODO: Verify validity of date + # TODO: Verify consistency of date elsewhere + ) + parser.add_argument("--config", default="config.ini", help="path to the configuration file") + args = parser.parse_args() + + if args.date: + datetime_target_naive = datetime.datetime.strptime(args.date, "%Y-%m-%d") + else: + datetime_target_naive = None + del args.date + + config = ConfigParser() + config.read(args.config) + + tzinfo = zoneinfo.ZoneInfo(config["general"]["timezone"]) + if datetime_target_naive: + datetime_target_aware = datetime_target_naive.replace(tzinfo=tzinfo) + else: + datetime_current_aware = datetime.datetime.now(tz=tzinfo) + datetime_target_aware = datetime_current_aware + datetime.timedelta(days=1) + del datetime_current_aware + del datetime_target_naive + logger.info("Generating for %s" % datetime_target_aware.strftime("%Y-%m-%d %Z")) + + build_path = config["general"]["build_path"] + os.chdir(build_path) + + cycle_data_path = config["general"]["cycle_data"] + with open(cycle_data_path, "r", encoding="utf-8") as cycle_data_file: + cycle_data = json.load(cycle_data_file) + + the_week_ahead_url = config["the_week_ahead"]["file_url"] + + generate( + datetime_target_aware, + cycle_data=cycle_data, + the_week_ahead_url=the_week_ahead_url, + ) + + +def generate( + datetime_target: datetime.datetime, + the_week_ahead_url: str, + cycle_data: dict[str, str], +) -> str: + weekday_enum = datetime_target.weekday() + weekday_en = DAYNAMES[weekday_enum] + weekday_zh = DAYNAMES_CHINESE[weekday_enum] + weekdays_short = DAYNAMES_SHORT[weekday_enum:] + # next_weekday_short = DAYNAMES_SHORT[weekday_enum + 1] + try: + day_of_cycle = cycle_data[datetime_target.strftime("%Y-%m-%d")] + except KeyError: + day_of_cycle = "SA" + logger.warning('Cycle day not found, using "SA"') + + for days_since_beginning in range(0, 5): + week_start_date = datetime_target - datetime.timedelta(days=days_since_beginning) + try: + with open( + "week-%s.json" % week_start_date.strftime("%Y%m%d"), + "r", + encoding="utf-8", + ) as week_file: + week_data = json.load(week_file) + except FileNotFoundError: + continue + else: + break + else: + raise FileNotFoundError("Cannot find a week-{date}.json file without five prior days") + + try: + aod = week_data["aods"][days_since_beginning] + except IndexError: + logger.warning("AOD not found") + aod = "None" + + breakfast_today = week_data["menu"]["breakfast"][days_since_beginning] + lunch_today = week_data["menu"]["lunch"][days_since_beginning] + dinner_today = week_data["menu"]["dinner"][days_since_beginning] + try: + breakfast_tomorrow = week_data["menu"]["breakfast"][days_since_beginning + 1] + except IndexError: + breakfast_tomorrow = None + try: + snack_morning = week_data["snacks"][0][days_since_beginning] + except (KeyError, IndexError): + snack_morning = None + try: + snack_afternoon = week_data["snacks"][1][days_since_beginning] + except (KeyError, IndexError): + snack_afternoon = None + try: + snack_evening = week_data["snacks"][2][days_since_beginning] + except (KeyError, IndexError): + snack_evening = None + + logger.info("Checking for inspirations") + # TODO: Should probably allow inspirations to be reused on the same day + # e.g. "used" should be set to the date it was used on + for inspfn in os.listdir(): + if not inspfn.startswith("inspire-"): + continue + with open(inspfn, "r", encoding="utf-8") as inspfd: + inspjq = json.load(inspfd) + if (not inspjq["approved"]) or inspjq["used"]: + continue + inspjq["used"] = True + with open(inspfn, "w", encoding="utf-8") as inspfd: + json.dump(inspjq, inspfd, indent="\t") + inspiration_type = inspjq["type"] + if inspiration_type not in ["text", "media", "canteen"]: + logger.warning("Inspiration type for %s invalid, skipping" % inspfn) + continue + inspiration_origin = inspjq["origin"] + inspiration_shared_by = inspjq["uname"] + inspiration_text = inspjq["text"] + inspiration_image_fn = inspjq["file"] + if inspiration_image_fn: + logger.info("Inspiration has attachment %s" % inspiration_image_fn) + inspiration_image_mime, inspiration_image_extra_encoding = mimetypes.guess_type(inspiration_image_fn) + assert not inspiration_image_extra_encoding + with open("inspattach-%s" % os.path.basename(inspiration_image_fn), "rb") as ifd: + inspiration_image_data = base64.b64encode(ifd.read()).decode("ascii") + else: + inspiration_image_data = None + inspiration_image_mime = None + break + else: + inspiration_image_data = None + inspiration_image_mime = None + inspiration_type = None + inspiration_origin = None + inspiration_shared_by = None + inspiration_text = None + inspiration_image_fn = None + + logger.info("Finished processing inspirations") + logger.info("Starting On This Day") + + on_this_day_html_en = "" + on_this_day_html_zh = "" + # on_this_day_html_en: typing.Optional[str] + # try: + # with open("otd_en-%s.html" % datetime_target.strftime("%m-%d"), "r") as fd: + # on_this_day_html_en = fd.read() + # except FileNotFoundError: + # logger.warning("On This Day English not found") + # on_this_day_html_zh: typing.Optional[str] + # try: + # with open("otd_zh-%s.html" % datetime_target.strftime("%m-%d"), "r") as fd: + # on_this_day_html_zh = fd.read() + # except FileNotFoundError: + # logger.warning("On This Day Chinese not found") + # logger.info("Finished On This Day") + + in_the_news_html_en = "" + in_the_news_html_zh = "" + # logger.info("Starting In The News") + # in_the_news_html_en = legacy_wikipedia.get_in_the_news_en() + # in_the_news_html_zh = legacy_wikipedia.get_in_the_news_zh() + # logger.info("Finished In The News") + + data = { + "stddate": datetime_target.strftime("%Y-%m-%d"), + "community_time": week_data["community_time"][days_since_beginning:], + "days_after_this": len(week_data["community_time"][days_since_beginning:]) - 1, + "aod": aod, + "weekday_english": weekday_en, + "weekdays_abbrev": weekdays_short, + "weekday_chinese": weekday_zh, + "day_of_cycle": day_of_cycle, + "today_breakfast": breakfast_today, + "today_lunch": lunch_today, + "today_dinner": dinner_today, + "next_breakfast": breakfast_tomorrow, + "the_week_ahead_url": the_week_ahead_url, + "snack_morning": snack_morning, + "snack_afternoon": snack_afternoon, + "snack_evening": snack_evening, + "inspiration_type": inspiration_type, + "inspiration_shared_by": inspiration_shared_by, + "inspiration_origin": inspiration_origin, + "inspiration_text": inspiration_text, + "inspiration_image_data": inspiration_image_data, + "inspiration_image_mime": inspiration_image_mime, + "on_this_day_html_en": on_this_day_html_en, + "on_this_day_html_zh": on_this_day_html_zh, + "in_the_news_html_en": in_the_news_html_en, + "in_the_news_html_zh": in_the_news_html_zh, + } + with open("day-%s.json" % datetime_target.strftime("%Y%m%d"), "w", encoding="utf-8") as fd: + json.dump(data, fd, ensure_ascii=False, indent="\t") + logger.info( + "Data dumped to " + "day-%s.json" % datetime_target.strftime("%Y%m%d"), + ) + return "day-%s.json" % datetime_target.strftime("%Y%m%d") + + +if __name__ == "__main__": + main() diff --git a/sjdbmk/pack.py b/sjdbmk/pack.py new file mode 100755 index 0000000..192cd09 --- /dev/null +++ b/sjdbmk/pack.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# +# Daily script to pack the YK Pao School Daily Bulletin HTML from JSON data +# Copyright (C) 2024 Runxi Yu <https://runxiyu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + +from configparser import ConfigParser +import os +import json +import datetime +import argparse +import logging +import zoneinfo +from jinja2 import Template, StrictUndefined + + +def main(date: str, config: ConfigParser) -> None: + + with open( + os.path.join(config["templates"]["directory"], config["templates"]["main"]), + "r", + encoding="utf-8", + ) as template_file: + template = Template(template_file.read(), undefined=StrictUndefined, autoescape=True) + + with open( + os.path.join(config["general"]["build_path"], "day-" + date.replace("-", "") + ".json"), + "r", + encoding="utf-8", + ) as fd: + data = json.load(fd) + + # extra_data = { + # } + # + # data = data | extra_data + + template.stream(**data).dump(os.path.join(config["general"]["build_path"], "sjdb-%s.html" % date.replace("-", ""))) + + # FIXME: Escape the dangerous HTML! + + +if __name__ == "__main__": + try: + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser(description="Daily Bulletin Packer") + parser.add_argument( + "--date", + default=None, + help="the day to generate for, in local time, in YYYY-MM-DD; defaults to tomorrow", + # TODO: Verify validity of date + # TODO: Verify consistency of date elsewhere + ) + parser.add_argument("--config", default="config.ini", help="path to the configuration file") + args = parser.parse_args() + config = ConfigParser() + config.read(args.config) + if args.date: + date = args.date + else: + now = datetime.datetime.now(zoneinfo.ZoneInfo(config["general"]["timezone"])) + date = (now + datetime.timedelta(days=1)).strftime("%Y-%m-%d") + logging.info("Generating for day %s" % date) + # main(date, config) + main(date, config) + except KeyboardInterrupt: + logging.critical("KeyboardInterrupt") diff --git a/sjdbmk/twa.py b/sjdbmk/twa.py index 7c937cc..34f00a5 100644 --- a/sjdbmk/twa.py +++ b/sjdbmk/twa.py @@ -1,3 +1,22 @@ +#!/usr/bin/env python3 +# +# The Week Ahead Interpretation in the Songjiang Daily Bulletin Build System +# Copyright (C) 2024 Runxi Yu <https://runxiyu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# + import logging import datetime import os |