yweather 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +6 -0
  2. data/CHANGELOG +3 -0
  3. data/Gemfile +2 -0
  4. data/Guardfile +9 -0
  5. data/LICENSE +504 -0
  6. data/README.md +109 -0
  7. data/Rakefile +14 -0
  8. data/bin/yweather +30 -0
  9. data/lib/yweather.rb +22 -0
  10. data/lib/yweather/astronomy.rb +12 -0
  11. data/lib/yweather/atmosphere.rb +34 -0
  12. data/lib/yweather/client.rb +80 -0
  13. data/lib/yweather/condition.rb +71 -0
  14. data/lib/yweather/forecast.rb +22 -0
  15. data/lib/yweather/image.rb +19 -0
  16. data/lib/yweather/location.rb +15 -0
  17. data/lib/yweather/response.rb +87 -0
  18. data/lib/yweather/units.rb +20 -0
  19. data/lib/yweather/utils.rb +7 -0
  20. data/lib/yweather/version.rb +3 -0
  21. data/lib/yweather/wind.rb +15 -0
  22. data/spec/bin/yweather_spec.rb +7 -0
  23. data/spec/cassettes/invalid_weather_transaction.yml +44 -0
  24. data/spec/cassettes/invalid_woeid_conversion_transaction.yml +38 -0
  25. data/spec/cassettes/valid_weather_transaction.yml +66 -0
  26. data/spec/cassettes/valid_weather_transaction_with_zipcode_conversion.yml +101 -0
  27. data/spec/cassettes/valid_woeid_conversion_transaction.yml +38 -0
  28. data/spec/lib/yweather/astronomy_spec.rb +27 -0
  29. data/spec/lib/yweather/atmosphere_spec.rb +25 -0
  30. data/spec/lib/yweather/client_spec.rb +58 -0
  31. data/spec/lib/yweather/condition_spec.rb +25 -0
  32. data/spec/lib/yweather/forecast_spec.rb +25 -0
  33. data/spec/lib/yweather/image_spec.rb +25 -0
  34. data/spec/lib/yweather/location_spec.rb +25 -0
  35. data/spec/lib/yweather/response_spec.rb +49 -0
  36. data/spec/lib/yweather/units_spec.rb +25 -0
  37. data/spec/lib/yweather/wind_spec.rb +25 -0
  38. data/spec/spec_helper.rb +34 -0
  39. data/spec/support/helpers/spec_parse_helpers.rb +25 -0
  40. data/spec/support/helpers/spec_yweather_helpers.rb +16 -0
  41. data/yweather.gemspec +39 -0
  42. metadata +283 -0
