noaa_weather_client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +7 -0
  7. data/.yardopts +2 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +107 -0
  11. data/Rakefile +8 -0
  12. data/bin/noaa_weather_client +43 -0
  13. data/data/xml/current_observation.xsd +79 -0
  14. data/data/xml/dwml.xsd +97 -0
  15. data/data/xml/location.xsd +142 -0
  16. data/data/xml/meta_data.xsd +100 -0
  17. data/data/xml/moreWeatherInformation.xsd +23 -0
  18. data/data/xml/ndfd_data.xsd +43 -0
  19. data/data/xml/parameters.xsd +1173 -0
  20. data/data/xml/summarizationType.xsd +29 -0
  21. data/data/xml/time_layout.xsd +51 -0
  22. data/lib/noaa_weather_client.rb +9 -0
  23. data/lib/noaa_weather_client/cli.rb +53 -0
  24. data/lib/noaa_weather_client/cli/templates.rb +53 -0
  25. data/lib/noaa_weather_client/client.rb +61 -0
  26. data/lib/noaa_weather_client/errors.rb +7 -0
  27. data/lib/noaa_weather_client/responses/current_observation.rb +93 -0
  28. data/lib/noaa_weather_client/responses/forecast.rb +84 -0
  29. data/lib/noaa_weather_client/responses/generic_response.rb +9 -0
  30. data/lib/noaa_weather_client/responses/lat_lon_list.rb +25 -0
  31. data/lib/noaa_weather_client/responses/reactive_xml_response.rb +29 -0
  32. data/lib/noaa_weather_client/responses/station.rb +28 -0
  33. data/lib/noaa_weather_client/responses/stations.rb +41 -0
  34. data/lib/noaa_weather_client/responses/validatable_xml_response.rb +22 -0
  35. data/lib/noaa_weather_client/rest_client_factory.rb +12 -0
  36. data/lib/noaa_weather_client/services/calculate_distance_between_lat_lon.rb +20 -0
  37. data/lib/noaa_weather_client/services/current_observations.rb +32 -0
  38. data/lib/noaa_weather_client/services/find_nearest_station.rb +16 -0
  39. data/lib/noaa_weather_client/services/forecast_by_day.rb +52 -0
  40. data/lib/noaa_weather_client/services/postal_code_to_coordinate.rb +36 -0
  41. data/lib/noaa_weather_client/services/rest_service.rb +28 -0
  42. data/lib/noaa_weather_client/services/soap_service.rb +16 -0
  43. data/lib/noaa_weather_client/services/weather_stations.rb +32 -0
  44. data/lib/noaa_weather_client/soap_client_factory.rb +17 -0
  45. data/lib/noaa_weather_client/station_filters.rb +8 -0
  46. data/lib/noaa_weather_client/version.rb +3 -0
  47. data/lib/noaa_weather_client/xml_parser_factory.rb +9 -0
  48. data/noaa_weather_client.gemspec +27 -0
  49. data/spec/fixtures/vcr_cassettes/current_observations.yml +25890 -0
  50. data/spec/fixtures/vcr_cassettes/forecast_by_day_3.yml +772 -0
  51. data/spec/fixtures/vcr_cassettes/forecast_by_day_7.yml +829 -0
  52. data/spec/fixtures/vcr_cassettes/nearest_weather_station.yml +25842 -0
  53. data/spec/fixtures/vcr_cassettes/postal_code_to_coordinate.yml +75 -0
  54. data/spec/fixtures/vcr_cassettes/weather_stations.yml +25842 -0
  55. data/spec/fixtures/xml/forecast.xml +144 -0
  56. data/spec/lib/noaa_client/client_spec.rb +93 -0
  57. data/spec/lib/noaa_client/responses/current_observation_spec.rb +122 -0
  58. data/spec/lib/noaa_client/responses/forecast_spec.rb +66 -0
  59. data/spec/lib/noaa_client/responses/lat_lon_list_spec.rb +30 -0
  60. data/spec/lib/noaa_client/responses/station_spec.rb +53 -0
  61. data/spec/lib/noaa_client/responses/stations_spec.rb +86 -0
  62. data/spec/lib/noaa_client/rest_client_factory_spec.rb +15 -0
  63. data/spec/lib/noaa_client/services/calculate_distance_between_lat_lon_spec.rb +16 -0
  64. data/spec/lib/noaa_client/services/current_observations_spec.rb +47 -0
  65. data/spec/lib/noaa_client/services/find_nearest_station_spec.rb +36 -0
  66. data/spec/lib/noaa_client/services/forecast_by_day_spec.rb +62 -0
  67. data/spec/lib/noaa_client/services/postal_code_to_coordinate_spec.rb +41 -0
  68. data/spec/lib/noaa_client/services/rest_service_spec.rb +45 -0
  69. data/spec/lib/noaa_client/services/soap_service_spec.rb +56 -0
  70. data/spec/lib/noaa_client/services/weather_stations_spec.rb +40 -0
  71. data/spec/lib/noaa_client/soap_client_factory_spec.rb +13 -0
  72. data/spec/lib/noaa_client/xml_parser_factory_spec.rb +14 -0
  73. data/spec/spec_helper.rb +31 -0
  74. metadata +228 -0
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0"?>
2
+
3
+ <!-- **********************************************************************
4
+
5
+ summarizationType.xsd
6
+
7
+ John L. Schattel MDL 29 June 2009
8
+ Red Hat Linux Apache Server
9
+
10
+
11
+ ************************************************************************* -->
12
+
13
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
14
+
15
+ <xsd:simpleType name="summarizationType">
16
+ <xsd:restriction base="xsd:string">
17
+ <xsd:enumeration value="none" />
18
+ <xsd:enumeration value="mean" />
19
+ <xsd:enumeration value="maximum" />
20
+ <xsd:enumeration value="minimum" />
21
+ <xsd:enumeration value="12hourly" />
22
+ <xsd:enumeration value="24hourly" />
23
+ <xsd:enumeration value="national" />
24
+ <xsd:enumeration value="conus" />
25
+ <xsd:enumeration value="alaska" />
26
+ </xsd:restriction>
27
+ </xsd:simpleType>
28
+
29
+ </xsd:schema>
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0"?>
2
+
3
+ <!-- **********************************************************************
4
+
5
+ time_layout.xsd
6
+
7
+ John L. Schattel MDL 4 August 2004
8
+ Red Hat Linux Apache Server
9
+
10
+ <xsd:include schemaLocation="http://graphical.weather.gov/xml/DWMLgen/schema/summarizationType.xsd" />
11
+
12
+ ************************************************************************* -->
13
+
14
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
15
+
16
+ <xsd:include schemaLocation="summarizationType.xsd" />
17
+
18
+ <xsd:complexType name="time-layoutElementType">
19
+ <xsd:sequence>
20
+ <xsd:element name="layout-key" type="layout-keyType" minOccurs="1" maxOccurs="1" />
21
+ <xsd:sequence minOccurs="1" maxOccurs="unbounded">
22
+ <xsd:element name="start-valid-time" type="start-valid-timeType" minOccurs="1" maxOccurs="unbounded" />
23
+ <xsd:element name="end-valid-time" type="xsd:dateTime" minOccurs="0" maxOccurs="unbounded" />
24
+ </xsd:sequence>
25
+ </xsd:sequence>
26
+ <xsd:attribute name="time-coordinate" type="time-coordinateType" use="required" />
27
+ <xsd:attribute name="summarization" type="summarizationType" use="optional" />
28
+ </xsd:complexType>
29
+
30
+ <xsd:simpleType name="time-coordinateType">
31
+ <xsd:restriction base="xsd:string">
32
+ <xsd:enumeration value="UTC" />
33
+ <xsd:enumeration value="local" />
34
+ </xsd:restriction>
35
+ </xsd:simpleType>
36
+
37
+ <xsd:complexType name="start-valid-timeType">
38
+ <xsd:simpleContent>
39
+ <xsd:extension base="xsd:dateTime">
40
+ <xsd:attribute name="period-name" type="xsd:string" />
41
+ </xsd:extension>
42
+ </xsd:simpleContent>
43
+ </xsd:complexType>
44
+
45
+ <xsd:simpleType name="layout-keyType">
46
+ <xsd:restriction base="xsd:string">
47
+ <xsd:pattern value="k-p\d+[h|d|m|y]-n\d+-\d+" />
48
+ </xsd:restriction>
49
+ </xsd:simpleType>
50
+
51
+ </xsd:schema>
@@ -0,0 +1,9 @@
1
+ require "noaa_weather_client/version"
2
+ require_relative 'noaa_weather_client/client'
3
+ require_relative 'noaa_weather_client/cli'
4
+
5
+ module NoaaWeatherClient
6
+ def self.build_client
7
+ Client.new
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ require 'noaa_weather_client/cli/templates'
2
+
3
+ module NoaaWeatherClient
4
+ class CLI
5
+ def self.postal_code_to_coordinate(postal_code, buffer = STDOUT)
6
+ client = NoaaWeatherClient.build_client
7
+ coordinate = client.postal_code_to_coordinate postal_code
8
+ buffer.puts Templates::PostalCode.new(coordinate).to_s
9
+ end
10
+
11
+ attr_writer :buffer
12
+
13
+ def initialize(latitude, longitude)
14
+ @coordinate = Coordinate.new(latitude, longitude)
15
+ raise ArgumentError, "Invalid coordinate #{latitude}, #{longitude}" unless coordinate.valid?
16
+ end
17
+
18
+ def render(*features)
19
+ features.each { |f| render_feature buffer, f }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :coordinate
25
+
26
+ def render_feature(buffer, feature)
27
+ if feature.to_s == 'observations'
28
+ observations = client.current_observations(coordinate.latitude, coordinate.longitude)
29
+ buffer.puts Templates::CurrentObservations.new(observations).to_s
30
+ elsif feature.to_s == 'forecast'
31
+ forecast = client.forecast_by_day(coordinate.latitude, coordinate.longitude)
32
+ buffer.puts Templates::Forecast.new(forecast).to_s
33
+ end
34
+ end
35
+
36
+ def buffer
37
+ @buffer || STDOUT
38
+ end
39
+
40
+ def client
41
+ @client ||= NoaaWeatherClient.build_client
42
+ end
43
+
44
+ Coordinate = Struct.new(:latitude, :longitude) do
45
+ def valid?
46
+ latitude && longitude rescue false
47
+ end
48
+
49
+ def latitude; Float(self[:latitude]) end
50
+ def longitude; Float(self[:longitude]) end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ module NoaaWeatherClient
2
+ module Templates
3
+ CurrentObservations = Struct.new(:observation) do
4
+ def to_s
5
+ <<-template
6
+ Current Observations
7
+ ================================================================================
8
+ StationID: #{observation.station_id}
9
+ Station Name: #{observation.location}
10
+ Location: #{observation.latitude}, #{observation.longitude}
11
+ Observation Time: #{observation.observation_time_string}
12
+ Temperature: #{observation.temperature_fahrenheit} | #{observation.temperature_celsius}
13
+ Dewpoint: #{observation.dewpoint_fahrenheit} | #{observation.dewpoint_celsius}
14
+ Pressure: #{observation.pressure_in} in | #{observation.pressure_mb} mb
15
+ Humidity: #{observation.relative_humidity}
16
+ Wind: #{observation.wind_string}
17
+ Wind Direction: #{observation.wind_dir} | #{observation.wind_degrees} degrees
18
+ Wind Speed: #{observation.wind_mph} mph | #{observation.wind_kt} kt
19
+ Visibility: #{observation.visibility_mi} mi
20
+
21
+ template
22
+ end
23
+ end
24
+
25
+ PostalCode = Struct.new(:coordinate) do
26
+ def to_s
27
+ "#{coordinate.latitude} #{coordinate.longitude}"
28
+ end
29
+ end
30
+
31
+ ForecastDay = Struct.new(:day) do
32
+ def to_s
33
+ <<-template
34
+ Day: #{day.name}
35
+ --------------------------------------------------------------------------------
36
+ High: #{day.maximum_temperature} F
37
+ Low: #{day.minimum_temperature} F
38
+ Summary: #{day.weather_summary}
39
+
40
+ template
41
+ end
42
+ end
43
+
44
+ Forecast = Struct.new(:forecast) do
45
+ def to_s
46
+ template = "\n#{forecast.days.size}-Day Forecast\n"
47
+ template += "================================================================================\n"
48
+ forecast.days.each { |d| template += ForecastDay.new(d).to_s }
49
+ template
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'services/forecast_by_day'
2
+ require_relative 'services/weather_stations'
3
+ require_relative 'services/current_observations'
4
+ require_relative 'services/find_nearest_station'
5
+ require_relative 'services/postal_code_to_coordinate'
6
+
7
+ module NoaaWeatherClient
8
+ # Provides entry point for interacting with the noaa api via multiple services.
9
+ class Client
10
+ # Fetches a daily forecast for a location from today. Default is 7 days.
11
+ # The forecast for the current day ceases to be returned after 6:00pm at the
12
+ # observation area.
13
+ # @param lat [Float] latitude
14
+ # @param lon [Float] longitude
15
+ # @return [Responses::Forecast] a list of daily forecasts.
16
+ def forecast_by_day(lat, lon, options = {})
17
+ Services::ForecastByDay.new.fetch(lat, lon, options)
18
+ end
19
+
20
+ # Retrieves a list of weather stations from noaa.
21
+ # @return [Responses::Stations] a list of weather stations.
22
+ def weather_stations(options = {})
23
+ if options.delete(:reload) { false } || @weather_stations.nil?
24
+ @weather_stations = Services::WeatherStations.new(options).fetch
25
+ end
26
+ @weather_stations
27
+ end
28
+
29
+ # Retrieves the current weather observations for a location.
30
+ # @note It is important to cache a copy of the available stations for use here, as the xml stations response is quite large noaa does not appreciate repeated calls.
31
+ # @param lat [Float] latitude
32
+ # @param lon [Float] longitude
33
+ # @param [Hash] options
34
+ # @option options [Responses::Stations] stations A cached stations response to prevent having to query noaa for the list.
35
+ # @return [Responses::CurrentObservation]
36
+ def current_observations(lat, lon, options = {})
37
+ station = options.fetch(:station) { nearest_weather_station(lat, lon, options) }
38
+ Services::CurrentObservations.new(options).fetch(station)
39
+ end
40
+
41
+ # Retrieves the weather station nearest to the location.
42
+ # @note It is important to cache a copy of the available stations for use here, as the xml stations response is quite large noaa does not appreciate repeated calls.
43
+ # @param lat [Float] latitude
44
+ # @param lon [Float] longitude
45
+ # @param [Hash] options
46
+ # @option options [Responses::Stations] stations A cached stations response to prevent having to query noaa for the list.
47
+ # @return [Responses::Station]
48
+ def nearest_weather_station(lat, lon, options = {})
49
+ stations = options.fetch(:stations) { weather_stations }
50
+ Services::FindNearestStation.find(lat, lon, stations)
51
+ end
52
+
53
+ # Converts a zip code to a latitude and longitude.
54
+ # @param lat [Float] latitude
55
+ # @param lon [Float] longitude
56
+ # @return [Responses::LatLonList]
57
+ def postal_code_to_coordinate(postal_code, options = {})
58
+ Services::PostalCodeToCoordinate.new(options).resolve(postal_code)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ module NoaaWeatherClient
2
+ module Errors
3
+ class Error < StandardError; end
4
+ class CommunicationError < Error; end
5
+ class InvalidXmlError < ArgumentError; end
6
+ end
7
+ end
@@ -0,0 +1,93 @@
1
+ require_relative '../xml_parser_factory'
2
+ require_relative 'reactive_xml_response'
3
+ require_relative 'validatable_xml_response'
4
+
5
+ module NoaaWeatherClient
6
+ module Responses
7
+ class CurrentObservation
8
+ include ReactiveXmlResponse
9
+ include ValidatableXmlResponse
10
+
11
+ def initialize(response)
12
+ @source = XmlParserFactory.build_parser.parse response
13
+ validate! @source, :current_observation
14
+ init
15
+ end
16
+
17
+ def temperature_celsius
18
+ @temp_c.to_f
19
+ end
20
+
21
+ def temperature_fahrenheit
22
+ @temp_f.to_f
23
+ end
24
+
25
+ def dewpoint_celsius
26
+ @dewpoint_c.to_f
27
+ end
28
+
29
+ def dewpoint_fahrenheit
30
+ @dewpoint_f.to_f
31
+ end
32
+
33
+ def latitude
34
+ @latitude.to_f
35
+ end
36
+
37
+ def longitude
38
+ @longitude.to_f
39
+ end
40
+
41
+ def observation_time_string
42
+ @observation_time
43
+ end
44
+
45
+ def observation_time
46
+ Time.parse(@observation_time_rfc822.to_s) if @observation_time_rfc822
47
+ end
48
+
49
+ def pressure_in
50
+ @pressure_in.to_f
51
+ end
52
+
53
+ def pressure_mb
54
+ @pressure_mb.to_f
55
+ end
56
+
57
+ def relative_humidity
58
+ @relative_humidity.to_f
59
+ end
60
+
61
+ def wind_degrees
62
+ @wind_degrees.to_f
63
+ end
64
+
65
+ def wind_kt
66
+ @wind_kt.to_f
67
+ end
68
+
69
+ def wind_mph
70
+ @wind_mph.to_f
71
+ end
72
+
73
+ def visibility_mi
74
+ @visibility_mi.to_f
75
+ end
76
+
77
+ def to_hash
78
+ arr = instance_variables.map { |v|
79
+ [ v.to_s[1..-1], instance_variable_get(v) ] unless v =~ /source/
80
+ }.compact
81
+ Hash[arr]
82
+ end
83
+
84
+ private
85
+
86
+ def init
87
+ source.root.elements.each do |e|
88
+ instance_variable_set("@#{e.name}", e.text)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,84 @@
1
+ require_relative '../xml_parser_factory'
2
+ require_relative 'validatable_xml_response'
3
+ require 'ostruct'
4
+
5
+ module NoaaWeatherClient
6
+ module Responses
7
+ class Forecast
8
+ include Enumerable
9
+ include ValidatableXmlResponse
10
+
11
+ def initialize(hashed_response)
12
+ @body = XmlParserFactory.build_parser.parse hashed_response[:ndf_dgen_by_day_response][:dwml_by_day_out]
13
+ validate! @body, :dwml
14
+ end
15
+
16
+ def each
17
+ days.each { |d| yield d }
18
+ end
19
+
20
+ def size
21
+ days.size
22
+ end
23
+
24
+ def days
25
+ @days ||= build_days
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :body
31
+
32
+ def time_layout
33
+ @time_layout ||= body.css("time-layout[summarization='24hourly']").first
34
+ end
35
+
36
+ def layout_key
37
+ @layout_key ||= time_layout.css('layout-key').text
38
+ end
39
+
40
+ def parameters
41
+ @paramteters ||= body.css('parameters')
42
+ end
43
+
44
+ def time_layout_periods
45
+ unless @time_layout_periods
46
+ periods = time_layout.element_children.to_a
47
+ periods.shift #remove layout-key
48
+ @time_layout_periods = [].tap do |arr|
49
+ periods.each_slice(2) do |slice|
50
+ name = slice.first['period-name']
51
+ arr << TimePeriod.new(*slice, name)
52
+ end
53
+ end
54
+ end
55
+ @time_layout_periods
56
+ end
57
+
58
+ def build_days
59
+ unless @days
60
+ @day = [].tap do |arr|
61
+ time_layout_periods.each_with_index do |p, i|
62
+ params = build_day_params p, i
63
+ arr << Day.new(params)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def build_day_params(period, index)
70
+ {
71
+ start_time: Time.parse(period.start_time.to_s),
72
+ end_time: Time.parse(period.end_time.to_s),
73
+ name: Time.parse(period.start_time.to_s).strftime("%A"),
74
+ maximum_temperature: parameters.css('temperature[type=maximum] value')[index].text.to_f,
75
+ minimum_temperature: parameters.css('temperature[type=minimum] value')[index].text.to_f,
76
+ weather_summary: parameters.css('weather weather-conditions')[index]['weather-summary']
77
+ }
78
+ end
79
+
80
+ class Day < OpenStruct; end
81
+ TimePeriod = Struct.new(:start_time, :end_time, :name)
82
+ end
83
+ end
84
+ end