From b8ed1d5d65928d88710ba3fc56bbc66ab5cd8fc9 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 26 Jun 2024 08:48:58 +0100 Subject: [PATCH 1/6] Handle missing space launch --- update.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/update.py b/update.py index 09a6d39..31d346e 100755 --- a/update.py +++ b/update.py @@ -99,10 +99,15 @@ Agenda: https://edwardbetts.com/agenda/ def report_space_launch_change( - config: flask.config.Config, prev_launch: StrDict, cur_launch: StrDict + config: flask.config.Config, prev_launch: StrDict | None, cur_launch: StrDict | None ) -> None: """Send mail to announce change to space launch data.""" - subject = f'Change to {cur_launch["name"]}' + if cur_launch: + name = cur_launch["name"] + else: + assert prev_launch + name = prev_launch["name"] + subject = f"Change to {name}" body = f""" A space launch of interest was updated. @@ -119,9 +124,11 @@ New launch data send_mail(config, subject, body) -def get_launch_by_slug(data: StrDict, slug: str) -> StrDict: +def get_launch_by_slug(data: StrDict, slug: str) -> StrDict | None: """Find last update for space launch.""" - return {item["slug"]: typing.cast(StrDict, item) for item in data["results"]}[slug] + return {item["slug"]: typing.cast(StrDict, item) for item in data["results"]}.get( + slug + ) def update_thespacedevs(config: flask.config.Config) -> None: @@ -142,8 +149,11 @@ def update_thespacedevs(config: flask.config.Config) -> None: for slug in config["FOLLOW_LAUNCHES"]: prev, cur = prev_launches[slug], cur_launches[slug] - if prev["last_updated"] != cur["last_updated"]: - report_space_launch_change(config, prev, cur) + if prev is None and cur is None: + continue + if prev and cur and prev["last_updated"] == cur["last_updated"]: + continue + report_space_launch_change(config, prev, cur) time_taken = time() - t0 if not sys.stdin.isatty(): From 0e49d1872137f37f18a01c2c5bd35182586196ed Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Jul 2024 18:50:08 +0100 Subject: [PATCH 2/6] Correct spelling mistake --- web_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_view.py b/web_view.py index ca643c4..2cd1e80 100755 --- a/web_view.py +++ b/web_view.py @@ -35,7 +35,7 @@ agenda.error_mail.setup_error_mail(app) @app.before_request def handle_auth() -> None: - """Handle autentication and set global user.""" + """Handle authentication and set global user.""" flask.g.user = UniAuth.auth.get_current_user() From 01b42845c3974da7b79b738f05399c34f87372c8 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Jul 2024 22:22:01 +0300 Subject: [PATCH 3/6] Move some functions into a utils module --- agenda/types.py | 42 +++++++++++------------------------------- agenda/utils.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 agenda/utils.py diff --git a/agenda/types.py b/agenda/types.py index 6d18909..a8f3a8f 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -12,28 +12,12 @@ from pycountry.db import Country import agenda from agenda import format_list_with_ampersand +from . import utils + StrDict = dict[str, typing.Any] DateOrDateTime = datetime.datetime | datetime.date -def as_date(d: DateOrDateTime) -> datetime.date: - """Convert datetime to date.""" - if isinstance(d, datetime.datetime): - return d.date() - assert isinstance(d, datetime.date) - return d - - -def as_datetime(d: DateOrDateTime) -> datetime.datetime: - """Date/time of event.""" - t0 = datetime.datetime.min.time() - return ( - d - if isinstance(d, datetime.datetime) - else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) - ) - - @dataclass class TripElement: """Trip element.""" @@ -106,18 +90,20 @@ class Trip: def end(self) -> datetime.date | None: """End date for trip.""" max_conference_end = ( - max(as_date(item["end"]) for item in self.conferences) + max(utils.as_date(item["end"]) for item in self.conferences) if self.conferences else datetime.date.min ) assert isinstance(max_conference_end, datetime.date) - arrive = [as_date(item["arrive"]) for item in self.travel if "arrive" in item] + arrive = [ + utils.as_date(item["arrive"]) for item in self.travel if "arrive" in item + ] travel_end = max(arrive) if arrive else datetime.date.min assert isinstance(travel_end, datetime.date) accommodation_end = ( - max(as_date(item["to"]) for item in self.accommodation) + max(utils.as_date(item["to"]) for item in self.accommodation) if self.accommodation else datetime.date.min ) @@ -314,7 +300,7 @@ class Trip: ) ) - return sorted(elements, key=lambda e: as_datetime(e.start_time)) + return sorted(elements, key=lambda e: utils.as_datetime(e.start_time)) def elements_grouped_by_day(self) -> list[tuple[datetime.date, list[TripElement]]]: """Group trip elements by day.""" @@ -325,7 +311,7 @@ class Trip: for element in self.elements(): # Extract the date part of the 'when' attribute - day = as_date(element.start_time) + day = utils.as_date(element.start_time) grouped_elements[day].append(element) # Sort elements within each day @@ -334,7 +320,7 @@ class Trip: key=lambda e: ( e.element_type == "check-in", # check-out elements last e.element_type != "check-out", # check-in elements first - as_datetime(e.start_time), # then sort by time + utils.as_datetime(e.start_time), # then sort by time ) ) @@ -403,13 +389,7 @@ class Event: @property def as_datetime(self) -> datetime.datetime: """Date/time of event.""" - d = self.date - t0 = datetime.datetime.min.time() - return ( - d - if isinstance(d, datetime.datetime) - else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) - ) + return utils.as_datetime(self.date) @property def has_time(self) -> bool: diff --git a/agenda/utils.py b/agenda/utils.py new file mode 100644 index 0000000..0324df3 --- /dev/null +++ b/agenda/utils.py @@ -0,0 +1,23 @@ +"""Utility functions.""" + +import datetime + +DateOrDateTime = datetime.datetime | datetime.date + + +def as_date(d: DateOrDateTime) -> datetime.date: + """Convert datetime to date.""" + if isinstance(d, datetime.datetime): + return d.date() + assert isinstance(d, datetime.date) + return d + + +def as_datetime(d: DateOrDateTime) -> datetime.datetime: + """Date/time of event.""" + t0 = datetime.datetime.min.time() + return ( + d + if isinstance(d, datetime.datetime) + else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) + ) From c41bcc33041adc488fc020cf48e54f59adad85dd Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Jul 2024 22:22:23 +0300 Subject: [PATCH 4/6] Catch errors retrieving FX rates and return cached version --- agenda/fx.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agenda/fx.py b/agenda/fx.py index 90cbc5f..efd4d7d 100644 --- a/agenda/fx.py +++ b/agenda/fx.py @@ -90,10 +90,14 @@ def get_rates(config: flask.config.Config) -> dict[str, Decimal]: except httpx.ConnectError: return read_cached_rates(full_path, currencies) + try: + data = json.loads(response.text, parse_float=Decimal) + except json.decoder.JSONDecodeError: + return read_cached_rates(full_path, currencies) + with open(os.path.join(fx_dir, filename), "w") as file: file.write(response.text) - data = json.loads(response.text, parse_float=Decimal) return { cur: Decimal(data["quotes"][f"GBP{cur}"]) for cur in currencies From b65d79cb633a0d83ef696f5efda70d0e8b2c1e96 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Jul 2024 22:27:19 +0300 Subject: [PATCH 5/6] Add filters for space launches --- agenda/thespacedevs.py | 7 +++++-- templates/launches.html | 37 +++++++++++++++++++++++++++++++++++-- web_view.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 15ad455..3192448 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -136,7 +136,7 @@ def summarize_launch(launch: Launch) -> Summary: "launch_provider": launch_provider, "launch_provider_abbrev": launch_provider_abbrev, "launch_provider_type": get_nested(launch, ["launch_service_provider", "type"]), - "rocket": launch["rocket"]["configuration"]["full_name"], + "rocket": launch["rocket"]["configuration"], "mission": launch.get("mission"), "mission_name": get_nested(launch, ["mission", "name"]), "pad_name": launch["pad"]["name"], @@ -174,7 +174,10 @@ def get_launches( existing.sort(reverse=True) if refresh or not existing or (now - existing[0][0]).seconds > ttl: - return next_launch_api(rocket_dir, limit=limit) + try: + return next_launch_api(rocket_dir, limit=limit) + except Exception: + pass # fallback to cached version f = existing[0][1] diff --git a/templates/launches.html b/templates/launches.html index a8baede..1756424 100644 --- a/templates/launches.html +++ b/templates/launches.html @@ -6,7 +6,40 @@

