Merge branch 'main' of https://git.4angle.com/edward/agenda
This commit is contained in:
commit
1e39e75117
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
try:
|
||||||
return next_launch_api(rocket_dir, limit=limit)
|
return next_launch_api(rocket_dir, limit=limit)
|
||||||
|
except Exception:
|
||||||
|
pass # fallback to cached version
|
||||||
|
|
||||||
f = existing[0][1]
|
f = existing[0][1]
|
||||||
|
|
||||||
|
|
|
@ -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
23
agenda/utils.py
Normal 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)
|
||||||
|
)
|
|
@ -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 }}
|
||||||
–
|
–
|
||||||
<strong>{{launch.mission.name }}</strong>
|
<strong>{{launch.mission.name }}</strong>
|
||||||
–
|
–
|
||||||
|
|
|
@ -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" %}
|
||||||
|
|
20
update.py
20
update.py
|
@ -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,7 +149,10 @@ 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:
|
||||||
|
continue
|
||||||
|
if prev and cur and prev["last_updated"] == cur["last_updated"]:
|
||||||
|
continue
|
||||||
report_space_launch_change(config, prev, cur)
|
report_space_launch_change(config, prev, cur)
|
||||||
|
|
||||||
time_taken = time() - t0
|
time_taken = time() - t0
|
||||||
|
|
30
web_view.py
30
web_view.py
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue