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,9 @@
1
+ module NoaaWeatherClient
2
+ module Responses
3
+ class GenericResponse
4
+ def initialize(response_body)
5
+ @response_body = response_body
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
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 LatLonList
8
+ include ReactiveXmlResponse
9
+ include ValidatableXmlResponse
10
+
11
+ def initialize(response)
12
+ @source = XmlParserFactory.build_parser.parse(response)
13
+ validate! @source, :dwml
14
+ end
15
+
16
+ def latitude
17
+ @latitude ||= latLonList.split(",").first.to_f
18
+ end
19
+
20
+ def longitude
21
+ @longitude ||= latLonList.split(",").last.to_f
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ module NoaaWeatherClient
2
+ module Responses
3
+ module ReactiveXmlResponse
4
+ def method_missing(method_name, *arguments, &block)
5
+ if tag = source.css(method_name.to_s)
6
+ if block
7
+ block.call tag.text
8
+ else
9
+ tag.text
10
+ end
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ def respond_to_missing?(method_name, include_private = false)
17
+ source.css(method_name.to_s) || super
18
+ end
19
+
20
+ def source
21
+ @source || NullResponse.new
22
+ end
23
+
24
+ class NullResponse
25
+ def css(*args); end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'reactive_xml_response'
2
+
3
+ module NoaaWeatherClient
4
+ module Responses
5
+ class Station
6
+ include ReactiveXmlResponse
7
+
8
+ def initialize(station)
9
+ @source = station
10
+ end
11
+
12
+ def latitude
13
+ source.css('latitude').text.to_f
14
+ end
15
+
16
+ def longitude
17
+ source.css('longitude').text.to_f
18
+ end
19
+
20
+ def xml_url
21
+ @xml_url ||= begin
22
+ path = URI(source.css('xml_url').text).path
23
+ "http://w1.weather.gov#{path}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../xml_parser_factory'
2
+ require_relative 'station'
3
+ require 'forwardable'
4
+
5
+ module NoaaWeatherClient
6
+ module Responses
7
+ class Stations
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ def_delegators :stations, :'[]', :fetch, :sort_by!, :take, :size
12
+
13
+ def initialize(response, options = {})
14
+ @body = XmlParserFactory.build_parser.parse response
15
+ @options = options
16
+ end
17
+
18
+ def each
19
+ stations.each { |s| yield s }
20
+ end
21
+
22
+ def to_xml
23
+ body.to_xml
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :body, :options
29
+
30
+ def stations
31
+ @stations ||= body.css('station').map do |station|
32
+ station_class.new station
33
+ end
34
+ end
35
+
36
+ def station_class
37
+ options.fetch(:station_class, Station)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../errors'
2
+
3
+ module NoaaWeatherClient
4
+ module Responses
5
+ module ValidatableXmlResponse
6
+ SCHEMA_PATH = File.expand_path("../../../../data/xml", __FILE__)
7
+
8
+ def validate!(doc, schema_name)
9
+ # chdir to help Nokogiri load included schemas
10
+ Dir.chdir(SCHEMA_PATH) do
11
+ schema_file = File.join(SCHEMA_PATH, "#{schema_name}.xsd")
12
+ schema = Nokogiri::XML::Schema(File.read(schema_file))
13
+ errors = schema.validate(doc)
14
+ unless errors.empty?
15
+ raise Errors::InvalidXmlError, "Invalid xml: #{errors.map(&:message).join("\n")}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,12 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ module NoaaWeatherClient
5
+ module RestClientFactory
6
+ def self.build_client(options = {})
7
+ provider = options.fetch(:provider, Net::HTTP)
8
+ uri = URI(options.fetch(:url))
9
+ provider.new uri.host, uri.port
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module NoaaWeatherClient
2
+ module Services
3
+ module CalculateDistanceBetweenLatLon
4
+ extend Math
5
+
6
+ def self.get_distance(lat1, lon1, lat2, lon2)
7
+ lat1, lon1, lat2, lon2 = convert_to_radian(lat1, lon1, lat2, lon2)
8
+
9
+ radius = 6371 #km
10
+ s = sin(lat1) * sin(lat2)
11
+ c = cos(lat1) * cos(lat2) * cos(lon2-lon1)
12
+ acos(s + c) * radius;
13
+ end
14
+
15
+ def self.convert_to_radian(*args)
16
+ args.map { |arg| arg / 180 * Math::PI }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'rest_service'
2
+ require_relative '../responses/current_observation'
3
+
4
+ module NoaaWeatherClient
5
+ module Services
6
+ class CurrentObservations
7
+ include RestService
8
+
9
+ def initialize(options = {})
10
+ @options = options
11
+ end
12
+
13
+ def fetch(station, options = {})
14
+ rest_service.object_from_response(:get,
15
+ station.xml_url,
16
+ response_class: response_class)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :options
22
+
23
+ def rest_service
24
+ options.fetch(:rest_service, self)
25
+ end
26
+
27
+ def response_class
28
+ options.fetch(:response_class, Responses::CurrentObservation)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'calculate_distance_between_lat_lon'
2
+
3
+ module NoaaWeatherClient
4
+ module Services
5
+ module FindNearestStation
6
+ def self.find(lat, lon, stations, options = {})
7
+ calc = options.fetch(:calculator, CalculateDistanceBetweenLatLon)
8
+ filter = options.fetch(:filter, nil)
9
+ count = options.fetch(:count, 1)
10
+ stations.select!(&filter) if filter
11
+ stations.sort_by! { |s| calc.get_distance(lat, lon, s.latitude, s.longitude) }
12
+ count == 1 ? stations.first : stations.take(count)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ require_relative 'soap_service'
2
+ require_relative '../responses/forecast'
3
+
4
+ module NoaaWeatherClient
5
+ module Services
6
+ class ForecastByDay
7
+ include SoapService
8
+
9
+ def initialize(options = {})
10
+ @options = options
11
+ end
12
+
13
+ def fetch(lat, lon, options = {})
14
+ fopts = build_formated_options options.merge({ latitude: lat.to_s, longitude: lon.to_s })
15
+ soap_service.object_from_response(:ndf_dgen_by_day, fopts, response_class: response_class)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :options
21
+
22
+ def soap_service
23
+ options.fetch(:soap_service, self)
24
+ end
25
+
26
+ def response_class
27
+ options.fetch(:response_class, Responses::Forecast)
28
+ end
29
+
30
+ def build_formated_options(options)
31
+ opts = options.dup
32
+ {
33
+ latitude: opts.delete(:latitude),
34
+ longitude: opts.delete(:longitude),
35
+ startDate: opts.delete(:start_date) { Date.today.to_s },
36
+ numDays: opts.delete(:days) { 7 },
37
+ unit: unit!(opts),
38
+ format: opts.delete(:format) { '24 hourly' }
39
+ }.merge!(opts)
40
+ end
41
+
42
+ def unit!(options)
43
+ u = options.delete(:unit) { :standard }
44
+ if u == :standard
45
+ 'e'
46
+ elsif u == :metric
47
+ 'm'
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'rest_service'
2
+ require_relative '../responses/lat_lon_list'
3
+
4
+ module NoaaWeatherClient
5
+ module Services
6
+ class PostalCodeToCoordinate
7
+ include RestService
8
+
9
+ URL = "http://graphical.weather.gov/xml/sample_products/browser_interface/ndfdXMLclient.php?listZipCodeList="
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def resolve(zip, options = {})
16
+ rest_service.object_from_response(:get, build_url(zip), response_class: response_class)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :options
22
+
23
+ def rest_service
24
+ options.fetch(:rest_service, self)
25
+ end
26
+
27
+ def response_class
28
+ options.fetch(:response_class, Responses::LatLonList)
29
+ end
30
+
31
+ def build_url(zip)
32
+ "#{URL}#{zip}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'net/http'
2
+ require_relative '../rest_client_factory'
3
+ require_relative '../responses/generic_response'
4
+ require_relative '../errors'
5
+
6
+ module NoaaWeatherClient
7
+ module RestService
8
+ def object_from_response(action, url, options = {})
9
+ response_class = options.fetch(:response_class, Responses::GenericResponse)
10
+ client = options.fetch(:client) { RestClientFactory.build_client(url: url) }
11
+ request = build_request_for_action action, url, options
12
+ response_class.new client.request(request).body
13
+ rescue Net::HTTPError,
14
+ Net::HTTPRetriableError,
15
+ Net::HTTPServerException,
16
+ Net::HTTPFatalError => e
17
+ raise Errors::CommunicationError, e.message, e.backtrace
18
+ end
19
+
20
+ # @note Much more to be done here when needed.
21
+ def build_request_for_action(action, url, options = {})
22
+ r_class = case action
23
+ when :get then Net::HTTP::Get
24
+ end
25
+ r_class.new url
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ require 'savon'
2
+ require_relative '../soap_client_factory'
3
+ require_relative '../responses/generic_response'
4
+ require_relative '../errors'
5
+
6
+ module NoaaWeatherClient
7
+ module SoapService
8
+ def object_from_response(soap_action, message, options = {})
9
+ client = options.fetch(:client, SoapClientFactory.build_client)
10
+ response_class = options.fetch(:response_class, Responses::GenericResponse)
11
+ response_class.new client.call(soap_action, message: message).body
12
+ rescue Savon::Error => e
13
+ raise Errors::CommunicationError, e.message, e.backtrace
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'rest_service'
2
+ require_relative '../responses/stations'
3
+
4
+ module NoaaWeatherClient
5
+ module Services
6
+ class WeatherStations
7
+ include RestService
8
+
9
+ URL = 'http://w1.weather.gov/xml/current_obs/index.xml'
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def fetch(options = {})
16
+ rest_service.object_from_response(:get, URL, response_class: response_class)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :options
22
+
23
+ def rest_service
24
+ options.fetch(:rest_service, self)
25
+ end
26
+
27
+ def response_class
28
+ options.fetch(:response_class, Responses::Stations)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ require 'savon'
2
+
3
+ module NoaaWeatherClient
4
+ module SoapClientFactory
5
+ WSDL = 'http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl'
6
+
7
+ def self.build_client(options = {})
8
+ provider = options.fetch(:provider, Savon)
9
+ wsdl = options.fetch(:wsdl, WSDL)
10
+ provider.client(wsdl: wsdl,
11
+ log: false,
12
+ open_timeout: 10,
13
+ read_timeout: 30,
14
+ ssl_verify_mode: :none)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ module NoaaWeatherClient
2
+ module StationFilters
3
+ # {https://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code ICAO reference}
4
+ def self.icao
5
+ ->(station) { (station.station_id =~ /\A[A-Z|0-9]{4}\z/) ? true : false }
6
+ end
7
+ end
8
+ end