From ca9bdf7a521ab45dd01f2c3aeac07db280e3f9f5 Mon Sep 17 00:00:00 2001 From: JC Connell Date: Sun, 11 Sep 2022 11:32:02 -1000 Subject: [PATCH 1/2] early work towards adding animated chart gifs --- magicseaweed/__init__.py | 147 ++++++++++++++++++++++++++++++--------- run.py | 10 +-- 2 files changed, 120 insertions(+), 37 deletions(-) diff --git a/magicseaweed/__init__.py b/magicseaweed/__init__.py index db54ccf..609a6ba 100644 --- a/magicseaweed/__init__.py +++ b/magicseaweed/__init__.py @@ -1,7 +1,10 @@ +from ast import Str +import requests +from PIL import Image +from enum import Enum from datetime import timedelta from flatten_json import flatten from datetime import datetime as dt -import requests MSW_URL = 'http://magicseaweed.com/api/{}/forecast' @@ -12,29 +15,68 @@ HTTP_GET = 'GET' ERROR_RESPONSE = 'error_response' UNITS = ['us', 'uk', 'eu'] -CHART_TYPES = ['swell', 'period', 'wind', 'pressure', 'sst'] SWELL_TYPES = ['combined', 'primary', 'secondary', 'tertiary'] -FIELD_TYPES = ['timestamp', 'localTimestamp', 'issueTimestamp', 'fadedRating', - 'solidRating', 'threeHourTimeText', 'swell.minBreakingHeight', - 'swell.*', 'swell.absMinBreakingHeight', 'swell.maxBreakingHeight', - 'swell.absMaxBreakingHeight', 'swell.probability', 'swell.unit', - 'swell.components.combined.*', 'swell.components.combined.height', - 'swell.components.combined.period', 'swell.components.combined.direction', +FIELD_TYPES = ['timestamp', + 'localTimestamp', + 'issueTimestamp', + 'fadedRating', + 'solidRating', + 'threeHourTimeText', + 'swell.minBreakingHeight', + 'swell.*', + 'swell.absMinBreakingHeight', + 'swell.maxBreakingHeight', + 'swell.absMaxBreakingHeight', + 'swell.probability', + 'swell.unit', + 'swell.components.combined.*', + 'swell.components.combined.height', + 'swell.components.combined.period', + 'swell.components.combined.direction', 'swell.components.combined.compassDirection', - 'swell.components.primary.*', 'swell.components.primary.height', - 'swell.components.primary.period', 'swell.components.primary.direction', + 'swell.components.primary.*', + 'swell.components.primary.height', + 'swell.components.primary.period', + 'swell.components.primary.direction', 'swell.components.primary.compassDirection', - 'swell.components.secondary.*', 'swell.components.secondary.height', - 'swell.components.secondary.period', 'swell.components.secondary.direction', + 'swell.components.secondary.*', + 'swell.components.secondary.height', + 'swell.components.secondary.period', + 'swell.components.secondary.direction', 'swell.components.secondary.compassDirection', - 'swell.components.tertiary.*', 'swell.components.tertiary.height', - 'swell.components.tertiary.period', 'swell.components.tertiary.direction', + 'swell.components.tertiary.*', + 'swell.components.tertiary.height', + 'swell.components.tertiary.period', + 'swell.components.tertiary.direction', 'swell.components.tertiary.compassDirection', - 'wind.*', 'wind.speed', 'wind.direction', - 'wind.compassDirection', 'wind.chill', 'wind.gusts', 'wind.unit', - 'condition.*', 'condition.temperature', 'condition.weather', 'condition.pressure', - 'condition.unitPressure', 'condition.unit', 'charts.*', 'charts.swell', - 'charts.period', 'charts.wind', 'charts.pressure', 'charts.sst'] + 'wind.*', + 'wind.speed', + 'wind.direction', + 'wind.compassDirection', + 'wind.chill', + 'wind.gusts', + 'wind.unit', + 'condition.*', + 'condition.temperature', + 'condition.weather', + 'condition.pressure', + 'condition.unitPressure', + 'condition.unit', + 'charts.*', + 'charts.swell', + 'charts.period', + 'charts.wind', + 'charts.pressure', + 'charts.sst' + ] + + +class Chart_type(Enum): + SWELL = "swell" + PERIOD = "period" + WIND = "wind" + PRESSURE = "pressure" + SST = "sst" def _validate_unit_types(unit): @@ -87,10 +129,14 @@ def _forecast_transform(f_d): 'stars': "{} solid, {} faded".format(solid_stars, faded_stars), 'begins': dt.utcfromtimestamp(begins).strftime("%a %-I %p"), 'issued': dt.utcfromtimestamp(issued).strftime("%a %-I %p"), - 'max_breaking_height': "{} {}".format(swell_max_breaking_height, swell_unit), - 'abs_max_breaking_height': "{} {}".format(swell_abs_max_breaking_height, swell_unit), - 'min_breaking_height': "{} {}".format(swell_min_breaking_height, swell_unit), - 'abs_min_breaking_height': "{} {}".format(swell_abs_min_breaking_height, swell_unit), + 'max_breaking_height': "{} {}".format( + swell_max_breaking_height, swell_unit), + 'abs_max_breaking_height': "{} {}".format( + swell_abs_max_breaking_height, swell_unit), + 'min_breaking_height': "{} {}".format( + swell_min_breaking_height, swell_unit), + 'abs_min_breaking_height': "{} {}".format( + swell_abs_min_breaking_height, swell_unit), 'probability': "{}%".format(probability), 'swell_direction': "{}".format(swell_dir), 'swell_period': "{} seconds".format(period), @@ -108,15 +154,16 @@ def build_request(api_key, spot_id, fields=None, unit=None, API details https://magicseaweed.com/developer/forecast-api key: Magic Seaweed API key - spot_id: The ID of a location, available from the URL when visiting the - corresponding spot on the Magic Seaweed website. IE '616' in + spot_id: The ID of a location, available from the + URL when visiting the corresponding spot on + the Magic Seaweed website. IE '616' in http://magicseaweed.com/Pipeline-Backdoor-Surf-Report/616/ fields: Comma separated list of fields to include in the request - URL. Defaults to none, which returns all information. Specifying - fields may reduce response time. Example: + URL. Defaults to none, which returns all information. + Specifying fields may reduce response time. Example: fields=timestamp,wind.*,condition.temperature - units: A string of the preferred unit of measurement. Defaults to unit at - location of spot_id. eu, uk, us are available + units: A string of the preferred unit of measurement. Defaults + to unit at location of spot_id. eu, uk, us are available start: Local timestamp for the start of a desired forecast range end: Local timestamp for the end of the desired forecast range """ @@ -195,7 +242,7 @@ def get_manual(self, start, end): class ForecastDataBlock(): - def __init__(self, d=None, headers=None, response=None): + def __init__(self, d: dict | None = None, headers: dict | None = None, response: requests.Response | None = None): d = d or {} self.headers = headers self.response = response @@ -203,7 +250,7 @@ def __init__(self, d=None, headers=None, response=None): for datapoint in d] self.summary = self._summary(self.data) - def _summary(self, d): + def _summary(self, d: dict) -> str: try: num = len(d) start = d[0].attrs['begins'] @@ -212,6 +259,28 @@ def _summary(self, d): except KeyError: return "No forecasts." + def make_gif(self, chart_type: Chart_type) -> None: + gif_file = '{}.gif'.format(chart_type.value) + frames = self.load_charts(chart_type) + frame_one = frames[0] + frame_one.save(gif_file, format="GIF", append_images=frames, + save_all=True, duration=100, loop=0) + + def get_gif_bytes(self, chart_type: Chart_type) -> bytes | None: + gif_file = '{}.gif'.format(chart_type.value) + frames = self.load_charts(chart_type) + frame_one = frames[0] + frame_one.save(gif_file, format="GIF", append_images=frames, + save_all=True, duration=100, loop=0) + + def load_charts(self, chart_type: Chart_type): + images_frames = [] + for datapoint in self.data: + url = datapoint.get_chart_url(chart_type) + image = Image.open(requests.get(url, stream=True).raw) + images_frames.append(image) + return images_frames + class ForecastDataPoint(): @@ -228,7 +297,8 @@ def _summary(self, d): min_breaking_height = self.attrs['min_breaking_height'] max_breaking_height = self.attrs['max_breaking_height'] local_tm = self.attrs['begins'] - return "{} - {} at {}".format(min_breaking_height, max_breaking_height, local_tm) + return "{} - {} at {}".format(min_breaking_height, + max_breaking_height, local_tm) except KeyError: return None @@ -239,7 +309,8 @@ def __getattr__(self, name): try: return self.f_d[name] except KeyError: - return PropertyUnavailable("Property {} is unavailable for this forecast".format(name)) + return PropertyUnavailable(('Property {} is unavailable ' + 'for this forecast').format(name)) def get_swell_url(self, swell_type): """Get swell arrow url.""" @@ -258,6 +329,16 @@ def get_wind_url(self): rounded = int(5 * round(float(wind_direction)/5)) return WIND_ARROW_URL.format(rounded) + def get_chart_url(self, chartType): + """Get url for the chart type.""" + + if not isinstance(chartType, Chart_type): + raise TypeError( + 'chartType must be an instance of Chart_type Enum') + + chart_type = '{}_{}'.format("charts", chartType.value) + return self.f_d.get(chart_type, None) + class PropertyUnavailable(AttributeError): """Raise when an attribute is not available for a forecast.""" diff --git a/run.py b/run.py index c061893..7194016 100644 --- a/run.py +++ b/run.py @@ -1,7 +1,7 @@ -from magicseaweed import MSW_Forecast -from dotenv import load_dotenv import os import pprint +import magicseaweed +from dotenv import load_dotenv load_dotenv() @@ -10,14 +10,16 @@ bethune_id = 3771 pp = pprint.PrettyPrinter(indent=4) -ponce_forecast = MSW_Forecast(api_key, ponce_id) +ponce_forecast = magicseaweed.MSW_Forecast(api_key, ponce_id) ponce_now = ponce_forecast.get_current() print(ponce_now.attrs) -bethune_forecast = MSW_Forecast(api_key, bethune_id) +bethune_forecast = magicseaweed.MSW_Forecast(api_key, bethune_id) bethune_future = bethune_forecast.get_future() print(bethune_future.summary) +bethune_future.make_gif(magicseaweed.Chart_type.SWELL) + for forecast in bethune_future.data: pp.pprint(forecast.attrs) pp.pprint(forecast.charts_swell) From 004d5b0b4cf1c9bce795c006280ded7ca42c5f1a Mon Sep 17 00:00:00 2001 From: JC Connell Date: Mon, 12 Sep 2022 17:39:36 -1000 Subject: [PATCH 2/2] WIP --- magicseaweed/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/magicseaweed/__init__.py b/magicseaweed/__init__.py index 609a6ba..3d66d15 100644 --- a/magicseaweed/__init__.py +++ b/magicseaweed/__init__.py @@ -266,12 +266,12 @@ def make_gif(self, chart_type: Chart_type) -> None: frame_one.save(gif_file, format="GIF", append_images=frames, save_all=True, duration=100, loop=0) - def get_gif_bytes(self, chart_type: Chart_type) -> bytes | None: - gif_file = '{}.gif'.format(chart_type.value) - frames = self.load_charts(chart_type) - frame_one = frames[0] - frame_one.save(gif_file, format="GIF", append_images=frames, - save_all=True, duration=100, loop=0) + # def get_gif_bytes(self, chart_type: Chart_type) -> bytes | None: + # gif_file = '{}.gif'.format(chart_type.value) + # frames = self.load_charts(chart_type) + # frame_one = frames[0] + # frame_one.tobytes(gif_file, format="GIF", append_images=frames, + # save_all=True, duration=100, loop=0) def load_charts(self, chart_type: Chart_type): images_frames = []