noaa_weather_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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