weather_gov 0.1.0

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 (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