weather_gov 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/workflows/rspec.yml +23 -0
  4. data/.github/workflows/rubocop.yml +23 -0
  5. data/.gitignore +13 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +29 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +84 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +40 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/lib/weather_gov.rb +13 -0
  16. data/lib/weather_gov/address.rb +27 -0
  17. data/lib/weather_gov/alert.rb +45 -0
  18. data/lib/weather_gov/alert_collection.rb +28 -0
  19. data/lib/weather_gov/api.rb +93 -0
  20. data/lib/weather_gov/client.rb +115 -0
  21. data/lib/weather_gov/feature.rb +40 -0
  22. data/lib/weather_gov/feature_collection.rb +20 -0
  23. data/lib/weather_gov/forecast.rb +27 -0
  24. data/lib/weather_gov/forecast_period.rb +63 -0
  25. data/lib/weather_gov/gridpoint.rb +75 -0
  26. data/lib/weather_gov/identifier.rb +47 -0
  27. data/lib/weather_gov/identifier/alert.rb +13 -0
  28. data/lib/weather_gov/identifier/county_zone.rb +13 -0
  29. data/lib/weather_gov/identifier/fire_zone.rb +13 -0
  30. data/lib/weather_gov/identifier/forecast_zone.rb +13 -0
  31. data/lib/weather_gov/identifier/gridpoint.rb +29 -0
  32. data/lib/weather_gov/identifier/office.rb +13 -0
  33. data/lib/weather_gov/identifier/point.rb +21 -0
  34. data/lib/weather_gov/identifier/problem.rb +13 -0
  35. data/lib/weather_gov/identifier/station.rb +13 -0
  36. data/lib/weather_gov/observation_station.rb +27 -0
  37. data/lib/weather_gov/observation_station_collection.rb +20 -0
  38. data/lib/weather_gov/office.rb +68 -0
  39. data/lib/weather_gov/point.rb +81 -0
  40. data/lib/weather_gov/product.rb +29 -0
  41. data/lib/weather_gov/product_list.rb +17 -0
  42. data/lib/weather_gov/relative_location.rb +23 -0
  43. data/lib/weather_gov/valid_duration_value.rb +26 -0
  44. data/lib/weather_gov/valid_time.rb +48 -0
  45. data/lib/weather_gov/version.rb +5 -0
  46. data/lib/weather_gov/zone.rb +33 -0
  47. data/weather_gov.gemspec +39 -0
  48. metadata +190 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/office"