@@ -0,0 +1,109 @@
1
+ # Yweather
2
+
3
+ ## Overview
4
+
5
+ This is a rewrite of Walter Korman's <shaper@fatgoose.com>
6
+ [yahoo-weather](http://rubyforge.org/projects/yahoo-weather) gem.
7
+
8
+ The aim of this port/rewrite is to add proper specs, better test-coverage and active
9
+ maintenance. I have ported much of Walter's original intent and LICENSE, and thank him for
10
+ his contribution. My initial motivation is to send Prowl notifications to my iPad, iPhone,
11
+ iPod whenever hazardous weather is coming.
12
+
13
+ Yweather provides an object-oriented Ruby interface to the Yahoo! Weather XML RSS feed
14
+ detailed at [http://developer.yahoo.com/weather](http://developer.yahoo.com/weather).
15
+
16
+ People care a lot about the weather. This may seem ironic given they can just glance out
17
+ the window. However, we can all understand a fascination with details and forecasting.
18
+
19
+ Log the weather information to your database! Graph it to your heart's content! Write a
20
+ widget that emails the weather to your cell phone every five minutes with a link to your
21
+ friend's PayPal account to deposit money if the weather's sunny and you both bet that it
22
+ would be rainy. And the fun doesn't have to stop there.
23
+
24
+ Source code is at [http://github.com/midwire/yweather](http://github.com/midwire/yweather).
25
+
26
+ NOTE: This library was updated as of December 2009 to use a new WOEID-based lookup
27
+ interface. Yahoo has deprecated the older non-WOEID-based lookup API. The archived page
28
+ with the deprecated API details is at:
29
+
30
+ [http://developer.yahoo.com/weather/archive.html](http://developer.yahoo.com/weather/archive.html)
31
+
32
+ ## Installation
33
+
34
+ gem install yweather
35
+
36
+ ## Usage
37
+
38
+ A simple example program:
39
+
40
+ #!/usr/bin/env ruby
41
+ require "yweather"
42
+ require "colorize"
43
+
44
+ zipcode = ARGV.shift
45
+ if zipcode.nil?
46
+ puts ">>> Usage: yweather zipcode"
47
+ exit
48
+ end
49
+
50
+ client = Yweather::Client.new
51
+ response = client.get_response_for_zipcode(zipcode)
52
+
53
+ # TODO: Use a ~/.yweather YAML file to determine format of output
54
+
55
+ print <<edoc
56
+ #{response.title.yellow}
57
+ #{response.condition.temp} degrees
58
+ #{response.condition.text}
59
+ edoc
60
+
61
+ puts "Wind:".yellow
62
+ response.wind_conditions.each_pair do |key, value|
63
+ puts " #{key}: #{value}"
64
+ end
65
+
66
+ puts "Atmosphere:".yellow
67
+ response.atmospheric_conditions.each_pair do |key, value|
68
+ puts " #{key}: #{value}"
69
+ end
70
+
71
+
72
+ Produces output as:
73
+
74
+ Conditions for Beverly Hills, CA at 10:51 am PST
75
+ 61 degrees
76
+ Fair
77
+ Wind:
78
+ chill: 61°
79
+ direction: 0°
80
+ speed: 0 mph
81
+ Atmosphere:
82
+ humidity: 23%
83
+ visibility: 10 mi
84
+ pressure: steady at 30.18in
85
+
86
+ There is a variety of detailed weather information in other attributes of the Yweather::Response object.
87
+
88
+ ## License
89
+
90
+ This library is provided via the GNU LGPL license at http://www.gnu.org/licenses/lgpl.html.
91
+
92
+ ## Author
93
+
94
+ Copyright 2006 - 2009, Walter Korman <shaper@fatgoose.com>,
95
+ http://lemurware.blogspot.com.
96
+
97
+ Copyright 2012, Chris Blackburn <chris you-know-what-goes-here m and-here blackburn@gmail and-here com>,
98
+ http://midwire.github.com
99
+
100
+ Thanks to Walter Korman for the initial hack.
101
+
102
+ ## Notes
103
+
104
+ Pull requests are appreciated. Please use feature/branches or hotfix/branches and add specs for your changes.
105
+
106
+ ## ToDo
107
+
108
+ * Use a ~/.yweather YAML file, or some other templating scheme, to determine format of output when running the binary.
109
+
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ spec_tasks = Dir['spec/*/'].map { |d| File.basename(d) }
5
+
6
+ spec_tasks.each do |folder|
7
+ RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
8
+ t.pattern = "./spec/#{folder}/**/*_spec.rb"
9
+ t.rspec_opts = %w(-fs --color)
10
+ end
11
+ end
12
+
13
+ desc "Run complete application spec suite"
14
+ task 'spec' => spec_tasks.map { |f| "spec:#{f}" }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ require "yweather"
3
+ require "colorize"
4
+
5
+ zipcode = ARGV.shift
6
+ if zipcode.nil?
7
+ puts ">>> Usage: yweather zipcode"
8
+ exit
9
+ end
10
+
11
+ client = Yweather::Client.new
12
+ response = client.get_response_for_zipcode(zipcode)
13
+
14
+ # TODO: Use a ~/.yweather YAML file to determine format of output
15
+
16
+ print <<edoc
17
+ #{response.title.yellow}
18
+ #{response.condition.temp} degrees
19
+ #{response.condition.text}
20
+ edoc
21
+
22
+ puts "Wind:".yellow
23
+ response.wind_conditions.each_pair do |key, value|
24
+ puts " #{key}: #{value}"
25
+ end
26
+
27
+ puts "Atmosphere:".yellow
28
+ response.atmospheric_conditions.each_pair do |key, value|
29
+ puts " #{key}: #{value}"
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'time'
2
+ require 'cgi'
3
+ require 'rest-client'
4
+ require 'yql'
5
+ require 'active_support/core_ext'
6
+ require 'json'
7
+ require "yweather/utils"
8
+ require "yweather/version"
9
+ require "yweather/client"
10
+ require "yweather/response"
11
+ require "yweather/astronomy"
12
+ require "yweather/atmosphere"
13
+ require "yweather/location"
14
+ require "yweather/wind"
15
+ require "yweather/units"
16
+ require "yweather/image"
17
+ require "yweather/condition"
18
+ require "yweather/forecast"
19
+
20
+ module Yweather
21
+ class YweatherException < RuntimeError; end
22
+ end
@@ -0,0 +1,12 @@
1
+ module Yweather
2
+ class Astronomy
3
+ attr_reader :sunrise
4
+ attr_reader :sunset
5
+
6
+ def initialize(data)
7
+ @sunrise = data[:sunrise]
8
+ @sunset = data[:sunset]
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module Yweather
2
+ class Atmosphere
3
+
4
+ class Barometer
5
+ STEADY = 'steady'
6
+ RISING = 'rising'
7
+ FALLING = 'falling'
8
+
9
+ # lists all possible barometer constants
10
+ ALL = [ STEADY, RISING, FALLING ]
11
+ end
12
+
13
+ attr_reader :humidity
14
+ attr_reader :visibility
15
+ attr_reader :pressure
16
+ attr_reader :barometer
17
+
18
+ def initialize(data)
19
+ @humidity = data[:humidity].to_i
20
+ @visibility = data[:visibility].to_i
21
+ @pressure = data[:pressure].to_f
22
+ @barometer = nil
23
+ case data[:rising].to_i
24
+ when 0
25
+ @barometer = Barometer::STEADY
26
+ when 1
27
+ @barometer = Barometer::RISING
28
+ when 2
29
+ @barometer = Barometer::FALLING
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,80 @@
1
+ module Yweather
2
+ class Client
3
+ @@BASE_URL = "http://weather.yahooapis.com/forecastrss"
4
+ attr_reader :response
5
+
6
+ def initialize
7
+ @response = nil
8
+ end
9
+
10
+ def get_feed_for_zipcode(zipcode, options = {})
11
+ woeid = woeid_from_zipcode(zipcode)
12
+ get_feed_for_woeid(woeid, options)
13
+ end
14
+
15
+ def get_feed_for_woeid(woeid, options = {})
16
+ units = options[:units] || 'f'
17
+ format = options[:format] || :json
18
+ url = build_url(woeid, units)
19
+ get(url, format)
20
+ end
21
+
22
+ def get_response_for_woeid(woeid, options = {})
23
+ units = options[:units] || 'f'
24
+ format = options[:format] || :hash
25
+ url = build_url(woeid, units)
26
+ hash = get(url, format)
27
+ @response = Response.new(woeid, nil, url, hash)
28
+ end
29
+
30
+ def get_response_for_zipcode(zipcode, options = {})
31
+ units = options[:units] || 'f'
32
+ format = options[:format] || :hash
33
+ woeid = woeid_from_zipcode(zipcode)
34
+ url = build_url(woeid, units)
35
+ hash = get(url, format)
36
+ @response = Response.new(woeid, zipcode, url, hash)
37
+ end
38
+
39
+ ##################################################
40
+ private
41
+
42
+ def build_url(woeid, units)
43
+ "#{@@BASE_URL}?w=#{CGI.escape(woeid.to_s)}&u=#{CGI.escape(units)}"
44
+ end
45
+
46
+ def woeid_from_zipcode(zipcode)
47
+ yql = Yql::Client.new
48
+ query = Yql::QueryBuilder.new 'geo.places'
49
+ query.select = 'woeid'
50
+ query.conditions = "text = '#{zipcode}, usa'"
51
+ yql.query = query
52
+ yql.format = 'json'
53
+ response = yql.get
54
+ h = ::HashWithIndifferentAccess.new(::JSON::parse(response.body))
55
+ if h[:query][:results][:place].is_a?(Array)
56
+ raise YweatherException, "Cannot determine WOEID from the ZIPCODE [#{zipcode}]"
57
+ return
58
+ end
59
+ h[:query][:results][:place][:woeid]
60
+ end
61
+
62
+ def get(url, format)
63
+ response = RestClient.get(url)
64
+ if response.body =~ /Error/
65
+ hash = ::HashWithIndifferentAccess.new(Hash.from_xml(response.body))
66
+ item = hash[:rss][:channel][:item]
67
+ raise YweatherException, "#{hash[:rss][:channel][:title]} :: #{item[:title]} :: #{item[:description]}"
68
+ return
69
+ end
70
+ if format == :xml
71
+ response.body
72
+ elsif format == :json
73
+ Hash.from_xml(response.body).to_json
74
+ elsif format == :hash
75
+ ::HashWithIndifferentAccess.new(Hash.from_xml(response.body))
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,71 @@
1
+ module Yweather
2
+ class Condition
3
+ include Yweather::Utils
4
+
5
+ CODES = {
6
+ 0 => "tornado",
7
+ 1 => "tropical storm",
8
+ 2 => "hurricane",
9
+ 3 => "severe thunderstorms",
10
+ 4 => "thunderstorms",
11
+ 5 => "mixed rain and snow",
12
+ 6 => "mixed rain and sleet",
13
+ 7 => "mixed snow and sleet",
14
+ 8 => "freezing drizzle",
15
+ 9 => "drizzle",
16
+ 10 => "freezing rain",
17
+ 11 => "showers",
18
+ 12 => "showers",
19
+ 13 => "snow flurries",
20
+ 14 => "light snow showers",
21
+ 15 => "blowing snow",
22
+ 16 => "snow",
23
+ 17 => "hail",
24
+ 18 => "sleet",
25
+ 19 => "dust",
26
+ 20 => "foggy",
27
+ 21 => "haze",
28
+ 22 => "smoky",
29
+ 23 => "blustery",
30
+ 24 => "windy",
31
+ 25 => "cold",
32
+ 26 => "cloudy",
33
+ 27 => "mostly cloudy (night)",
34
+ 28 => "mostly cloudy (day)",
35
+ 29 => "partly cloudy (night)",
36
+ 30 => "partly cloudy (day)",
37
+ 31 => "clear (night)",
38
+ 32 => "sunny",
39
+ 33 => "fair (night)",
40
+ 34 => "fair (day)",
41
+ 35 => "mixed rain and hail",
42
+ 36 => "hot",
43
+ 37 => "isolated thunderstorms",
44
+ 38 => "scattered thunderstorms",
45
+ 39 => "scattered thunderstorms",
46
+ 40 => "scattered showers",
47
+ 41 => "heavy snow",
48
+ 42 => "scattered snow showers",
49
+ 43 => "heavy snow",
50
+ 44 => "partly cloudy",
51
+ 45 => "thundershowers",
52
+ 46 => "snow showers",
53
+ 47 => "isolated thundershowers",
54
+ 3200 => "not available"
55
+ }
56
+
57
+ # the Yahoo! Weather condition code, as detailed at http://developer.yahoo.com/weather.
58
+ attr_reader :code
59
+ attr_reader :date
60
+ attr_reader :temp
61
+ attr_reader :text
62
+
63
+ def initialize(data)
64
+ @code = data[:code].to_i
65
+ @date = parse_time(data[:date])
66
+ @temp = data[:temp].to_i
67
+ @text = data[:text]
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,22 @@
1
+ module Yweather
2
+ class Forecast
3
+ include Yweather::Utils
4
+
5
+ attr_reader :day
6
+ attr_reader :date
7
+ attr_reader :low
8
+ attr_reader :high
9
+ attr_reader :text
10
+ attr_reader :code
11
+
12
+ def initialize (data)
13
+ @day = data[:day]
14
+ @date = parse_time(data[:date])
15
+ @low = data[:low].to_i
16
+ @high = data[:high].to_i
17
+ @text = data[:text]
18
+ @code = data[:code].to_i
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Yweather
2
+ class Image
3
+
4
+ attr_reader :height
5
+ attr_reader :link
6
+ attr_reader :title
7
+ attr_reader :url
8
+ attr_reader :width
9
+
10
+ def initialize (data)
11
+ @title = data[:title]
12
+ @link = data[:link]
13
+ @url = data[:url]
14
+ @height = data[:height]
15
+ @width = data[:width]
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Yweather
2
+ class Location
3
+
4
+ attr_reader :city
5
+ attr_reader :country
6
+ attr_reader :region
7
+
8
+ def initialize(data)
9
+ @city = data[:city]
10
+ @country = data[:country]
11
+ @region = data[:region]
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: ascii-8bit
2
+ module Yweather
3
+ class Response
4
+ attr_accessor :astronomy
5
+ attr_accessor :atmosphere
6
+ attr_accessor :condition
7
+ attr_accessor :description
8
+ attr_accessor :forecasts
9
+ attr_accessor :image
10
+ attr_accessor :latitude
11
+ attr_accessor :location
12
+ attr_accessor :longitude
13
+ attr_accessor :page_url
14
+ attr_accessor :request_location
15
+ attr_accessor :request_url
16
+ attr_accessor :title
17
+ attr_accessor :units
18
+ attr_accessor :wind
19
+
20
+ def initialize(woeid, zipcode, url, hash)
21
+ if hash[:rss][:channel][:title] =~ /Error/
22
+ item = hash[:rss][:channel][:item]
23
+ raise YweatherException, "#{hash[:rss][:channel][:title]} :: #{item[:title]} :: #{item[:description]}"
24
+ return
25
+ end
26
+
27
+ @astronomy = Astronomy.new(hash[:rss][:channel][:astronomy])
28
+ @atmosphere = Atmosphere.new(hash[:rss][:channel][:atmosphere])
29
+ @location = Location.new(hash[:rss][:channel][:location])
30
+ @wind = Wind.new(hash[:rss][:channel][:wind])
31
+ @units = Units.new(hash[:rss][:channel][:units])
32
+ @image = Image.new(hash[:rss][:channel][:image])
33
+
34
+ item = hash[:rss][:channel][:item]
35
+ @condition = Condition.new(item[:condition])
36
+ @forecasts = []
37
+ item[:forecast].each do |forecast|
38
+ @forecasts << Forecast.new(forecast)
39
+ end
40
+ @latitude = item[:lat]
41
+ @longitude = item[:long]
42
+ @page_url = item[:link]
43
+ @title = item[:title]
44
+ @description = item[:description]
45
+ end
46
+
47
+ def atmospheric_conditions(options = {})
48
+ pressure = (@atmosphere.barometer == "steady") ?
49
+ "#{@atmosphere.barometer} at #{@atmosphere.pressure}#{@units.pressure}" :
50
+ "#{@atmosphere.pressure}#{@units.pressure} and #{@atmosphere.barometer}"
51
+
52
+ h = ::HashWithIndifferentAccess.new({
53
+ :humidity => "#{@atmosphere.humidity}%",
54
+ :visibility => "#{@atmosphere.visibility} #{@units.distance}",
55
+ :pressure => "#{pressure}"
56
+ })
57
+
58
+ return_content(h, options[:format])
59
+ end
60
+
61
+ def wind_conditions(options = {})
62
+ # chill: wind chill in degrees (integer)
63
+ # direction: wind direction, in degrees (integer)
64
+ # speed: wind speed, in the units specified in the speed attribute of the yweather:units element (mph or kph). (integer)
65
+ h = ::HashWithIndifferentAccess.new({
66
+ :chill => "#{@wind.chill}°",
67
+ :direction => "#{@wind.direction}°",
68
+ :speed => "#{@wind.speed} #{@units.speed}"
69
+ })
70
+ return_content(h, options[:format])
71
+ end
72
+
73
+ ##################################################
74
+ private
75
+
76
+ def return_content(hash, format)
77
+ if format == :json
78
+ hash.to_json
79
+ elsif format == :xml
80
+ hash.to_xml
81
+ else
82
+ hash
83
+ end
84
+ end
85
+
86
+ end
87
+ end