Compare commits

...

6 commits

Author SHA1 Message Date
5c27528247 Fix MIME type for image
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline was successful
2024-11-23 22:13:46 -08:00
e6722876bd Log outage geometries to database (#68)
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline failed
To enable us to detect changes to to the geometry for future updating of the map (#67)

Reviewed-on: #68
2024-11-23 17:14:30 -08:00
fbdc85bc29 Merge pull request 'Update dependency numpy to v2.1.3' (#66) from renovate/numpy-2.x into main
Some checks failed
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline failed
Reviewed-on: #66
2024-11-20 11:14:22 -08:00
7f4e8232dc Update dependency numpy to v2.1.3
Some checks failed
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/vulnerability-scan Pipeline failed
ci/woodpecker/pull_request_closed/lint Pipeline was successful
ci/woodpecker/pull_request_closed/vulnerability-scan Pipeline was successful
2024-11-06 19:16:31 +00:00
14f217a91e Update dependency SQLAlchemy to v2.0.36
All checks were successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/vulnerability-scan Pipeline was successful
ci/woodpecker/pull_request_closed/lint Pipeline was successful
ci/woodpecker/pull_request_closed/vulnerability-scan Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline was successful
2024-11-02 03:10:47 +00:00
ab112abeed quieter-restoration-time (#64)
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/vulnerability-scan Pipeline was successful
Closes #62

Reviewed-on: #64
2024-10-27 12:30:44 -07:00
3 changed files with 43 additions and 12 deletions

View file

@ -1,4 +1,5 @@
from shapely import MultiPolygon, Polygon from shapely import MultiPolygon, Polygon, Geometry, to_wkb, from_wkb
from sqlalchemy.types import TypeDecorator, LargeBinary
def convert_outage_geometry(event) -> MultiPolygon: def convert_outage_geometry(event) -> MultiPolygon:
@ -9,3 +10,21 @@ def convert_outage_geometry(event) -> MultiPolygon:
for ring in event["polygons"]["rings"]: for ring in event["polygons"]["rings"]:
polygon_list.append(Polygon(ring)) polygon_list.append(Polygon(ring))
return MultiPolygon(polygon_list) return MultiPolygon(polygon_list)
class DBGeometry(TypeDecorator):
impl = LargeBinary
cache_ok = True
def process_bind_param(self, value, dialect):
if isinstance(value, Geometry):
value = to_wkb(value)
return value
def process_result_value(self, value, dialect):
if value is None:
return value
else:
if not isinstance(value, Geometry):
value = from_wkb(value)
return value

View file

@ -6,7 +6,7 @@ greenlet==3.1.1
idna==3.10 idna==3.10
pip-install==1.3.5 pip-install==1.3.5
Mastodon.py==1.8.1 Mastodon.py==1.8.1
numpy==2.1.2 numpy==2.1.3
pillow==11.0.0 pillow==11.0.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-magic==0.4.27 python-magic==0.4.27
@ -14,7 +14,7 @@ PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
shapely==2.0.6 shapely==2.0.6
six==1.16.0 six==1.16.0
SQLAlchemy==2.0.35 SQLAlchemy==2.0.36
staticmap==0.5.7 staticmap==0.5.7
typing_extensions==4.12.2 typing_extensions==4.12.2
urllib3==2.2.3 urllib3==2.2.3

30
scl.py
View file

@ -5,15 +5,16 @@ from typing import Optional
import mastodon import mastodon
import requests import requests
import shapely import shapely
import staticmap
import yaml import yaml
from mastodon import Mastodon from mastodon import Mastodon
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from shapely import Geometry
from sqlalchemy import create_engine, select from sqlalchemy import create_engine, select
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
from staticmap import Polygon, StaticMap
from geospatial import convert_outage_geometry from geospatial import DBGeometry, convert_outage_geometry
post_datetime_format = "%b %e %l:%M %p" post_datetime_format = "%b %e %l:%M %p"
@ -36,7 +37,7 @@ mastodon_client = Mastodon(
) )
class AttribStaticMap(StaticMap, object): class AttribStaticMap(staticmap.StaticMap, object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.attribution = "© Stadia Maps © OpenMapTiles © OpenStreetMap" self.attribution = "© Stadia Maps © OpenMapTiles © OpenStreetMap"
super(AttribStaticMap, self).__init__(*args, **kwargs) super(AttribStaticMap, self).__init__(*args, **kwargs)
@ -137,7 +138,7 @@ def do_initial_post(
) )
assert event["polygons"]["type"] == "polygon" assert event["polygons"]["type"] == "polygon"
for ring in event["polygons"]["rings"]: for ring in event["polygons"]["rings"]:
polygon = Polygon( polygon = staticmap.Polygon(
ring, ring,
# Appending 7F to the fill_color makes it 50% transparent # Appending 7F to the fill_color makes it 50% transparent
fill_color="{}7F".format(event_class["outage_color"]), fill_color="{}7F".format(event_class["outage_color"]),
@ -221,7 +222,7 @@ def do_initial_post(
map_image.save(map_image_file, format="WebP", method=6) map_image.save(map_image_file, format="WebP", method=6)
map_media_post = mastodon_client.media_post( map_media_post = mastodon_client.media_post(
map_image_file.getvalue(), map_image_file.getvalue(),
mime_type="image/png", mime_type="image/webp",
description=alt_text, description=alt_text,
) )
map_media_post_id = map_media_post["id"] map_media_post_id = map_media_post["id"]
@ -272,7 +273,9 @@ Cause: {}
class Base(DeclarativeBase): class Base(DeclarativeBase):
pass type_annotation_map = {
Geometry: DBGeometry,
}
class SclOutage(Base): class SclOutage(Base):
@ -292,9 +295,11 @@ class SclOutage(Base):
max_num_people: Mapped[int] = mapped_column() max_num_people: Mapped[int] = mapped_column()
neighborhood: Mapped[Optional[str]] = mapped_column() neighborhood: Mapped[Optional[str]] = mapped_column()
city: Mapped[Optional[str]] = mapped_column() city: Mapped[Optional[str]] = mapped_column()
outage_geometries: Mapped[Optional[Geometry]] = mapped_column()
geometries_modified: Mapped[Optional[bool]] = mapped_column()
def __repr__(self) -> str: def __repr__(self) -> str:
return f"SclOutage(scl_outage_id={self.scl_outage_id!r}, most_recent_post_id={self.most_recent_post_id!r}, initial_post_id={self.initial_post_id!r}, map_media_post_id={self.map_media_post_id!r}, last_updated_time={self.last_updated_time!r}, no_longer_in_response_time={self.no_longer_in_response_time!r}, start_time={self.start_time!r}, num_people={self.num_people!r}, max_num_people={self.max_num_people!r}, neighborhood={self.neighborhood!r}, city={self.city!r})" return f"SclOutage(scl_outage_id={self.scl_outage_id!r}, most_recent_post_id={self.most_recent_post_id!r}, initial_post_id={self.initial_post_id!r}, map_media_post_id={self.map_media_post_id!r}, last_updated_time={self.last_updated_time!r}, no_longer_in_response_time={self.no_longer_in_response_time!r}, start_time={self.start_time!r}, num_people={self.num_people!r}, max_num_people={self.max_num_people!r}, neighborhood={self.neighborhood!r}, city={self.city!r}, outage_geometries={self.outage_geometries!r}, geometries_modified={self.geometries_modified!r})"
engine = create_engine("sqlite:///scl.db") engine = create_engine("sqlite:///scl.db")
@ -336,8 +341,8 @@ with Session(engine) as session:
) )
/ 60 / 60
) )
# Only post if estimated restoration time has changed by 30m or more # Only post if estimated restoration time has changed by 60m or more
if est_restoration_diff_mins >= 30: if est_restoration_diff_mins >= 60:
existing_record.estimated_restoration_time = estimated_restoration_time existing_record.estimated_restoration_time = estimated_restoration_time
if estimated_restoration_time > datetime.now(): if estimated_restoration_time > datetime.now():
# New estimated restoration time is in the future, so likely to be a real time # New estimated restoration time is in the future, so likely to be a real time
@ -366,6 +371,10 @@ with Session(engine) as session:
# Used to determine the maximum number of people affected by this outage, to determine if it's worth posting about # Used to determine the maximum number of people affected by this outage, to determine if it's worth posting about
existing_record.max_num_people = event["numPeople"] existing_record.max_num_people = event["numPeople"]
max_event_class = classify_event_size(existing_record.max_num_people) max_event_class = classify_event_size(existing_record.max_num_people)
if existing_record.outage_geometries != outage_geometries:
print("Geometries modified")
existing_record.outage_geometries = outage_geometries
existing_record.geometries_modified = True
if updated_properties: if updated_properties:
updated_properties.sort() updated_properties.sort()
@ -426,6 +435,8 @@ with Session(engine) as session:
existing_record.map_media_post_id = initial_post_result[ existing_record.map_media_post_id = initial_post_result[
"map_media_post_id" "map_media_post_id"
] ]
else:
print("Existing record was found, and no properties were updated.")
session.commit() session.commit()
except NoResultFound: except NoResultFound:
@ -476,6 +487,7 @@ with Session(engine) as session:
max_num_people=event["numPeople"], max_num_people=event["numPeople"],
neighborhood=neighborhood, neighborhood=neighborhood,
city=city, city=city,
outage_geometries=outage_geometries,
) )
session.add(new_outage_record) session.add(new_outage_record)
session.commit() session.commit()