Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DBC22-1804: populate data, serializer, and starting frontend logic #316

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 143 additions & 2 deletions src/backend/apps/feed/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import httpx
import requests
from apps.feed.constants import (
CURRENT_WEATHER,
CURRENT_WEATHER_STATIONS,
DIT,
INLAND_FERRY,
OPEN511,
Expand All @@ -14,6 +16,7 @@
)
from apps.feed.serializers import (
CarsEventSerializer,
CurrentWeatherSerializer,
EventAPISerializer,
EventFeedSerializer,
FerryAPISerializer,
Expand All @@ -25,6 +28,33 @@
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response

# Maps the key for our client API's serializer fields to the matching pair of
# the source API's DataSetName and DisplayName fields
SERIALIZER_TO_DATASET_MAPPING = {
"air_temperature": ("air_temp", "Air Temp"),
"road_temperature": ("sfc_temp", "Pavement Temp"),
"road_surface": ("sfc_stat_derived_state", "Pavement Status (State)", "MeaningfulValue"),
"precipitation": ("pcpn_amt_pst1hr", "Precip Hourly"),
"snow": ("snwfl_amt_pst1hr", "Snowfall (hourly)"),
"wind_direction": ("wnd_dir", "Wind Direction (current)"),
"average_wind": ("wnd_spd", "Wind Speed (current)"),
"maximum_wind": ("max_wnd_spd_pst1hr", "Wind Speed (max)")
}

# Generated list of DataSetName values for filtering excluded dataset entries
DATASETNAMES = [value[0] for value in SERIALIZER_TO_DATASET_MAPPING.values()]

# Generated mapping of DataSetName to DisplayName
DISPLAYNAME_MAPPING = {value[0]: value[1] for value in SERIALIZER_TO_DATASET_MAPPING.values()}

# Generated mapping of DataSetName to Serializer field
SERIALIZER_MAPPING = {value[0]: key for key, value in SERIALIZER_TO_DATASET_MAPPING.items()}

# Generated mapping of DataSetName to the name of the value field in the tuple;
# defaults to "Value"
VALUE_FIELD_MAPPING = {value[0]: (value[2] if len(value) > 2 else "Value")
for value in SERIALIZER_TO_DATASET_MAPPING.values()}

logger = logging.getLogger(__name__)


Expand All @@ -51,6 +81,12 @@ def __init__(self):
REGIONAL_WEATHER_AREAS: {
"base_url": settings.DRIVEBC_WEATHER_AREAS_API_BASE_URL,
},
CURRENT_WEATHER: {
"base_url": settings.DRIVEBC_WEATHER_CURRENT_API_BASE_URL,
},
CURRENT_WEATHER_STATIONS: {
"base_url": settings.DRIVEBC_WEATHER_CURRENT_STATIONS_API_BASE_URL
}
}

def _get_auth_headers(self, resource_type):
Expand Down Expand Up @@ -310,8 +346,8 @@ def get_regional_weather_list_feed(self, resource_type, resource_name, serialize
except requests.RequestException as e:
print(f"Error making API call for Area Code {area_code}: {e}")

except requests.RequestException as e:
return Response({"error": f"Error fetching data from weather API: {str(e)}"}, status=500)
except requests.RequestException:
return Response("Error fetching data from weather API", status=500)

try:
serializer.is_valid(raise_exception=True)
Expand All @@ -327,3 +363,108 @@ def get_regional_weather_list(self):
REGIONAL_WEATHER, 'regionalweather', RegionalWeatherSerializer,
{"format": "json", "limit": 500}
)

# Current Weather
def get_current_weather_list_feed(self, resource_type, resource_name, serializer_cls, params=None):
"""Get data feed for list of objects."""
area_code_endpoint = settings.DRIVEBC_WEATHER_CURRENT_STATIONS_API_BASE_URL
# Obtain Access Token
token_url = settings.DRIVEBC_WEATHER_API_TOKEN_URL
client_id = settings.WEATHER_CLIENT_ID
client_secret = settings.WEATHER_CLIENT_SECRET

