weathercom 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6a1cd129b393ad377eee8ab97d03889911468403c697df53e513e8fc6fb8feb7
4
+ data.tar.gz: 70f9251e341bf65aeef8a718e6e57a7628f6e1491c3c3487b3a80e049fc1c2f9
5
+ SHA512:
6
+ metadata.gz: 68303cb4b06e1b9c6d359ff489b56b1914de287e8b28b6f134c11da873e1da917af7fdbd9ee1fd5c70db347a3cb142a3ce8c80ba31effc32db17314e6402369a
7
+ data.tar.gz: 3a2d5409025765ed4e2217dc7daa9ffe7ebc273928bb1742a30c7d9bfc0f62decf5725bae6f3ae4df0223eb8d2f81d02e69536788ea34dce5b4b4b768da0213d
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright (c) 2018 Oleg Pudeyev
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # Weathercom
2
+
3
+ Ruby client for weather.com / wunderground API.
4
+
5
+ ## License
6
+
7
+ MIT
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+ require 'faraday'
3
+
4
+ require 'weathercom/client'
5
+ require 'weathercom/location'
6
+ require 'weathercom/metadata'
7
+ require 'weathercom/geocoded_location'
8
+ require 'weathercom/forecast_methods'
9
+ require 'weathercom/day_part_forecast'
10
+ require 'weathercom/daily_forecast'
11
+ require 'weathercom/hourly_forecast'
12
+ require 'weathercom/wwir_forecast'
13
+ require 'weathercom/observation'
@@ -0,0 +1,159 @@
1
+ module Weathercom
2
+
3
+ class Client
4
+ class ApiError < StandardError
5
+ def initialize(message, status: nil)
6
+ super(message)
7
+ @status = status
8
+ end
9
+
10
+ attr_reader :status
11
+ end
12
+
13
+ class ApiKeyScrapeError < StandardError
14
+ end
15
+
16
+ def initialize(api_key: nil, cache: nil)
17
+ @configured_api_key = api_key
18
+ @cache = cache
19
+ @connection ||= Faraday.new("https://api.weather.com") do |f|
20
+ f.request :url_encoded
21
+ #f.response :detailed_logger
22
+ f.adapter Faraday.default_adapter
23
+ f.headers['user-agent'] = 'Mozilla/5.0 (Compatible)'
24
+ end
25
+ if api_key.nil?
26
+ @api_key = cache.get('weathercom:api_key')
27
+ end
28
+ end
29
+
30
+ attr_reader :configured_api_key
31
+ attr_reader :connection
32
+
33
+ def api_key
34
+ configured_api_key or begin
35
+ @api_key ||= scrape_api_key
36
+ if @cache
37
+ @cache.set('weathercom:api_key', @api_key)
38
+ end
39
+ @api_key
40
+ end
41
+ end
42
+
43
+ private def clear_api_key
44
+ @api_key = nil
45
+ if @cache
46
+ @cache.set('weathercom:api_key', nil)
47
+ end
48
+ end
49
+
50
+ def get_json(url)
51
+ request_json(:get, url)
52
+ end
53
+
54
+ def get_json_with_cache(url)
55
+ request_json_with_cache(:get, url)
56
+ end
57
+
58
+ def request_json(meth, url)
59
+ attempt = 1
60
+ loop do
61
+ if url.include?('?')
62
+ full_url = "#{url}&apiKey=#{URI.encode(api_key)}"
63
+ else
64
+ full_url = "#{url}?apiKey=#{URI.encode(api_key)}"
65
+ end
66
+
67
+ response = connection.send(meth) do |req|
68
+ req.url(full_url)
69
+ end
70
+ if response.status == 401 && configured_api_key.nil? && attempt == 1
71
+ clear_api_key
72
+ attempt += 1
73
+ next
74
+ end
75
+ unless response.status == 200
76
+ error = nil
77
+ begin
78
+ error = JSON.parse(response.body)['error']
79
+ rescue
80
+ end
81
+ msg = "Weathercom #{meth.to_s.upcase} #{url} failed: #{response.status}"
82
+ if error
83
+ msg += ": #{error}"
84
+ end
85
+ raise ApiError.new(msg, status: response.status)
86
+ end
87
+ return JSON.parse(response.body)
88
+ end
89
+ end
90
+
91
+ def request_json_with_cache(meth, url)
92
+ cache_key = "weathercom:#{meth}:#{url}"
93
+ if @cache && (data = @cache.get(cache_key))
94
+ if data.key?('metadata') && data['metadata'].key?('expire_time_gmt') &&
95
+ data['metadata']['expire_time_gmt'] > Time.now.to_i
96
+ then
97
+ return data
98
+ end
99
+ @cache.set(cache_key, nil)
100
+ end
101
+
102
+ request_json(meth, url).tap do |data|
103
+ if @cache
104
+ @cache.set(cache_key, data)
105
+ end
106
+ end
107
+ end
108
+
109
+ # endpoints
110
+
111
+ def geocode(query, ttl: nil)
112
+ if @cache && ttl
113
+ cache_key = "weathercom:geocode:#{query}"
114
+ result = @cache.get(cache_key)
115
+ if result && result['expires_at'] && result['expires_at'] > Time.now.to_i
116
+ return GeocodedLocation.new(result['location'], self)
117
+ end
118
+ end
119
+
120
+ payload = raw_geocode(query)
121
+
122
+ if @cache && ttl
123
+ @cache.set(cache_key, 'expires_at' => Time.now.to_i + ttl, 'location' => payload)
124
+ end
125
+
126
+ GeocodedLocation.new(payload, self)
127
+ end
128
+
129
+ def location(lat, lng)
130
+ Location.new(lat, lng, self)
131
+ end
132
+
133
+ private
134
+
135
+ API_KEY_URL = "https://www.wunderground.com/weather/us/ny/new-york"
136
+
137
+ def scrape_api_key
138
+ resp = connection.get(API_KEY_URL)
139
+ if resp.status != 200
140
+ raise ApiKeyScrapeError, "Non-200 status while scraping API key: #{resp.status}"
141
+ end
142
+
143
+ unless resp.body =~ /apiKey=([a-zA-Z0-9]{10,})/
144
+ raise ApiKeyScrapeError, "Could not locate API key in response"
145
+ end
146
+
147
+ $1
148
+ end
149
+
150
+ def raw_geocode(query)
151
+ url = "/v3/location/search?language=EN&query=#{URI.encode(query)}&format=json"
152
+ payload = get_json(url)
153
+ payload = Hash[payload['location'].map do |key, values|
154
+ [key, values.first]
155
+ end]
156
+ end
157
+ end
158
+
159
+ end
@@ -0,0 +1,45 @@
1
+ module Weathercom
2
+
3
+ class DailyForecast
4
+ include ForecastMethods
5
+
6
+ def initialize(info, metadata)
7
+ @info = info.dup.freeze
8
+ @metadata = metadata
9
+ end
10
+
11
+ attr_reader :metadata
12
+
13
+ %w(expire_time_gmt fcst_valid fcst_valid_local
14
+ num dow night
15
+ max_temp min_temp
16
+ torcon stormcon
17
+ blurb blurb_author narrative
18
+ qualifier_code qualifier
19
+ qpf snow_qpf snow_range snow_phrase snow_code
20
+ lunar_phase_day lunar_phase_code sunrise sunset moonrise moonset
21
+ ).each do |m|
22
+ define_method(m) do
23
+ @info[m]
24
+ end
25
+ end
26
+
27
+ alias :start_timestamp :fcst_valid
28
+ alias :expire_timestamp :expire_time_gmt
29
+
30
+ def day_forecast
31
+ @day_forecast ||= if @info.key?('day')
32
+ DayPartForecast.new(@info['day'])
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ def night_forecast
39
+ @night_forecast ||= DayPartForecast.new(@info['night'])
40
+ end
41
+
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,45 @@
1
+ module Weathercom
2
+
3
+ class DayPartForecast
4
+
5
+ def initialize(info)
6
+ @info = info.dup.freeze
7
+ end
8
+
9
+ %w(
10
+ fcst_valid fcst_valid_local
11
+ day_ind thunder_enum
12
+ daypart_name long_daypart_name alt_daypart_name
13
+ thunder_enum_phrase
14
+ num temp hi wc rh icon_extd icon_code wxman
15
+ phrase_12char phrase_22char phrase_32char
16
+ subphrase_pt1 subphrase_pt2 subphrase_pt3
17
+ pop precip_type
18
+ wspd wdir wdir_cardinal
19
+ clds
20
+ pop_phrase temp_phrase accumulation_phrase wind_phrase
21
+ shortcast narrative
22
+ qpf snow_qpf
23
+ snow_range snow_phrase snow_code
24
+ vocal_key qualifier_code qualifier
25
+ uv_index_raw uv_index uv_warning uv_desc
26
+ golf_index golf_category
27
+ ).each do |m|
28
+ define_method(m) do
29
+ @info[m]
30
+ end
31
+ end
32
+
33
+ alias :start_timestamp :fcst_valid
34
+ alias :precip_probability :pop
35
+ alias :phrase :phrase_32char
36
+
37
+ # Narrative without the temperature and wind speed, which is available
38
+ # via other attributes
39
+ def cut_narrative
40
+ narrative.sub(/\..*/, '')
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,15 @@
1
+ module Weathercom
2
+
3
+ module ForecastMethods
4
+
5
+ def wc_class
6
+ @info['class']
7
+ end
8
+
9
+ def expires_at
10
+ @expires_at ||= Time.at(expire_time_gmt)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,25 @@
1
+ module Weathercom
2
+
3
+ class GeocodedLocation < Location
4
+ def initialize(info, client)
5
+ info = info.dup
6
+ super(info.delete('latitude'), info.delete('longitude'), client)
7
+ @info = info.freeze
8
+ end
9
+
10
+ %w(address admin_district admin_district_code city country country_code
11
+ display_name iana_time_zone locale neighborhood place_id postal_code
12
+ postal_key disputed_area loc_id location_category pws_id type
13
+ ).each do |m|
14
+ key = m.gsub(/_(\w)/) { |match| $1.upcase }
15
+ define_method(m) do
16
+ @info[key]
17
+ end
18
+ end
19
+
20
+ alias :state_name :admin_district
21
+ alias :state_abbr :admin_district_code
22
+ alias :zipcode :postal_code
23
+ end
24
+
25
+ end
@@ -0,0 +1,36 @@
1
+ module Weathercom
2
+
3
+ class HourlyForecast
4
+ include ForecastMethods
5
+
6
+ def initialize(info, metadata)
7
+ @info = info.dup.freeze
8
+ @metadata = metadata
9
+ end
10
+
11
+ attr_reader :metadata
12
+
13
+ %w(
14
+ expire_time_gmt fcst_valid fcst_valid_local
15
+ num day_ind dow
16
+ temp feels_like dewpt pop precip_type severity
17
+ hi wc qpf snow_qpf rh wspd wdir wdir_cardinal gust clds vis mslp
18
+ uv_index_raw uv_index uv_warning uv_desc golf_index golf_category
19
+ icon_extd wxman icon_code
20
+ phrase_12char phrase_22char phrase_32char
21
+ subphrase_pt1 subphrase_pt2 subphrase_pt3
22
+ ).each do |m|
23
+ define_method(m) do
24
+ @info[m]
25
+ end
26
+ end
27
+
28
+ alias :start_timestamp :fcst_valid
29
+ alias :expire_timestamp :expire_time_gmt
30
+
31
+ alias :precip_probability :pop
32
+ alias :phrase :phrase_32char
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,59 @@
1
+ module Weathercom
2
+
3
+ class Location
4
+ def initialize(lat, lng, client)
5
+ @lat, @lng, @client = lat, lng, client
6
+ end
7
+
8
+ attr_reader :lat
9
+ attr_reader :lng
10
+ attr_reader :client
11
+
12
+ def current_observation
13
+ payload = client.get_json_with_cache("#{url_prefix}/observations/current.json")
14
+ Observation.new(payload['observation'], Metadata.new(payload['metadata']))
15
+ end
16
+
17
+ def daily_forecasts_5
18
+ payload = client.get_json_with_cache("#{url_prefix}/forecast/daily/5day.json?#{query}")
19
+ payload['forecasts'].map do |info|
20
+ DailyForecast.new(info, Metadata.new(payload['metadata']))
21
+ end
22
+ end
23
+
24
+ def daily_forecasts_10
25
+ payload = client.get_json_with_cache("#{url_prefix}/forecast/daily/10day.json?#{query}")
26
+ payload['forecasts'].map do |info|
27
+ DailyForecast.new(info, Metadata.new(payload['metadata']))
28
+ end
29
+ end
30
+
31
+ alias :daily_forecasts :daily_forecasts_10
32
+
33
+ def hourly_forecasts_240
34
+ payload = client.get_json_with_cache("#{url_prefix}/forecast/hourly/240hour.json?#{query}")
35
+ payload['forecasts'].map do |info|
36
+ HourlyForecast.new(info, Metadata.new(payload['metadata']))
37
+ end
38
+ end
39
+
40
+ alias :hourly_forecasts :hourly_forecasts_240
41
+
42
+ # When Will It Rain Forecast
43
+ def wwir_forecast
44
+ payload = client.get_json_with_cache("#{url_prefix}/forecast/wwir.json?#{query}")
45
+ WwirForecast.new(payload['forecast'], Metadata.new(payload['metadata']))
46
+ end
47
+
48
+ private
49
+
50
+ def url_prefix
51
+ "/v1/geocode/#{URI.encode(lat.to_s)}/#{URI.encode(lng.to_s)}"
52
+ end
53
+
54
+ def query
55
+ "units=e"
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,26 @@
1
+ module Weathercom
2
+
3
+ class Metadata
4
+
5
+ def initialize(info)
6
+ @info = info.dup.freeze
7
+ end
8
+
9
+ %w(
10
+ language transaction_id version latitude longitude expire_time_gmt status_code
11
+ ).each do |m|
12
+ define_method(m) do
13
+ @info[m]
14
+ end
15
+ end
16
+
17
+ alias :lat :latitude
18
+ alias :lng :longitude
19
+
20
+ def expires_at
21
+ @expires_at ||= Time.at(expire_time_gmt)
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,55 @@
1
+ module Weathercom
2
+
3
+ class Observation
4
+ include ForecastMethods
5
+
6
+ def initialize(info, metadata)
7
+ @info = info.dup.freeze
8
+ @metadata = metadata
9
+ end
10
+
11
+ attr_reader :metadata
12
+
13
+ %w(
14
+ expire_time_gmt obs_time obs_time_local
15
+ day_ind dow
16
+ obs_qualifier_code obs_qualifier_severity
17
+ ptend_code ptend_desc sky_cover clds
18
+ wdir wdir_cardinal
19
+ icon_code icon_extd wxman
20
+ sunrise sunset
21
+ uv_index uv_warning uv_desc
22
+ phrase_12char phrase_22char phrase_32char
23
+ vocal_key imperial
24
+ ).each do |m|
25
+ define_method(m) do
26
+ @info[m]
27
+ end
28
+ end
29
+
30
+ def sunrise_at
31
+ Time.parse(sunrise)
32
+ end
33
+
34
+ def sunset_at
35
+ Time.parse(sunset)
36
+ end
37
+
38
+ def day?
39
+ day_ind == 'Y'
40
+ end
41
+
42
+ %w(wspd gust vis mslp altimeter temp dewpt rh wc hi temp_change_24hour
43
+ temp_max_24hour temp_min_24hour pchange feels_like snow_1hour snow_6hour
44
+ snow_24hour snow_7day ceiling precip_1hour precip_6hour precip_24hour
45
+ precip_mtd precip_ytd precip_2day precip_3day precip_7day obs_qualifier_100char
46
+ obs_qualifier_50char obs_qualifier_32char
47
+ ).each do |m|
48
+ define_method(m) do
49
+ @info['imperial'][m]
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,25 @@
1
+ module Weathercom
2
+
3
+ class WwirForecast
4
+ include ForecastMethods
5
+
6
+ def initialize(info, metadata)
7
+ @info = info.dup.freeze
8
+ @metadata = metadata
9
+ end
10
+
11
+ attr_reader :metadata
12
+
13
+ %w(
14
+ expire_time_gmt fcst_valid fcst_valid_local
15
+ overall_type phrase terse_phrase phrase_template terse_phrase_template
16
+ precip_day precip_time_24hr precip_time_12hr precip_time_iso time_zone_abbrv
17
+ ).each do |m|
18
+ define_method(m) do
19
+ @info[m]
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: weathercom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Oleg Pudeyev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-06-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Weather.com API client
14
+ email: oleg@olegp.name
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/weathercom.rb
22
+ - lib/weathercom/client.rb
23
+ - lib/weathercom/daily_forecast.rb
24
+ - lib/weathercom/day_part_forecast.rb
25
+ - lib/weathercom/forecast_methods.rb
26
+ - lib/weathercom/geocoded_location.rb
27
+ - lib/weathercom/hourly_forecast.rb
28
+ - lib/weathercom/location.rb
29
+ - lib/weathercom/metadata.rb
30
+ - lib/weathercom/observation.rb
31
+ - lib/weathercom/wwir_forecast.rb
32
+ homepage: https://github.com/p/weathercom
33
+ licenses:
34
+ - MIT
35
+ metadata: {}
36
+ post_install_message:
37
+ rdoc_options:
38
+ - "--charset=UTF-8"
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.0.1
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Weather.com API client
56
+ test_files: []