This commit is contained in:
Edward Betts 2024-07-02 07:15:23 +01:00
commit 1e39e75117
8 changed files with 123 additions and 46 deletions

View file

@ -90,10 +90,14 @@ def get_rates(config: flask.config.Config) -> dict[str, Decimal]:
except httpx.ConnectError: except httpx.ConnectError:
return read_cached_rates(full_path, currencies) 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: with open(os.path.join(fx_dir, filename), "w") as file:
file.write(response.text) file.write(response.text)
data = json.loads(response.text, parse_float=Decimal)
return { return {
cur: Decimal(data["quotes"][f"GBP{cur}"]) cur: Decimal(data["quotes"][f"GBP{cur}"])
for cur in currencies for cur in currencies

View file

@ -136,7 +136,7 @@ def summarize_launch(launch: Launch) -> Summary:
"launch_provider": launch_provider, "launch_provider": launch_provider,
"launch_provider_abbrev": launch_provider_abbrev, "launch_provider_abbrev": launch_provider_abbrev,
"launch_provider_type": get_nested(launch, ["launch_service_provider", "type"]), "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": launch.get("mission"),
"mission_name": get_nested(launch, ["mission", "name"]), "mission_name": get_nested(launch, ["mission", "name"]),
"pad_name": launch["pad"]["name"], "pad_name": launch["pad"]["name"],
@ -174,7 +174,10 @@ def get_launches(
existing.sort(reverse=True) existing.sort(reverse=True)
if refresh or not existing or (now - existing[0][0]).seconds > ttl: 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] f = existing[0][1]

View file

@ -12,28 +12,12 @@ from pycountry.db import Country
import agenda import agenda
from agenda import format_list_with_ampersand from agenda import format_list_with_ampersand
from . import utils
StrDict = dict[str, typing.Any] StrDict = dict[str, typing.Any]
DateOrDateTime = datetime.datetime | datetime.date 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 @dataclass
class TripElement: class TripElement:
"""Trip element.""" """Trip element."""
@ -106,18 +90,20 @@ class Trip:
def end(self) -> datetime.date | None: def end(self) -> datetime.date | None:
"""End date for trip.""" """End date for trip."""
max_conference_end = ( 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 if self.conferences
else datetime.date.min else datetime.date.min
) )
assert isinstance(max_conference_end, datetime.date) 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 travel_end = max(arrive) if arrive else datetime.date.min
assert isinstance(travel_end, datetime.date) assert isinstance(travel_end, datetime.date)
accommodation_end = ( 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 if self.accommodation
else datetime.date.min 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]]]: def elements_grouped_by_day(self) -> list[tuple[datetime.date, list[TripElement]]]:
"""Group trip elements by day.""" """Group trip elements by day."""
@ -325,7 +311,7 @@ class Trip:
for element in self.elements(): for element in self.elements():
# Extract the date part of the 'when' attribute # 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) grouped_elements[day].append(element)
# Sort elements within each day # Sort elements within each day
@ -334,7 +320,7 @@ class Trip:
key=lambda e: ( key=lambda e: (
e.element_type == "check-in", # check-out elements last e.element_type == "check-in", # check-out elements last
e.element_type != "check-out", # check-in elements first 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 @property
def as_datetime(self) -> datetime.datetime: def as_datetime(self) -> datetime.datetime:
"""Date/time of event.""" """Date/time of event."""
d = self.date return utils.as_datetime(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)
)
@property @property
def has_time(self) -> bool: def has_time(self) -> bool:

23
agenda/utils.py Normal file
View file

@ -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)
)

View file

@ -6,7 +6,40 @@
<div class="container-fluid mt-2"> <div class="container-fluid mt-2">
<h1>Space launches</h1> <h1>Space launches</h1>
{% for launch in rockets %} <h4>Filters</h4>
<p>Mission type:
{% if request.args.type %}<a href="{{ request.path }}">🗙</a>{% endif %}
{% for t in mission_types | sort %}
{% if t == request.args.type %}
<strong>{{ t }}</strong>
{% else %}
<a href="?type={{ t }}" class="text-nowrap">
{{ t }}
</a>
{% endif %}
{% if not loop.last %} | {% endif %}
{% endfor %}
</p>
<p>Vehicle:
{% if request.args.rocket %}<a href="{{ request.path }}">🗙</a>{% endif %}
{% for r in rockets | sort %}
{% if r == request.args.rockets %}
<strong>{{ r }}</strong>
{% else %}
<a href="?rocket={{ r }}" class="text-nowrap">
{{ r }}
</a>
{% endif %}
{% if not loop.last %} | {% endif %}
{% endfor %}
</p>
{% for launch in launches %}
{% set highlight =" bg-primary-subtle" if launch.slug in config.FOLLOW_LAUNCHES else "" %} {% set highlight =" bg-primary-subtle" if launch.slug in config.FOLLOW_LAUNCHES else "" %}
{% set country = get_country(launch.country_code) %} {% set country = get_country(launch.country_code) %}
<div class="row{{highlight}}"> <div class="row{{highlight}}">
@ -24,7 +57,7 @@
<div class="col"> <div class="col">
<div> <div>
{{ country.flag }} {{ country.flag }}
{{ launch.rocket }} {{ launch.rocket.full_name }}
&ndash; &ndash;
<strong>{{launch.mission.name }}</strong> <strong>{{launch.mission.name }}</strong>
&ndash; &ndash;

View file

@ -21,7 +21,7 @@
<td class="text-end"> <td class="text-end">
{{ weekend.date.isocalendar().week }} {{ weekend.date.isocalendar().week }}
</td> </td>
<td class="text-end"> <td class="text-end text-nowrap">
{{ weekend.date.strftime("%-d %b %Y") }} {{ weekend.date.strftime("%-d %b %Y") }}
</td> </td>
{% for day in "saturday", "sunday" %} {% for day in "saturday", "sunday" %}

View file

@ -99,10 +99,15 @@ Agenda: https://edwardbetts.com/agenda/
def report_space_launch_change( 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: ) -> None:
"""Send mail to announce change to space launch data.""" """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""" body = f"""
A space launch of interest was updated. A space launch of interest was updated.
@ -119,9 +124,11 @@ New launch data
send_mail(config, subject, body) 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.""" """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: 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"]: for slug in config["FOLLOW_LAUNCHES"]:
prev, cur = prev_launches[slug], cur_launches[slug] prev, cur = prev_launches[slug], cur_launches[slug]
if prev["last_updated"] != cur["last_updated"]: if prev is None and cur is None:
report_space_launch_change(config, prev, cur) continue
if prev and cur and prev["last_updated"] == cur["last_updated"]:
continue
report_space_launch_change(config, prev, cur)
time_taken = time() - t0 time_taken = time() - t0
if not sys.stdin.isatty(): if not sys.stdin.isatty():

View file

@ -35,7 +35,7 @@ agenda.error_mail.setup_error_mail(app)
@app.before_request @app.before_request
def handle_auth() -> None: def handle_auth() -> None:
"""Handle autentication and set global user.""" """Handle authentication and set global user."""
flask.g.user = UniAuth.auth.get_current_user() flask.g.user = UniAuth.auth.get_current_user()
@ -145,10 +145,34 @@ def launch_list() -> str:
now = datetime.now() now = datetime.now()
data_dir = app.config["DATA_DIR"] data_dir = app.config["DATA_DIR"]
rocket_dir = os.path.join(data_dir, "thespacedevs") 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( 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,
) )