Space launches

- {% for launch in rockets %} +

Filters

+ +

Mission type: + + {% if request.args.type %}🗙{% endif %} + + {% for t in mission_types | sort %} + {% if t == request.args.type %} + {{ t }} + {% else %} + + {{ t }} + + {% endif %} + {% if not loop.last %} | {% endif %} + {% endfor %} +

+ +

Vehicle: + {% if request.args.rocket %}🗙{% endif %} + + {% for r in rockets | sort %} + {% if r == request.args.rockets %} + {{ r }} + {% else %} + + {{ r }} + + {% endif %} + {% if not loop.last %} | {% endif %} + {% endfor %} +

+ + {% for launch in launches %} {% set highlight =" bg-primary-subtle" if launch.slug in config.FOLLOW_LAUNCHES else "" %} {% set country = get_country(launch.country_code) %}
@@ -24,7 +57,7 @@
{{ country.flag }} - {{ launch.rocket }} + {{ launch.rocket.full_name }} – {{launch.mission.name }} – diff --git a/web_view.py b/web_view.py index 2cd1e80..c3e663a 100755 --- a/web_view.py +++ b/web_view.py @@ -145,10 +145,34 @@ def launch_list() -> str: now = datetime.now() data_dir = app.config["DATA_DIR"] rocket_dir = os.path.join(data_dir, "thespacedevs") - rockets = agenda.thespacedevs.get_launches(rocket_dir, limit=100) + launches = agenda.thespacedevs.get_launches(rocket_dir, limit=100) + + mission_type_filter = flask.request.args.get("type") + rocket_filter = flask.request.args.get("rocket") + + mission_types = { + launch["mission"]["type"] for launch in launches if launch["mission"] + } + + rockets = {launch["rocket"]["full_name"] for launch in launches} + + launches = [ + launch + for launch in launches + if ( + not mission_type_filter + or (launch["mission"] and launch["mission"]["type"] == mission_type_filter) + ) + and (not rocket_filter or launch["rocket"]["full_name"] == rocket_filter) + ] return flask.render_template( - "launches.html", rockets=rockets, now=now, get_country=agenda.get_country + "launches.html", + launches=launches, + rockets=rockets, + now=now, + get_country=agenda.get_country, + mission_types=mission_types, ) From 4e328d401e89089e0c5c8e2e383c359b7b520181 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Mon, 1 Jul 2024 22:28:15 +0300 Subject: [PATCH 6/6] No wrap for date in weekend list --- templates/weekends.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/weekends.html b/templates/weekends.html index e787aa6..f443ff6 100644 --- a/templates/weekends.html +++ b/templates/weekends.html @@ -21,7 +21,7 @@ {{ weekend.date.isocalendar().week }} - + {{ weekend.date.strftime("%-d %b %Y") }} {% for day in "saturday", "sunday" %}