token_params = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
}

try:
response = requests.post(token_url, data=token_params)
response.raise_for_status()
token_data = response.json()
access_token = token_data.get("access_token")
except requests.RequestException as e:
return Response({"error": f"Error obtaining access token: {str(e)}"}, status=500)
Dismissed Show dismissed Hide dismissed
external_api_url = area_code_endpoint
headers = {"Authorization": f"Bearer {access_token}"}
try:
response = requests.get(external_api_url, headers=headers)
response.raise_for_status()
data = response.json()
json_response = data
json_objects = []

for station in json_response:
station_number = station["WeatherStationNumber"]
api_endpoint = settings.DRIVEBC_WEATHER_CURRENT_API_BASE_URL + f"{station_number}"
# Reget access token in case the previous token expired
try:
response = requests.post(token_url, data=token_params)
response.raise_for_status()
token_data = response.json()
access_token = token_data.get("access_token")
except requests.RequestException as e:
return Response({"error": f"Error obtaining access token: {str(e)}"}, status=500)
Dismissed Show dismissed Hide dismissed
headers = {"Authorization": f"Bearer {access_token}"}

try:
response = requests.get(api_endpoint, headers=headers)
data = response.json()
datasets = data.get("Datasets") if data else None
issuedUtc = data.get("IssuedUtc")
elevation = data.get('WeatherStation').get("Elevation")
Longitude = data.get('WeatherStation').get("Longitude")
Latitude = data.get('WeatherStation').get("Latitude")
weather_station_name = data.get('WeatherStation').get("WeatherStationName")
location_description = data.get('WeatherStation').get("LocationDescription")
# filtering down dataset to just SensorTypeName and DataSetName
filtered_dataset = {}

if datasets is None:
continue

for dataset in datasets:
dataset_name = dataset["DataSetName"]
if dataset_name not in DATASETNAMES:
continue
display_name = DISPLAYNAME_MAPPING[dataset_name]
serializer_name = SERIALIZER_MAPPING[dataset_name]
value_field = VALUE_FIELD_MAPPING[dataset_name]

if display_name == dataset["DisplayName"]:
filtered_dataset[serializer_name] = {
"value": dataset[value_field], "unit": dataset["Unit"],
}

current_weather_data = {
'weather_station_name': weather_station_name,
'elevation': elevation,
'location_description': location_description,
'datasets': filtered_dataset,
'location_longitude': Longitude,
'location_latitude': Latitude,
'issuedUtc': issuedUtc,
}
serializer = serializer_cls(data=current_weather_data,
many=isinstance(current_weather_data, list))
json_objects.append(current_weather_data)

except requests.RequestException as e:
print(f"Error making API call for Area Code {station_number}: {e}")
try:
serializer.is_valid(raise_exception=True)
return json_objects

except (KeyError, ValidationError):
field_errors = serializer.errors
for field, errors in field_errors.items():
print(f"Field: {field}, Errors: {errors}")
except requests.RequestException:
return Response("Error fetching data from weather API", status=500)

def get_current_weather_list(self):
return self.get_current_weather_list_feed(
CURRENT_WEATHER, 'currentweather', CurrentWeatherSerializer,
{"format": "json", "limit": 500}
)
4 changes: 3 additions & 1 deletion src/backend/apps/feed/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
INLAND_FERRY = "inland_ferry"
REGIONAL_WEATHER = "regional_weather"
REGIONAL_WEATHER_AREAS = "regional_weather_areas"
CURRENT_WEATHER = "current_weather"
CURRENT_WEATHER_STATIONS = "current_weather_stations"

DIRECTIONS = {
'in both directions': 'BOTH',
'northbound': 'N',
'southbound': 'S',
'eastbound': 'E',
'westbound': 'W',
}
}
18 changes: 17 additions & 1 deletion src/backend/apps/feed/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
WebcamRegionField,
WebcamRegionGroupField,
)
from apps.weather.models import RegionalWeather
from apps.weather.models import CurrentWeather, RegionalWeather
from rest_framework import serializers


