Notes about development of ATOM/RSS feeds for storm surge alerts. Started in the process of working out the details of how to provide a feed to Port Metro Vancouver during the 2015/2016 storm surge season. Consideration is also given to the possibility of providing a wider collection of feeds in the future.
Port Metro Vancouver presently consumes Environment Canada feeds, so we use that as our pattern.
Feeds are generically referred to as RSS, but there are 2 current standards for them: ATOM and RSS-2.0. EC uses ATOM, and it seems to be somewhat more favourable technically, so that's what we'll focus on initially.
The IETF standard for ATOM is RFC 4287.
The Vancouver weather forecast web page has links to the weather forecast feed, and the weather alerts feed. Using the Firefox "View Page Source" function on the page that loads from either of those feed links lets you see the structure of the feed content.
Other useful links:
A blog post about creating ATOM ID elements
A blog post (linked from the above one) about using tag URIs as ATOM IDs
The Python feedgen
package documentation,
Github repository, and PyPI page.
The storm surge alerts feeds will be located under http://salishsea.eos.ubc.ca/storm-surge/.
The initial ATOM feed for Port Metro Vancouver (PMV) will be http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml.
That pattern can be extended to provide other feed like:
based on Ben's narrative template developed during the Aug-2015 sprint: http://salishsea.eos.ubc.ca/storm-surge/atom/alerts.xml
This URL structure also allows for the possibility of generating RSS-2.0 feeds in the future at locations like:
Even though the PMV and Point Atkinson feeds are for the same geolocation and are expressions of the same model results, they are treated separately so that the PMV feed can be customized with units, threshold levels, etc. requested by that stakeholder.
EC appears to generate a new feed each time the forecast page is updated (so, hourly, at least) rather than appeding new feed entries to an existing feed. We will adopt the same practice for the PMV feed:
we will only generate feed entries when the Point Atkinson water level is forecast to exceed the risk and extreme risk thresholds
We'll use the feedgen
package
to construct the feed and its entries and store them to disk.
from pprint import pprint
import arrow
from feedgen.feed import FeedGenerator
fg = FeedGenerator()
utcnow = arrow.utcnow()
fg.title('Salish Sea NEMO Model Storm Surge Alerts for Port Metro Vancouver')
fg.id(
'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/{utcnow}'
.format(utcnow=utcnow.format('YYYYMMDDHHmmss')))
fg.language('en-ca')
fg.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')
fg.rights(
'Copyright {this_year}, Salish Sea MEOPAR Project Contributors and The University of British Columbia'
.format(this_year=utcnow.year))
fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml', rel='self', type='application/atom+xml')
fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/forecast.html', rel='related', type='text/html')
pprint(fg.atom_str(pretty=True).decode('ascii'))
("<?xml version='1.0' encoding='UTF-8'?>\n" '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-ca">\n' ' ' '<id>tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151222160527</id>\n' ' <title>Salish Sea NEMO Model Storm Surge Alerts for Port Metro ' 'Vancouver</title>\n' ' <updated>2015-12-22T16:05:27.801607+00:00</updated>\n' ' <author>\n' ' <name>Salish Sea MEOPAR Project</name>\n' ' <url>http://salishsea.eos.ubc.ca/</url>\n' ' </author>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml" ' 'rel="self" type="application/atom+xml"/>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/forecast.html" ' 'rel="related" type="text/html"/>\n' ' <generator version="0.3.2">python-feedgen</generator>\n' ' <rights>Copyright 2015, Salish Sea MEOPAR Project Contributors and The ' 'University of British Columbia</rights>\n' '</feed>\n')
Instead of pretty-printing the ASCII version of the feed, the production code will save it as binary data in a file with:
fg.atom_file(os.path.join(path_to_storm_surge, 'atom', 'pmv.xml')
The only really interesting bit in the code above is the calculation of the id
element for the feed.
Quoting Mark Pilgrim's blog post:
There are three requirements for an Atom ID:
- The ID must be a valid URI, as defined by RFC 2396.
- The ID must be globally unique, across all Atom feeds, everywhere, for all time. This part is actually easier than it sounds.
- The ID must never, ever change.
We use the tag URI
technique to create our id
with the EC tactic of appending the UTC feed create date/time
as a string of digits:
tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151221020629
This tag is composed of:
salishsea.eos.ubc.ca
2015-12-12
.xml
extension: /storm-surge/atom/pmv
/20151221020629
The feed above will be generated after every forecast and forecast2 run
by the make_feeds
worker between the successful completion of the
make_site_page forecast publish
worker and the launch of the push_to_web
worker.
If the Point Atkinson water level is forecast to exceed the risk and extreme risk thresholds,
the make_feeds
worker will also add an entry to the feed.
Apart from the calculation of the alert text for the entry,
its creation looks like:
fe = fg.add_entry()
now = arrow.now()
fe.title('Storm Surge Alert for Point Atkinson')
fe.id(
'tag:salishsea.eos.ubc.ca,{today}:/storm-surge/atom/pmv/{now}'
.format(
today=now.format('YYYY-MM-DD'),
now=now.format('YYYYMMDDHHmmss')))
fe.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')
fe.content('', type='html')
fe.link(href='salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html', rel='alternate', type='text/html')
pprint(fg.atom_str(pretty=True).decode('ascii'))
("<?xml version='1.0' encoding='UTF-8'?>\n" '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-ca">\n' ' ' '<id>tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151222160527</id>\n' ' <title>Salish Sea NEMO Model Storm Surge Alerts for Port Metro ' 'Vancouver</title>\n' ' <updated>2015-12-22T16:05:27.801607+00:00</updated>\n' ' <author>\n' ' <name>Salish Sea MEOPAR Project</name>\n' ' <url>http://salishsea.eos.ubc.ca/</url>\n' ' </author>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml" ' 'rel="self" type="application/atom+xml"/>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/forecast.html" ' 'rel="related" type="text/html"/>\n' ' <generator version="0.3.2">python-feedgen</generator>\n' ' <rights>Copyright 2015, Salish Sea MEOPAR Project Contributors and The ' 'University of British Columbia</rights>\n' ' <entry>\n' ' ' '<id>tag:salishsea.eos.ubc.ca,2015-12-22:/storm-surge/atom/pmv/20151222080553</id>\n' ' <title>Storm Surge Alert for Point Atkinson</title>\n' ' <updated>2015-12-22T16:05:53.366853+00:00</updated>\n' ' <author>\n' ' <name>Salish Sea MEOPAR Project</name>\n' ' <url>http://salishsea.eos.ubc.ca/</url>\n' ' </author>\n' ' <content type="html"/>\n' ' <link ' 'href="salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html" ' 'rel="alternate" type="text/html"/>\n' ' </entry>\n' '</feed>\n')
Each entry also requires a unique id
.
Here we use a similar algorithm to the feed id
except that the date/time is local rather than UTC:
tag:salishsea.eos.ubc.ca,2015-12-20:/storm-surge/atom/pmv/20151220180702
This tag is composed of:
salishsea.eos.ubc.ca
2015-12-20
.xml
extension: /storm-surge/atom/pmv
/20151220180702
summary
Element¶The summary
element of the feed entry for a forecast for which there is
a water level alert condition will be something like:
STORM SURGE ADVISORY!
Extreme sea levels expected for the marine areas of Vancouver
Synopsis:
Strong winds over the northeast Pacific Ocean are expected
to produce elevated sea levels near Vancouver early Sunday morning.
These elevated sea levels may present a flood risk to
coastal structures and communities at high tide.
Point Atkinson
Maximum Water Level: 5.37 m above chart datum
Wind: 33.67 km/hr (18.18 knots) from the W (276°)
Time: Dec 13, 2015 07:22 [PST]
Wind speed and direction are average over the 4 hours preceding
the maximum water level to give information regarding wave setup
that may augment flood risks.
Things that need to be calculated for this message:
storm surge forecast page graphic
The summary
element can be either plain text for HTML.
We probably need to use the latter to get formatting like above.
That opens the possibility of font faces (bold, italic, ...)
and inclusion of a link to the appropriate forecast page,
although the rel="alternate"
link
element may provide that.
Most of the code that we need to calculate that text is available in the SalishSeaNowcast
package,
in particular,
in the nowcast.figures
module.
There is also a Mako template that generates RST in SalishSeaNowcast/nowcast/www/templates/surgetext.mako
.
import datetime
import os
import netCDF4 as nc
import numpy as np
from salishsea_tools import (
nc_tools,
stormtools,
unit_conversions,
wind_tools,
)
from salishsea_tools.places import PLACES
from nowcast import figures
The next cell mounts the /results
filesystem on skookum
locally.
It is intended for use if when this notebook is run on a laptop or other
non-Waterhole machine that has sshfs
installed.
Don't execute the cell if that doesn't describe your situation.
!sshfs skookum:/results results
The nowcast.figures.plot_threshold_website()
has code
(reproduced below) that calculates:
grid_T
run results filesite_name = 'Point Atkinson'
grid_T_15m = nc.Dataset('results/SalishSea/forecast/20dec15/{}.nc'.format(site_name.replace(' ', '')))
tidal_predictions = 'results/nowcast-sys/tools/SalishSeaNowcast/nowcast/tidal_predictions/'
weather_path = 'results/forcing/atmospheric/GEM2.5/operational/fcst'
ssh_model, t_model = figures.load_model_ssh(grid_T_15m)
ttide = figures.get_tides(site_name, tidal_predictions)
ssh_corr = figures.correct_model_ssh(ssh_model, t_model, ttide)
residual = figures.compute_residual(ssh_corr, t_model, ttide)
max_ssh, _, max_ssh_time, _, max_ssh_wind, i_max_ssh_wind = figures.get_maxes(
ssh_corr, t_model, residual,
figures.SITES[site_name]['lon'], figures.SITES[site_name]['lat'],
weather_path)
During development of the nowcast.workers.make_feeds()
worker
the above code was refactored to:
site_name = 'Point Atkinson'
grid_T_15m = nc.Dataset('results/SalishSea/forecast/20dec15/{}.nc'.format(site_name.replace(' ', '')))
tidal_predictions = 'results/nowcast-sys/tools/SalishSeaNowcast/nowcast/tidal_predictions/'
ssh_model, t_model = nc_tools.ssh_timeseries_at_point(grid_T_15m, 0, 0, datetimes=True)
ttide = figures.get_tides(site_name, tidal_predictions)
ssh_corr = figures.correct_model_ssh(ssh_model, t_model, ttide)
max_ssh = np.max(ssh_corr) + figures.SITES[site_name]['msl']
max_ssh_time = t_model[np.argmax(ssh_corr)]
From those results we can calculate:
max_ssh, arrow.get(max_ssh_time).to('local')
(4.997162480377197, <Arrow [2015-12-21T13:07:30-08:00]>)
Formating the date/time would be easy if it weren't for adding the timezone name:
a = arrow.get(max_ssh_time).to('local')
'{datetime} [{tzname}]'.format(datetime=a.format('ddd MMM DD, YYYY HH:mm'), tzname=a.tzinfo.tzname(a.datetime))
'Mon Dec 21, 2015 13:07 [PST]'
We also want to "humanize" the date/time into a phrase like "early Monday afternoon". There is code in the surge_warning notebook that forms the basis for a function to do that.
def humanize_time_of_day(date_time):
day_of_week = date_time.format('dddd')
if date_time.hour < 12:
part_of_day = 'morning'
early_late = 'early' if date_time.hour < 8 else 'late'
elif date_time.hour >= 12 and date_time.hour < 17:
part_of_day = 'afternoon'
early_late = 'early' if date_time.hour < 15 else 'late'
else:
part_of_day = 'evening'
early_late = 'early' if date_time.hour < 20 else 'late'
return ' '.join((early_late, day_of_week, part_of_day))
humanize_time_of_day(a)
'early Monday afternoon'
During development of the nowcast.workers.make_feeds()
worker
a slightly different version of that function that uses the same
time of day descriptions as Environment Canada does in the public
weather forecasts was implemented as
salishsea_tools.unit_conversions.humanize_time_of_day()
:
unit_conversions.humanize_time_of_day(a)
'early Monday afternoon'
Some code adapted from nowcast.figures.plot_threshold_map()
provides the risk level:
def storm_surge_risk_level(max_ssh, site_msl, max_historic_ssh, ttide):
max_tide_ssh = max(ttide.pred_all) + site_msl
extreme_threshold = max_tide_ssh + (max_historic_ssh - max_tide_ssh) / 2
risk_level = (
None if max_ssh < max_tide_ssh
else 'extreme risk' if max_ssh > extreme_threshold
else 'moderate risk')
return risk_level
The above function has been added to the stormtools
module.
risk_level = stormtools.storm_surge_risk_level(site_name, max_ssh, ttide)
print(risk_level)
moderate risk
There is code in nowcast.figures.get_model_winds()
that finds the wind component arrays
at the grid point in the weather forcing dataset at the lat-lon-time point that is closest
to the tide gauge site at the time of the maximum water level:
weather_path = 'results/forcing/atmospheric/GEM2.5/operational/fcst'
weather = nc.Dataset(os.path.join(weather_path, '{:ops_y%Ym%md%d.nc}'.format(max_ssh_time)))
weather_lats = weather.variables['nav_lat'][:]
weather_lons = weather.variables['nav_lon'][:] - 360
j, i = figures.find_model_point(
figures.SITES[site_name]['lon'],
figures.SITES[site_name]['lat'],
weather_lons, weather_lats)
u_wind = weather.variables['u_wind'][:, j, i]
v_wind = weather.variables['v_wind'][:, j, i]
Since we regularly need to get wind component values at the weather grid
points closest to tide gauge stations,
we'll put the grid indices in the salishsea_tools.places.PLACES
data structure.
print(j, i)
print(PLACES[site_name]['wind grid ji'])
[146] [155] (146, 155)
Having done that we can use the salishsea_tools.nc_tools.uv_wind_timeseries_at_point()
function to get the wind component time series:
wind = nc_tools.uv_wind_timeseries_at_point(weather, *PLACES[site_name]['wind grid ji'])
nowcast.figures.get_maxes()
above gave us i_max_ssh_wind
, the index in the wind component arrays
of the hour in which the maximum water level occurs.
i_max_ssh_wind = np.asscalar(i_max_ssh_wind)
As a result of the development refactoring we're no longer using
nowcast.figures.get_maxes()
to provide i_max_ssh_wind
.
Instead we'll just grab the code that calculates it:
i_max_ssh_wind = np.asscalar(
np.where(
wind.time == arrow.get(
max_ssh_time.year, max_ssh_time.month, max_ssh_time.day, max_ssh_time.hour))[0])
print(i_max_ssh_wind)
21
We use that to calculate the average wind components in the 4 hours preceding that hour:
u_wind_4h_avg = np.mean(u_wind[i_max_ssh_wind-4:i_max_ssh_windax_ssh_wind])
v_wind_4h_avg = np.mean(v_wind[i_max_ssh_wind-4:i_max_ssh_wind])
u_wind_4h_avg, v_wind_4h_avg
(-4.4057636, -0.36821717)
# Calculating speed and direction arrays from components values or arrays
# should probably be a library function in the planned `salishsea_tools.wind_tools` module
def wind_speed_dir(u_wind, v_wind):
wind_speed = np.sqrt(u_wind**2 + v_wind**2)
wind_dir = np.arctan2(v_wind, u_wind)
wind_dir = np.rad2deg(wind_dir + (wind_dir < 0) * 2 * np.pi)
return wind_speed, wind_dir
wind_speed_4h_avg, wind_dir_4h_avg = wind_speed_dir(u_wind_4h_avg, v_wind_4h_avg)
wind_speed_4h_avg, wind_dir_4h_avg
(4.4211239536471059, 184.77746642865748)
wind_speed_4h_avg, wind_dir_4h_avg = wind_tools.wind_speed_dir(u_wind_4h_avg, v_wind_4h_avg)
wind_speed_4h_avg, wind_dir_4h_avg
(4.4211239536471059, 184.77746642865748)
Conversion of the wind speed from m/s to km/hr and knots is easily
accomplished with some conversion factors and functions that should be
added to a module in the SalishSeaTools
package:
M_PER_S__KM_PER_HR = 3600 / 1000
M_PER_S__KNOTS = 3600 / 1852
def mps_kph(m_per_s):
return m_per_s * M_PER_S__KM_PER_HR
def mps_knots(m_per_s):
return m_per_s * M_PER_S__KNOTS
Those conversion factors and functions are implemented in the salishsea_tools.unit_conversions
module.
unit_conversions.mps_kph(wind_speed_4h_avg), unit_conversions.mps_knots(wind_speed_4h_avg)
(15.916046233129581, 8.5939774476941579)
We need a function that converts wind bearings from the "wind to" orientation that physicists use to the "wind from" orientation that people are used to:
def wind_to_from(wind_to):
return 270 - wind_to if wind_to <= 270 else 270 - wind_to + 360
This function is also now implemented as salishsea_tools.unit_converstions.wind_to_from()
.
unit_conversions.wind_to_from(wind_dir_4h_avg)
85.222533571342524
We also need a function that converts compass bearings to compass headings:
def bearing_heading(
bearing,
headings=['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'],
):
return headings[int(round(bearing * (len(headings) - 1) / 360, 0))]
This function is available as salishsea_tools.unit_conversions.bearing_heading()
:
unit_conversions.bearing_heading(unit_conversions.wind_to_from(wind_dir_4h_avg))
'E'
summary
Element¶We'll start with a reStructuredText template based on SalishSeaNowcast/nowcast/www/templates/surgetext.mako
:
import docutils.core
import IPython.display
import mako.template
template = """
**STORM SURGE ADVISORY**
${'Extreme' if 'extreme' in conditions[tide_gauge_stn]['risk_level'] else 'Elevated'} sea levels expected for the marine areas of ${city}
**Synopsis**:
Strong winds over the northeast Pacific Ocean are expected
to produce elevated sea levels near ${city} ${conditions[tide_gauge_stn]['humanized_max_ssh_time']}.
These elevated sea levels may present a flood risk to
coastal structures and communities at high tide.
${tide_gauge_stn_info(tide_gauge_stn, conditions)}
Wind speed and direction are averages over the 4 hours preceding
the maximum water level to give information regarding wave setup
that may augment flood risks.
<%def name="tide_gauge_stn_info(stn, conditions)">
**${stn}**
**Risk Level:** ${conditions[stn]['risk_level'].title()}
**Maximum Water Level:** ${round(conditions[stn]['max_ssh_msl'], 1)} m above chart datum
**Wind:** ${int(round(conditions[stn]['wind_speed_4h_avg_kph'], 0))} km/hr (${int(round(conditions[stn]['wind_speed_4h_avg_knots'], 0))} knots) from the ${conditions[stn]['wind_dir_4h_avg_heading']} (${int(round(conditions[stn]['wind_dir_4h_avg_bearing'], 0))}°)
**Time:** ${conditions[stn]['max_ssh_time'].format('ddd MMM DD, YYYY HH:mm')} [${conditions[stn]['max_ssh_time_tzname']}]
</%def>
"""
max_ssh_time_local = arrow.get(max_ssh_time).to('local')
values = {
'city': 'Vancouver',
'tide_gauge_stn': 'Point Atkinson',
'conditions': {
'Point Atkinson': {
'risk_level': risk_level,
'max_ssh_msl': max_ssh_msl,
'wind_speed_4h_avg_kph': mps_kph(wind_speed_4h_avg),
'wind_speed_4h_avg_knots': mps_knots(wind_speed_4h_avg),
'wind_dir_4h_avg_heading': bearing_heading(wind_to_from(wind_dir_4h_avg)),
'wind_dir_4h_avg_bearing': wind_to_from(wind_dir_4h_avg),
'max_ssh_time': max_ssh_time_local,
'max_ssh_time_tzname': max_ssh_time_local.tzinfo.tzname(max_ssh_time_local.datetime),
'humanized_max_ssh_time': humanize_time_of_day(max_ssh_time_local),
},
},
}
rendered_rst = mako.template.Template(template).render(**values)
print(rendered_rst)
**STORM SURGE ADVISORY** Elevated sea levels expected for the marine areas of Vancouver **Synopsis**: Strong winds over the northeast Pacific Ocean are expected to produce elevated sea levels near Vancouver early Monday afternoon. These elevated sea levels may present a flood risk to coastal structures and communities at high tide. **Point Atkinson** **Risk Level:** Moderate Risk **Maximum Water Level:** 5.0 m above chart datum **Wind:** 16 km/hr (9 knots) from the E (85°) **Time:** Mon Dec 21, 2015 13:07 [PST] Wind speed and direction are averages over the 4 hours preceding the maximum water level to give information regarding wave setup that may augment flood risks.
Rendering the RST to HTML:
parts = docutils.core.publish_parts(rendered_rst, writer_name='html')
IPython.display.HTML(parts['body'])
STORM SURGE ADVISORY
Elevated sea levels expected for the marine areas of Vancouver
Synopsis: Strong winds over the northeast Pacific Ocean are expected to produce elevated sea levels near Vancouver early Monday afternoon. These elevated sea levels may present a flood risk to coastal structures and communities at high tide.
Point Atkinson
Risk Level: Moderate Risk
Maximum Water Level: 5.0 m above chart datum
Wind: 16 km/hr (9 knots) from the E (85°)
Time: Mon Dec 21, 2015 13:07 [PST]
Wind speed and direction are averages over the 4 hours preceding the maximum water level to give information regarding wave setup that may augment flood risks.
Finally, we have everything we need to generate the feed and an entry if the model is predicting a storm surge risk:
fg = FeedGenerator()
utcnow = arrow.utcnow()
fg.title('Salish Sea NEMO Model Storm Surge Alerts for Port Metro Vancouver')
fg.id(
'tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/{utcnow}'
.format(utcnow=utcnow.format('YYYYMMDDHHmmss')))
fg.language('en-ca')
fg.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')
fg.rights(
'Copyright {this_year}, Salish Sea MEOPAR Project Contributors and The University of British Columbia'
.format(this_year=utcnow.year))
fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml', rel='self', type='application/atom+xml')
fg.link(href='http://salishsea.eos.ubc.ca/storm-surge/forecast.html', rel='related', type='text/html')
if risk_level is not None:
rendered_rst = mako.template.Template(template).render(**values)
html = docutils.core.publish_parts(rendered_rst, writer_name='html')
now = arrow.now()
fe = fg.add_entry()
fe.title('Storm Surge Alert for Point Atkinson')
fe.id(
'tag:salishsea.eos.ubc.ca,{today}:/storm-surge/atom/pmv/{now}'
.format(
today=now.format('YYYY-MM-DD'),
now=now.format('YYYYMMDDHHmmss')))
fe.author(name='Salish Sea MEOPAR Project', uri='http://salishsea.eos.ubc.ca/')
fe.content(html['body'], type='html')
fe.link(href='http://salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html', rel='alternate', type='text/html')
pprint(fg.atom_str(pretty=True).decode('utf8'))
("<?xml version='1.0' encoding='UTF-8'?>\n" '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-ca">\n' ' ' '<id>tag:salishsea.eos.ubc.ca,2015-12-12:/storm-surge/atom/pmv/20151221053237</id>\n' ' <title>Salish Sea NEMO Model Storm Surge Alerts for Port Metro ' 'Vancouver</title>\n' ' <updated>2015-12-21T05:32:37.988672+00:00</updated>\n' ' <author>\n' ' <name>Salish Sea MEOPAR Project</name>\n' ' <url>http://salishsea.eos.ubc.ca/</url>\n' ' </author>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/atom/pmv.xml" ' 'rel="self" type="application/atom+xml"/>\n' ' <link href="http://salishsea.eos.ubc.ca/storm-surge/forecast.html" ' 'rel="related" type="text/html"/>\n' ' <generator version="0.3.2">python-feedgen</generator>\n' ' <rights>Copyright 2015, Salish Sea MEOPAR Project Contributors and The ' 'University of British Columbia</rights>\n' ' <entry>\n' ' ' '<id>tag:salishsea.eos.ubc.ca,2015-12-20:/storm-surge/atom/pmv/20151220213237</id>\n' ' <title>Storm Surge Alert for Point Atkinson</title>\n' ' <updated>2015-12-21T05:32:38.000038+00:00</updated>\n' ' <author>\n' ' <name>Salish Sea MEOPAR Project</name>\n' ' <url>http://salishsea.eos.ubc.ca/</url>\n' ' </author>\n' ' <content type="html"><p><strong>STORM SURGE ' 'ADVISORY</strong></p>\n' '<p>Elevated sea levels expected for the marine areas of ' 'Vancouver</p>\n' '<p><strong>Synopsis</strong>:\n' 'Strong winds over the northeast Pacific Ocean are expected\n' 'to produce elevated sea levels near Vancouver early Monday afternoon.\n' 'These elevated sea levels may present a flood risk to\n' 'coastal structures and communities at high tide.</p>\n' '<p><strong>Point Atkinson</strong></p>\n' '<p><strong>Risk Level:</strong> Moderate Risk</p>\n' '<p><strong>Maximum Water Level:</strong> 5.0 m above chart ' 'datum</p>\n' '<p><strong>Wind:</strong> 16 km/hr (9 knots) from the E ' '(85°)</p>\n' '<p><strong>Time:</strong> Mon Dec 21, 2015 13:07 ' '[PST]</p>\n' '<p>Wind speed and direction are averages over the 4 hours preceding\n' 'the maximum water level to give information regarding wave setup\n' 'that may augment flood risks.</p>\n' '</content>\n' ' <link ' 'href="http://salishsea.eos.ubc.ca/nemo/results/forecast/publish_20dec15.html" ' 'rel="alternate" type="text/html"/>\n' ' </entry>\n' '</feed>\n')
To store the feed in a file instead of rendering it to a string use:
fg.atom_file('pmv.xml')
Now the code in this notebook will be used to create a nowcast.worker
module that
will generate feeds and store them in the appropriate locations after every
forecast and forecast2 run.