From 51b9c95a7c33dd852d1a90215f631a6688ee1e06 Mon Sep 17 00:00:00 2001 From: Troy Anderson Date: Tue, 2 Jul 2024 16:15:14 -0700 Subject: [PATCH] [#3] Added support for the hourly forecast in the pro 2.5 API --- CHANGELOG.md | 3 +- README.md | 24 ++++++++++ lib/open_weather/client.rb | 1 + lib/open_weather/config.rb | 2 + lib/open_weather/endpoints.rb | 1 + lib/open_weather/endpoints/hourly.rb | 16 +++++++ lib/open_weather/models.rb | 1 + lib/open_weather/models/forecast.rb | 5 +++ lib/open_weather/models/forecast/city.rb | 17 +++++++ lib/open_weather/models/forecast/forecast.rb | 33 ++++++++++++++ lib/open_weather/models/forecast/hourly.rb | 25 +++++++++++ .../fixtures/open_weather/forecast/hourly.yml | 45 +++++++++++++++++++ spec/open_weather/forecast/hourly_spec.rb | 32 +++++++++++++ 13 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 lib/open_weather/endpoints/hourly.rb create mode 100644 lib/open_weather/models/forecast.rb create mode 100644 lib/open_weather/models/forecast/city.rb create mode 100644 lib/open_weather/models/forecast/forecast.rb create mode 100644 lib/open_weather/models/forecast/hourly.rb create mode 100644 spec/fixtures/open_weather/forecast/hourly.yml create mode 100644 spec/open_weather/forecast/hourly_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 651f9ae..ccf7275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ * [#25](https://github.com/dblock/open-weather-ruby-client/pull/25): Exposed the national weather alerts response in the One Call API - [@troya2](https://github.com/troya2). * [#38](https://github.com/dblock/open-weather-ruby-client/pull/38): Migrated to One Call 3.0 API - [@jeanmartin](https://github.com/jeanmartin). -* Your contribution here. +* [#3](https://github.com/dblock/open-weather-ruby-client/issues/3): Added support for the hourly forecast in the pro 2.5 API[@troya2](https://github.com/troya2). +Your contribution here. ### 0.4.0 (2023/08/13) diff --git a/README.md b/README.md index bd954d3..26122bf 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ See [OpenWeather::Models::OneCall](lib/open_weather/models/one_call) for all ava ```ruby data = client.one_call(lat: 33.441792, lon: -94.037689) # => OpenWeather::Models::OneCall::Weather + data.lat # => 33.44 data.lon # => -94.04 data.timezone # => 'America/Chicago' @@ -206,6 +207,7 @@ client.one_call(lat: 33.441792, lon: -94.037689, exclude: ['minutely', 'hourly'] ```ruby data = client.one_call(lat: 33.441792, lon: -94.037689, dt: Time.now - 24 * 60 * 60) # => OpenWeather::Models::OneCall::Weather + data.lat # => 33.44 data.lon # => -94.04 data.timezone # => 'America/Chicago' @@ -213,6 +215,28 @@ data.current # => OpenWeather::Models::OneCall::CurrentWeather data.hourly # => Array[OpenWeather::Models::OneCall::HourlyWeather] ``` +### Hourly Forecast (Pro) + +The [Hourly Forecast API](https://openweathermap.org/api/hourly-forecast) provides hourly weather forecast for 4 days. Note: This API requires a piad api-key from [OpenWeather.org](https://openweathermap.org/full-price#current). + +```ruby +data = client.client.hourly(lat: 33.5312, lon: -111.9426) # => OpenWeather::Models::Forecast::Hourly + +data.cnt # => 96 (number of entries) +data.list.first # => OpenWeather::Models::Forecast::Forecast +data.list.first.dt # => Time +data.list.first.main # => OpenWeather::Models::Forecast::Main +data.list.first.weather # => Array[OpenWeather::Models::Forecast::Weather] +data.list.first.clouds # => OpenWeather::Models::Forecast::Clouds or nil +data.list.first.wind # => OpenWeather::Models::Forecast::Wind or nil +data.list.first.visibility # => 10000 +data.list.first.pop # => 0.1 (probability of precipitation from 0.0 to 1.0 (0% to 100%)) +data.list.first.rain # => OpenWeather::Models::Forecast::Rain or nil +data.list.first.snow # => OpenWeather::Models::Forecast::Snow or nil +data.list.first.sys # => OpenWeather::Models::Forecast::Sys or nil +data.list.first.dt_txt # => String (Time of data forecasted, ISO, UTC) +``` + ### Stations The [Stations API](https://openweathermap.org/stations) lets your manage personal weather stations and measurements. diff --git a/lib/open_weather/client.rb b/lib/open_weather/client.rb index 4058f97..15b533c 100644 --- a/lib/open_weather/client.rb +++ b/lib/open_weather/client.rb @@ -5,6 +5,7 @@ class Client include Connection include Request include Endpoints::Current + include Endpoints::Hourly include Endpoints::OneCall include Endpoints::Stations diff --git a/lib/open_weather/config.rb b/lib/open_weather/config.rb index 7a51e29..092be22 100644 --- a/lib/open_weather/config.rb +++ b/lib/open_weather/config.rb @@ -6,6 +6,7 @@ module Config ATTRIBUTES = %i[ endpoint + pro_endpoint api_key proxy user_agent @@ -22,6 +23,7 @@ module Config def reset self.endpoint = 'https://api.openweathermap.org/data' + self.pro_endpoint = 'https://pro.openweathermap.org/data' self.api_key = nil self.user_agent = "OpenWeather Ruby Client/#{OpenWeather::VERSION}" self.ca_path = nil diff --git a/lib/open_weather/endpoints.rb b/lib/open_weather/endpoints.rb index def9fcc..78ba719 100644 --- a/lib/open_weather/endpoints.rb +++ b/lib/open_weather/endpoints.rb @@ -3,3 +3,4 @@ require_relative 'endpoints/current' require_relative 'endpoints/one_call' require_relative 'endpoints/stations' +require_relative 'endpoints/hourly' diff --git a/lib/open_weather/endpoints/hourly.rb b/lib/open_weather/endpoints/hourly.rb new file mode 100644 index 0000000..3bf2cef --- /dev/null +++ b/lib/open_weather/endpoints/hourly.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module OpenWeather + module Endpoints + module Hourly + def hourly(lat, lon = nil, options = {}) + # default to the pro endpoint if not specified + endpoint = options.delete(:endpoint) || pro_endpoint + options = options.merge(endpoint:) + + options = lat.is_a?(Hash) ? options.merge(lat) : options.merge(lat:, lon:) + OpenWeather::Models::Forecast::Hourly.new(get('2.5/forecast/hourly', options), options) + end + end + end +end diff --git a/lib/open_weather/models.rb b/lib/open_weather/models.rb index 632fbcc..c067eef 100644 --- a/lib/open_weather/models.rb +++ b/lib/open_weather/models.rb @@ -13,6 +13,7 @@ require_relative 'models/list' require_relative 'models/city' require_relative 'models/one_call' +require_relative 'models/forecast' require_relative 'models/station' require_relative 'models/stations/measurement' require_relative 'models/stations/temp' diff --git a/lib/open_weather/models/forecast.rb b/lib/open_weather/models/forecast.rb new file mode 100644 index 0000000..d86f273 --- /dev/null +++ b/lib/open_weather/models/forecast.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require_relative 'forecast/hourly' +require_relative 'forecast/forecast' +require_relative 'forecast/city' diff --git a/lib/open_weather/models/forecast/city.rb b/lib/open_weather/models/forecast/city.rb new file mode 100644 index 0000000..6190aba --- /dev/null +++ b/lib/open_weather/models/forecast/city.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module OpenWeather + module Models + module Forecast + class City < Model + property 'id' # City ID + property 'name' # City name + property 'coord', transform_with: ->(v) { OpenWeather::Models::Coord.new(v) } # City geo location + property 'country' # Country code (GB, JP etc.). + property 'timezone' # shift in seconds from UTC + property 'sunrise', transform_with: ->(v) { Time.at(v).utc } # Sunrise time, UTC + property 'sunset', transform_with: ->(v) { Time.at(v).utc } # Sunset time, UTC + end + end + end +end diff --git a/lib/open_weather/models/forecast/forecast.rb b/lib/open_weather/models/forecast/forecast.rb new file mode 100644 index 0000000..53745dc --- /dev/null +++ b/lib/open_weather/models/forecast/forecast.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module OpenWeather + module Models + module Forecast + class Forecast < Model + property 'dt', transform_with: ->(v) { Time.at(v).utc } # time of data forcasted, UTC + property 'dt_txt' # Time of data forecasted, ISO, UTC + property 'main' + property 'weather' + property 'clouds' + property 'wind' + property 'rain' + property 'snow' + property 'visibility' # Average visibility, metres. The maximum value of the visibility is 10km + property 'pop' # Probability of precipitation. The values of the parameter vary between 0 and 1, where 0 is equal to 0%, 1 is equal to 100% + property 'sys' + + def initialize(args = nil, options = {}) + super args, options + + self.main = OpenWeather::Models::Main.new(main, options) if main + self.weather = weather.map { |w| OpenWeather::Models::Weather.new(w, options) } if weather + self.clouds = OpenWeather::Models::Clouds.new(clouds, options) if clouds + self.wind = OpenWeather::Models::Wind.new(wind, options) if wind + self.rain = OpenWeather::Models::Rain.new(rain, options) if rain + self.snow = OpenWeather::Models::Snow.new(snow, options) if snow + self.sys = OpenWeather::Models::Sys.new(sys, options) if sys + end + end + end + end +end diff --git a/lib/open_weather/models/forecast/hourly.rb b/lib/open_weather/models/forecast/hourly.rb new file mode 100644 index 0000000..716ccfe --- /dev/null +++ b/lib/open_weather/models/forecast/hourly.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module OpenWeather + module Models + module Forecast + class Hourly < Model + include Enumerable + + property 'cod' + property 'calctime' + property 'cnt', from: 'count' + property 'list' + property 'message' + property 'city' + + def initialize(args = nil, options = {}) + super args, options + + self.list = list.map { |forecast| OpenWeather::Models::Forecast::Forecast.new(forecast, options) } if list + self.city = OpenWeather::Models::Forecast::City.new(city, options) if city + end + end + end + end +end diff --git a/spec/fixtures/open_weather/forecast/hourly.yml b/spec/fixtures/open_weather/forecast/hourly.yml new file mode 100644 index 0000000..4ef759e --- /dev/null +++ b/spec/fixtures/open_weather/forecast/hourly.yml @@ -0,0 +1,45 @@ +--- +http_interactions: +- request: + method: get + uri: https://pro.openweathermap.org/data/2.5/forecast/hourly?appid=api-key&lat=33.5312&lon=-111.9426 + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - application/json; charset=utf-8 + Content-Type: + - application/json + User-Agent: + - OpenWeather Ruby Client/0.5.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Server: + - openresty + Date: + - Tue, 02 Jul 2024 20:52:29 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '1845' + Connection: + - keep-alive + X-Cache-Key: + - "/data/2.5/forecast/hourly?lat=33.53&lon=-111.94" + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST + body: + encoding: UTF-8 + string: '{"city":{"coord":{"lat":44.34,"lon":10.99},"country":"IT","id":3163858,"name":"Zocca","population":4593,"sunrise":1661834187,"sunset":1661882248,"timezone":7200},"cnt":96,"cod":"200","list":[{"clouds":{"all":97},"dt":1661875200,"dt_txt":"2022-08-30 16:00:00","main":{"feels_like":296.02,"grnd_level":933,"humidity":50,"pressure":1015,"sea_level":1015,"temp":296.34,"temp_kf":-1.9,"temp_max":298.24,"temp_min":296.34},"pop":0.32,"rain":{"1h":0.13},"sys":{"pod":"d"},"visibility":10000,"weather":[{"description":"light rain","icon":"10d","id":500,"main":"Rain"}],"wind":{"deg":66,"gust":2.16,"speed":1.06}},{"clouds":{"all":95},"dt":1661878800,"dt_txt":"2022-08-30 17:00:00","main":{"feels_like":296.07,"grnd_level":932,"humidity":53,"pressure":1015,"sea_level":1015,"temp":296.31,"temp_kf":0.11,"temp_max":296.31,"temp_min":296.2},"pop":0.4,"rain":{"1h":0.24},"sys":{"pod":"d"},"visibility":10000,"weather":[{"description":"light rain","icon":"10d","id":500,"main":"Rain"}],"wind":{"deg":103,"gust":3.52,"speed":1.58}},{"clouds":{"all":93},"dt":1661882400,"dt_txt":"2022-08-30 18:00:00","main":{"feels_like":294.74,"grnd_level":931,"humidity":60,"pressure":1015,"sea_level":1015,"temp":294.94,"temp_kf":2.1,"temp_max":294.94,"temp_min":292.84},"pop":0.33,"rain":{"1h":0.2},"sys":{"pod":"n"},"visibility":10000,"weather":[{"description":"light rain","icon":"10n","id":500,"main":"Rain"}],"wind":{"deg":157,"gust":3.39,"speed":1.97}},{"clouds":{"all":100},"dt":1662217200,"dt_txt":"2022-09-03 15:00:00","main":{"feels_like":293.99,"grnd_level":931,"humidity":65,"pressure":1014,"sea_level":1014,"temp":294.14,"temp_kf":0,"temp_max":294.14,"temp_min":294.14},"pop":0.53,"sys":{"pod":"d"},"visibility":10000,"weather":[{"description":"overcast clouds","icon":"04d","id":804,"main":"Clouds"}],"wind":{"deg":104,"gust":1.92,"speed":0.91}}],"message":0}' + recorded_at: Tue, 02 Jul 2024 20:52:29 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/open_weather/forecast/hourly_spec.rb b/spec/open_weather/forecast/hourly_spec.rb new file mode 100644 index 0000000..ea60b63 --- /dev/null +++ b/spec/open_weather/forecast/hourly_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'hourly forecast' do + include_context 'API client' + + it 'hourly', vcr: { cassette_name: 'forecast/hourly' } do + data = client.hourly(lat: 33.5312, lon: -111.9426) + + expect(data).to be_a OpenWeather::Models::Forecast::Hourly + expect(data.cnt).to eq 96 + expect(data.city).to be_a OpenWeather::Models::Forecast::City + + expect(data.list).to be_a Array + data.list.first.tap do |forecast| + expect(forecast).to be_a OpenWeather::Models::Forecast::Forecast + expect(forecast.dt).to eq Time.at(1661875200) + expect(forecast.main).to be_a OpenWeather::Models::Main + expect(forecast.weather).to be_a Array + expect(forecast.weather.first).to be_a OpenWeather::Models::Weather + expect(forecast.clouds).to be_a OpenWeather::Models::Clouds + expect(forecast.wind).to be_a OpenWeather::Models::Wind + expect(forecast.rain).to be_a OpenWeather::Models::Rain + expect(forecast.snow).to be_nil + expect(forecast.visibility).to eq 10000 + expect(forecast.pop).to eq 0.32 + expect(forecast.sys).to be_a OpenWeather::Models::Sys + expect(forecast.dt_txt).to eq '2022-08-30 16:00:00' + end + end +end