4
+ require "weather_gov/observation_station"
5
+ require "weather_gov/observation_station_collection"
6
+ require "weather_gov/point"
7
+ require "weather_gov/gridpoint"
8
+ require "weather_gov/alert_collection"
9
+ require "weather_gov/forecast"
10
+ require "weather_gov/zone"
11
+ require "weather_gov/product"
12
+ require "weather_gov/product_list"
13
+
14
+ module WeatherGov
15
+ class Client
16
+ attr_reader :api
17
+
18
+ def initialize(user_agent:)
19
+ @api = API.new(user_agent: user_agent)
20
+ end
21
+
22
+ def json_for(response)
23
+ return response.parsed_response unless response.parsed_response.include?("status")
24
+
25
+ raise response.parsed_response.fetch("title", "Unknown Error")
26
+ end
27
+
28
+ def stations(data: nil, uri: nil)
29
+ return ObservationStationCollection.new(client: self, data: data) if data
30
+ return ObservationStationCollection.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
31
+
32
+ # In theory an "all stations" query is okay, but in practice it seems to time out.
33
+
34
+ raise ArgumentError, "data or uri parameter required"
35
+ end
36
+
37
+ def station(data: nil, uri: nil, id: nil)
38
+ return ObservationStation.new(client: self, data: data) if data
39
+ return ObservationStation.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
40
+ return ObservationStation.new(client: self, data: -> { json_for(api.station(id: id)) }) if id
41
+
42
+ raise ArgumentError, "data or id parameter required"
43
+ end
44
+
45
+ def office(data: nil, uri: nil, id: nil)
46
+ return Office.new(client: self, data: data) if data
47
+ return Office.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
48
+ return Office.new(client: self, data: -> { json_for(api.office(id: id)) }) if id
49
+
50
+ raise ArgumentError, "data, uri, or id parameter required"
51
+ end
52
+
53
+ def point(data: nil, uri: nil, lat: nil, lon: nil)
54
+ return Point.new(client: self, data: data) if data
55
+ return Point.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
56
+ return Point.new(client: self, data: -> { json_for(api.point(lat: lat, lon: lon)) }) if lat && lon
57
+
58
+ raise ArgumentError, "data, uri, or lat and lon parameters required"
59
+ end
60
+
61
+ def gridpoint(data: nil, uri: nil)
62
+ return Gridpoint.new(client: self, data: data) if data
63
+ return Gridpoint.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
64
+
65
+ raise ArgumentError, "data, uri, or lat and lon parameters required"
66
+ end
67
+
68
+ def alerts_active(data: nil, zone: nil, area: nil, region: nil)
69
+ return AlertCollection.new(client: self, data: data) if data
70
+
71
+ if zone || area || region
72
+ return AlertCollection.new(
73
+ client: self,
74
+ data: -> { json_for(api.alerts_active(zone: zone, area: area, region: region)) }
75
+ )
76
+ end
77
+
78
+ raise ArgumentError, "data, zone, area, or region parameter required"
79
+ end
80
+
81
+ def zone(data: nil, uri: nil, type: nil, id: nil)
82
+ return Zone.new(client: self, data: data) if data
83
+ return Zone.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
84
+ return Zone.new(client: self, data: -> { json_for(api.zone(type: type, id: id)) }) if type && id
85
+
86
+ raise ArgumentError, "data, uri, or type and id parameter required"
87
+ end
88
+
89
+ def forecast(data: nil, uri: nil)
90
+ return Forecast.new(client: self, data: data) if data
91
+ return Forecast.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
92
+
93
+ raise ArgumentError, "data or uri parameter required"
94
+ end
95
+
96
+ def products(data: nil, uri: nil, type: nil, location: nil)
97
+ return ProductList.new(client: self, data: data) if data
98
+ return ProductList.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
99
+
100
+ if type && location
101
+ return ProductList.new(client: self, data: -> { json_for(api.products(type: type, location: location)) })
102
+ end
103
+
104
+ raise ArgumentError, "data, uri, or type and location parameters required"
105
+ end
106
+
107
+ def product(data: nil, uri: nil, id: nil)
108
+ return Product.new(client: self, data: data) if data
109
+ return Product.new(client: self, data: -> { json_for(api.get(uri: uri)) }) if uri
110
+ return Product.new(client: self, data: -> { json_for(api.product(id: id)) }) if id
111
+
112
+ raise ArgumentError, "data, uri, or id parameter required"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeatherGov
4
+ class Feature
5
+ attr_reader :client
6
+
7
+ def initialize(client:, data:)
8
+ @client = client
9
+ if data.respond_to?(:call)
10
+ @data_proc = data
11
+ else
12
+ @data = data
13
+ end
14
+ end
15
+
16
+ def data
17
+ @data ||= @data_proc.call
18
+ end
19
+
20
+ def context
21
+ data.fetch("@context", nil)
22
+ end
23
+
24
+ def id
25
+ data.fetch("id")
26
+ end
27
+
28
+ def type
29
+ data.fetch("type")
30
+ end
31
+
32
+ def geometry
33
+ data.fetch("geometry", nil)
34
+ end
35
+
36
+ def properties
37
+ data.fetch("properties", nil)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/feature"
4
+ require "forwardable"
5
+
6
+ module WeatherGov
7
+ class FeatureCollection < Feature
8
+ extend Forwardable
9
+
10
+ def self.feature_class
11
+ Feature
12
+ end
13
+
14
+ def features
15
+ @features ||= data.fetch("features").map { |feature| self.class.feature_class.new(client: client, data: feature) }
16
+ end
17
+
18
+ def_delegators :features, :[], :size, :each, :first, :last, :map
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/forecast_period"
4
+
5
+ module WeatherGov
6
+ class Forecast < Feature
7
+ def update_time
8
+ @update_time ||= Time.parse(properties.fetch("updateTime"))
9
+ end
10
+
11
+ def valid_time
12
+ @valid_time ||= ValidTime.parse(properties.fetch("validTimes"))
13
+ end
14
+
15
+ def valid?
16
+ valid_time.valid?
17
+ end
18
+
19
+ def periods
20
+ properties.fetch("periods").map { |period| ForecastPeriod.new(data: period) }
21
+ end
22
+
23
+ def current
24
+ periods.find { |p| (p.start_time...p.end_time).include?(Time.now) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeatherGov
4
+ class ForecastPeriod
5
+ attr_reader :data
6
+
7
+ def initialize(data:)
8
+ @data = data
9
+ end
10
+
11
+ def number
12
+ data.fetch("number")
13
+ end
14
+
15
+ def name
16
+ data.fetch("name")
17
+ end
18
+
19
+ def start_time
20
+ @start_time ||= Time.parse(data.fetch("startTime"))
21
+ end
22
+
23
+ def end_time
24
+ @end_time ||= Time.parse(data.fetch("endTime"))
25
+ end
26
+
27
+ def daytime?
28
+ data.fetch("isDaytime")
29
+ end
30
+
31
+ def temperature
32
+ data.fetch("temperature")
33
+ end
34
+
35
+ def temperature_unit
36
+ data.fetch("temperatureUnit")
37
+ end
38
+
39
+ def temperature_trend
40
+ data.fetch("temperatureTrend")
41
+ end
42
+
43
+ def wind_speed
44
+ data.fetch("windSpeed")
45
+ end
46
+
47
+ def wind_direction
48
+ data.fetch("windDirection")
49
+ end
50
+
51
+ def icon
52
+ data.fetch("icon")
53
+ end
54
+
55
+ def short_forecast
56
+ data.fetch("shortForecast")
57
+ end
58
+
59
+ def detailed_forecast
60
+ data.fetch("detailedForecast")
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/valid_duration_value"
4
+
5
+ module WeatherGov
6
+ class Gridpoint < Feature
7
+ def update_time
8
+ @update_time ||= Time.parse(properties.fetch("updateTime"))
9
+ end
10
+
11
+ def forecast_office
12
+ @forecast_office ||= Identifier::Office.new(properties.fetch("forecastOffice"))
13
+ end
14
+
15
+ def temperature_unit
16
+ @temperature_unit ||= properties.dig("temperature", "uom").split(":").last
17
+ end
18
+
19
+ def temperature
20
+ @temperature ||= properties.dig("temperature", "values").map do |data|
21
+ ValidDurationValue.new(data: data, unit: temperature_unit)
22
+ end
23
+ end
24
+
25
+ def dewpoint_unit
26
+ @dewpoint_unit ||= properties.dig("dewpoint", "uom").split(":").last
27
+ end
28
+
29
+ def dewpoint
30
+ @dewpoint ||= properties.dig("dewpoint", "values").map do |data|
31
+ ValidDurationValue.new(data: data, unit: dewpoint_unit)
32
+ end
33
+ end
34
+
35
+ def min_temperature_unit
36
+ @min_temperature_unit ||= properties.dig("minTemperature", "uom").split(":").last
37
+ end
38
+
39
+ def min_temperature
40
+ @min_temperature ||= properties.dig("minTemperature", "values").map do |data|
41
+ ValidDurationValue.new(data: data, unit: min_temperature_unit)
42
+ end
43
+ end
44
+
45
+ def max_temperature_unit
46
+ @max_temperature_unit ||= properties.dig("maxTemperature", "uom").split(":").last
47
+ end
48
+
49
+ def max_temperature
50
+ @max_temperature ||= properties.dig("maxTemperature", "values").map do |data|
51
+ ValidDurationValue.new(data: data, unit: max_temperature_unit)
52
+ end
53
+ end
54
+
55
+ def apparent_temperature_unit
56
+ @apparent_temperature_unit ||= properties.dig("apparentTemperature", "uom").split(":").last
57
+ end
58
+
59
+ def apparent_temperature
60
+ @apparent_temperature ||= properties.dig("apparentTemperature", "values").map do |data|
61
+ ValidDurationValue.new(data: data, unit: apparent_temperature_unit)
62
+ end
63
+ end
64
+
65
+ def relative_humidity_unit
66
+ @relative_humidity_unit ||= properties.dig("relativeHumidity", "uom").split(":").last
67
+ end
68
+
69
+ def relative_humidity
70
+ @relative_humidity ||= properties.dig("relativeHumidity", "values").map do |data|
71
+ ValidDurationValue.new(data: data, unit: relative_humidity_unit)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeatherGov
4
+ class Identifier
5
+ attr_reader :uri
6
+
7
+ def self.parse(uri)
8
+ TYPES.each do |type|
9
+ return type.new(uri) if uri.start_with?(type.base_uri)
10
+ end
11
+
12
+ nil
13
+ end
14
+
15
+ def self.base_uri
16
+ API_BASE_URI
17
+ end
18
+
19
+ def initialize(uri)
20
+ @uri = uri
21
+ end
22
+
23
+ def to_s
24
+ id
25
+ end
26
+
27
+ def id
28
+ uri.sub(/^#{self.class.base_uri}/, "")
29
+ end
30
+ end
31
+ end
32
+
33
+ require "weather_gov/identifier/alert"
34
+ require "weather_gov/identifier/county_zone"
35
+ require "weather_gov/identifier/fire_zone"
36
+ require "weather_gov/identifier/forecast_zone"
37
+ require "weather_gov/identifier/gridpoint"
38
+ require "weather_gov/identifier/office"
39
+ require "weather_gov/identifier/point"
40
+ require "weather_gov/identifier/problem"
41
+ require "weather_gov/identifier/station"
42
+
43
+ module WeatherGov
44
+ class Identifier
45
+ TYPES = constants.map { |c| const_get(c) }.select { |c| c.superclass == self }
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/identifier"
4
+
5
+ module WeatherGov
6
+ class Identifier
7
+ class Alert < Identifier
8
+ def self.base_uri
9
+ URI.join(super, "/alerts/").to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/identifier"
4
+
5
+ module WeatherGov
6
+ class Identifier
7
+ class CountyZone < Identifier
8
+ def self.base_uri
9
+ URI.join(super, "/zones/county/").to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/identifier"
4
+
5
+ module WeatherGov
6
+ class Identifier
7
+ class FireZone < Identifier
8
+ def self.base_uri
9
+ URI.join(super, "/zones/fire/").to_s
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weather_gov/identifier"
4
+
5
+ module WeatherGov
6
+ class Identifier
7
+ class ForecastZone < Identifier
8
+ def self.base_uri
9
+ URI.join(super, "/zones/forecast/").to_s
10
+ end
11
+ end
12
+ end
13
+ end