Expand Down Expand Up @@ -239,3 +239,19 @@ class Meta:
'forecast_group',
'hourly_forecast_group',
)


# Current Weather serializer
class CurrentWeatherSerializer(serializers.Serializer):
class Meta:
model = CurrentWeather
fields = (
'id',
'location_latitude',
'location_longitude',
'weather_station_name',
'elevation',
'location_description',
'datasets',
'issuedUtc',
)
3 changes: 2 additions & 1 deletion src/backend/apps/weather/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from apps.weather.models import RegionalWeather
from apps.weather.models import CurrentWeather, RegionalWeather
from django.contrib import admin
from django.contrib.admin import ModelAdmin

Expand All @@ -8,3 +8,4 @@ class WeatherAdmin(ModelAdmin):


admin.site.register(RegionalWeather, WeatherAdmin)
admin.site.register(CurrentWeather, WeatherAdmin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from apps.weather.tasks import populate_all_current_weather_data
from django.core.management.base import BaseCommand


class Command(BaseCommand):
def handle(self, *args, **options):
populate_all_current_weather_data()
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 4.2.3 on 2024-02-29 18:55

import django.contrib.gis.db.models.fields
from django.db import migrations, models


class Migration(migrations.Migration):

replaces = [('weather', '0001_initial'), ('weather', '0002_rename_location_code_regionalweather_code_and_more'), ('weather', '0003_rename_location_name_regionalweather_name'), ('weather', '0004_currentweather'), ('weather', '0005_alter_currentweather_datasets')]

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='RegionalWeather',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('code', models.CharField(max_length=10, null=True)),
('location_latitude', models.CharField(max_length=10, null=True)),
('location_longitude', models.CharField(max_length=10, null=True)),
('name', models.CharField(max_length=100, null=True)),
('region', models.CharField(max_length=255, null=True)),
('observation_name', models.CharField(max_length=50, null=True)),
('observation_zone', models.CharField(max_length=10, null=True)),
('observation_utc_offset', models.IntegerField(null=True)),
('observation_text_summary', models.CharField(max_length=255, null=True)),
('conditions', models.JSONField(null=True)),
('forecast_group', models.JSONField(null=True)),
('hourly_forecast_group', models.JSONField(null=True)),
('location', django.contrib.gis.db.models.fields.PointField(null=True, srid=4326)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CurrentWeather',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('weather_station_name', models.CharField(max_length=100)),
('elevation', models.IntegerField(null=True)),
('location_description', models.TextField(null=True)),
('datasets', models.JSONField(default=[], null=True)),
],
options={
'abstract': False,
},
),
]
28 changes: 28 additions & 0 deletions src/backend/apps/weather/migrations/0004_currentweather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.3 on 2024-02-22 23:26

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('weather', '0003_rename_location_name_regionalweather_name'),
]

operations = [
migrations.CreateModel(
name='CurrentWeather',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('weather_station_name', models.CharField(max_length=100)),
('elevation', models.IntegerField(null=True)),
('location_description', models.TextField(null=True)),
('datasets', models.JSONField(default=[])),
],
options={
'abstract': False,
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.3 on 2024-02-26 17:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('weather', '0004_currentweather'),
]

operations = [
migrations.AlterField(
model_name='currentweather',
name='datasets',
field=models.JSONField(default=[], null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.3 on 2024-02-29 23:27

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('weather', '0001_squashed_0005_alter_currentweather_datasets'),
]

operations = [
migrations.AddField(
model_name='currentweather',
name='location_latitude',
field=models.CharField(max_length=10, null=True),
),
migrations.AddField(
model_name='currentweather',
name='location_longitude',
field=models.CharField(max_length=10, null=True),
),
]
Loading
Loading