technicalpickles-daywalker 0.1.2 → 0.2.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.
data/README.rdoc CHANGED
@@ -7,7 +7,7 @@ A Ruby wrapper for the Sunlight Labs API: http://wiki.sunlightlabs.com/Sunlight_
7
7
  # Run the following if you haven't already:
8
8
  gem sources -a http://gems.github.com
9
9
  # Install the gem(s):
10
- sudo gem install technicalpickles-daywalke
10
+ sudo gem install technicalpickles-daywalker
11
11
 
12
12
  == Get an API key
13
13
 
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 1
3
- :patch: 2
2
+ :minor: 2
3
+ :patch: 0
4
4
  :major: 0
data/lib/daywalker.rb CHANGED
@@ -1,14 +1,34 @@
1
1
  require 'happymapper'
2
2
  require 'httparty'
3
+ require 'graticule'
3
4
 
4
5
  require 'daywalker/base'
5
6
  require 'daywalker/type_converter'
6
7
  require 'daywalker/dynamic_finder_match'
7
8
  require 'daywalker/district'
8
9
  require 'daywalker/legislator'
10
+ require 'daywalker/geocoder'
9
11
 
12
+ # Daywalker is a Ruby-wrapper around the Sunlight API (http://wiki.sunlightlabs.com/Sunlight_API_Documentation). It implements all the functionality of the API related to Districts and Legislators (the Lobbyist API is considered experimental).
13
+ #
14
+ # Before using the API, you must register for an API key: http://services.sunlightlabs.com/api/register/
15
+ #
16
+ # After registering, you'll receive an email with a link to activate the key.
17
+ #
18
+ # To begin with, here's a small example script to print out all the districts and legislators for a zipcode:
19
+ #
20
+ # require 'rubygems'
21
+ # require 'pp'
22
+ # require 'daywalker'
23
+ #
24
+ # Daywalker.api_key = 'the api key you received'
25
+ #
26
+ # pp Daywalker::Legislator.all_by_zip(02114)
27
+ # pp Daywalker::District.all_by_zip(02114)
28
+ #
29
+ # See Daywalker::District and Daywalker::Legislator for more details on usage.
10
30
  module Daywalker
11
- # Set the API to be used
31
+ # Set the API to be used. This must be set when using Daywalker, BadApiKeyErrors will be occur.
12
32
  def self.api_key=(api_key)
13
33
  @api_key = api_key
14
34
  end
@@ -18,7 +38,25 @@ module Daywalker
18
38
  @api_key
19
39
  end
20
40
 
41
+ def self.geocoder=(geocoder) # :nodoc:
42
+ @geocoder = geocoder
43
+ end
44
+
45
+ def self.geocoder # :nodoc:
46
+ @geocoder
47
+ end
48
+
49
+ self.geocoder = Daywalker::Geocoder.new
50
+
21
51
  # Error for when you use the API with a bad API key
22
- class BadApiKey < StandardError
52
+ class BadApiKeyError < StandardError
53
+ end
54
+
55
+ # Error for when an address can't be geocoded
56
+ class AddressError < StandardError
57
+ end
58
+
59
+ # Error for when an object was specifically looked for, but does not exist
60
+ class NotFoundError < StandardError
23
61
  end
24
62
  end
@@ -7,7 +7,7 @@ module Daywalker
7
7
 
8
8
  def self.handle_response(response)
9
9
  case response.code.to_i
10
- when 403 then raise BadApiKey
10
+ when 403 then raise BadApiKeyError
11
11
  when 200
12
12
  begin
13
13
  parse(response.body)
@@ -21,7 +21,10 @@ module Daywalker
21
21
  end
22
22
 
23
23
  def self.handle_bad_request(body)
24
- raise "Don't know how to handle #{body.inspect}"
24
+ case body
25
+ when "No Such Object Exists" then raise NotFoundError
26
+ else raise "Don't know how to handle #{body.inspect}"
27
+ end
25
28
  end
26
29
  end
27
30
  end
@@ -11,23 +11,26 @@ module Daywalker
11
11
  element 'number', Integer
12
12
  element 'state', String
13
13
 
14
- # Find districts by latitude and longitude.
15
- def self.find_by_latlng(lat, lng)
16
- # TODO use ArgumentError
17
- raise(ArgumentError, 'missing required parameter latitude') if lat.nil?
14
+ # Find the district for a specific latitude and longitude.
15
+ #
16
+ # Returns a District. Raises ArgumentError if you omit latitude or longitude.
17
+ def self.unique_by_latitude_and_longitude(latitude, longitude)
18
+ raise(ArgumentError, 'missing required parameter latitude') if latitude.nil?
19
+ raise(ArgumentError, 'missing required parameter longitude') if longitude.nil?
18
20
 
19
21
  query = {
20
- :latitude => lat,
21
- :longitude => lng,
22
+ :latitude => latitude,
23
+ :longitude => longitude,
22
24
  :apikey => Daywalker.api_key
23
25
  }
24
26
  response = get('/districts.getDistrictFromLatLong.xml', :query => query)
25
- handle_response(response) # TODO should only ever return one?
27
+ handle_response(response).first
26
28
  end
27
29
 
28
- # Find districts by zip code
29
- def self.find_by_zip(zip)
30
- # TODO use ArgumentError
30
+ # Finds all districts for a specific zip code.
31
+ #
32
+ # Returns an Array of Districts. Raises ArgumentError if you omit the zip.
33
+ def self.all_by_zipcode(zip)
31
34
  raise(ArgumentError, 'missing required parameter zip') if zip.nil?
32
35
 
33
36
  query = {
@@ -39,5 +42,16 @@ module Daywalker
39
42
  handle_response(response)
40
43
  end
41
44
 
45
+ # Find the district for a specific address.
46
+ #
47
+ # Returns a District.
48
+ #
49
+ # Raises Daywalker::AddressError if the address can't be geocoded.
50
+ def self.unique_by_address(address)
51
+ raise(ArgumentError, 'missing required parameter address') if address.nil?
52
+ location = Daywalker.geocoder.locate(address)
53
+
54
+ unique_by_latitude_and_longitude(location[:latitude], location[:longitude])
55
+ end
42
56
  end
43
57
  end
@@ -4,11 +4,8 @@ module Daywalker
4
4
  attr_accessor :finder, :attribute_names
5
5
  def initialize(method)
6
6
  case method.to_s
7
- when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
8
- @finder = case $1
9
- when 'all_by' then :all
10
- when 'by' then :one
11
- end
7
+ when /^(unique|all)_by_([_a-zA-Z]\w*)$/
8
+ @finder = $1.to_sym
12
9
  @attribute_names = $2.split('_and_').map {|each| each.to_sym}
13
10
  end
14
11
  end
@@ -18,6 +15,5 @@ module Daywalker
18
15
  Daywalker::Legislator::VALID_ATTRIBUTES.include? each
19
16
  end
20
17
  end
21
-
22
18
  end
23
19
  end
@@ -0,0 +1,18 @@
1
+ module Daywalker
2
+ class Geocoder # :nodoc:
3
+
4
+ def locate(address)
5
+ location = geocoder.locate(address)
6
+ { :longitude => location.longitude, :latitude => location.latitude }
7
+ rescue Graticule::AddressError => e
8
+ raise Daywalker::AddressError, e.message
9
+ end
10
+
11
+ private
12
+
13
+ def geocoder
14
+ Graticule.service(:geocoder_us).new
15
+ end
16
+
17
+ end
18
+ end
@@ -2,36 +2,90 @@ module Daywalker
2
2
  # Represents a legislator, either a Senator or Representative.
3
3
  #
4
4
  # They have the following attributes:
5
- # * district_number
6
- # * title (ether :senator or :representative)
7
- # * eventful_id (on http://eventful.com)
8
- # * in_office (true or false)
9
- # * state (two-letter abbreviation)
10
- # * votesmart_id (on http://www.votesmart.org)
11
- # * party (:democrat, :republican, or :independent)
12
- # * crp_id (on http://opensecrets.org)
13
- # * website_url
14
- # * fax_number
15
- # * govtrack_id (on http://www.govtrack.us)
16
- # * first_name
17
- # * middle_name
18
- # * last_name
19
- # * congress_office (address in Washington, DC)
20
- # * bioguide_id (on http://bioguide.congress.gov)
21
- # * webform_url
22
- # * youtube_url
23
- # * nickname
24
- # * phone
25
- # * fec_id (on http://fec.gov)
26
- # * gender (:male or :female)
27
- # * name_suffix
28
- # * twitter_id (on http://twitter.com)
29
- # * congresspedia_url
5
+ #
6
+ # +title+::
7
+ # The title held by a Legislator, as a Symbol, ether <tt>:senator</tt> or <tt>:representative</tt>
8
+ #
9
+ # +first_name+::
10
+ # Legislator's first name
11
+ #
12
+ # +middle_name+::
13
+ # Legislator's middle name
14
+ #
15
+ # +last_name+::
16
+ # Legislator's last name
17
+ #
18
+ # +name_suffix+::
19
+ # Legislator's suffix (Jr., III, etc)
20
+ #
21
+ # +nickname+::
22
+ # Preferred nickname of Legislator
23
+ #
24
+ # +party+::
25
+ # Legislator's party as a +Sybmol+, <tt>:democrat</tt>, <tt>:republican</tt>, or <tt>:independent</tt>.
26
+ #
27
+ # +state+::
28
+ # two-letter +String+ abbreviation of the Legislator's state.
29
+ #
30
+ # +district+::
31
+ # The district a Legislator represents. For Representatives, this is a +Fixnum+. For Senators, this is a +Symbol+, either <tt>:junior_seat</tt> or <tt>:senior_seat</tt>.
32
+ #
33
+ # +in_office+::
34
+ # +true+ if the Legislator is currently server, or false if the Legislator is no longer due to defeat/resignation/death/etc.
35
+ #
36
+ # +gender+::
37
+ # Legislator's gender as a +Symbol+, :male or :female
38
+ #
39
+ # +phone+::
40
+ # Legislator's Congressional office phone number
41
+ # # FIXME normalize this to phone_number
42
+ #
43
+ # +fax_number+::
44
+ # Legislator's Congressional office fax number
45
+ #
46
+ # +website_url+::
47
+ # URL of the Legislator's Congressional wesbite as a +String+
48
+ #
49
+ # +webform_url+::
50
+ # URL of the Legislator's web contact form as a +String+
51
+ #
52
+ # +email+::
53
+ # Legislator's email address
54
+ #
55
+ # +congress_office+::
56
+ # Legislator's Washington, DC Office Address
57
+ #
58
+ # +bioguide_id+::
59
+ # Legislator's ID assigned by the COngressional Biographical DIrectory (http://bioguide.congress.gov) and also used by the Washington Post and NY Times.
60
+ #
61
+ # +votesmart_id+::
62
+ # Legislator ID assigned by Project Vote Smart (http://www.votesmart.org).
63
+ #
64
+ # +fec_id+::
65
+ # Legislator's ID provided by the Federal Election Commission (http://fec.gov)
66
+ #
67
+ # +govtrack_id+::
68
+ # Legislator's ID provided by Govtrack.us (http://www.govtrack.us)
69
+ #
70
+ # +crp_id+::
71
+ # Legislator's ID provided by Center for Responsive Politics (http://opensecrets.org)
72
+ #
73
+ # +eventful_id+::
74
+ # Legislator's 'Performer ID' on http://eventful.com
75
+ #
76
+ # +congresspedia_url+::
77
+ # URL of the Legislator's Congresspedia entry (http://congresspedia.org)
78
+ #
79
+ # +twitter_id+::
80
+ # Legislator's ID on Twitter (http://twitter.com)
81
+ #
82
+ # +youtube_url+::
83
+ # URL of the Legislator's YouTube account (http://youtube.com) as a +String+
30
84
  class Legislator < Base
31
85
  include HappyMapper
32
86
 
33
87
  tag 'legislator'
34
- element 'district_number', Integer, :tag => 'district'
88
+ element 'district', TypeConverter, :tag => 'district', :parser => :district_to_sym_or_i
35
89
  element 'title', TypeConverter, :parser => :title_abbr_to_sym
36
90
  element 'eventful_id', String
37
91
  element 'in_office', Boolean
@@ -60,11 +114,12 @@ module Daywalker
60
114
  element 'sunlight_old_id', String
61
115
  element 'congresspedia_url', String
62
116
 
63
- VALID_ATTRIBUTES = [:district_number, :title, :eventful_id, :in_office, :state, :votesmart_id, :official_rss_url, :party, :email, :crp_id, :website_url, :fax_number, :govtrack_id, :first_name, :middle_name, :last_name, :congress_office, :bioguide_id, :webform_url, :youtube_url, :nickname, :phone, :fec_id, :gender, :name_suffix, :twitter_id, :sunlight_old_id, :congresspedia_url]
117
+ VALID_ATTRIBUTES = [:district, :title, :eventful_id, :in_office, :state, :votesmart_id, :official_rss_url, :party, :email, :crp_id, :website_url, :fax_number, :govtrack_id, :first_name, :middle_name, :last_name, :congress_office, :bioguide_id, :webform_url, :youtube_url, :nickname, :phone, :fec_id, :gender, :name_suffix, :twitter_id, :sunlight_old_id, :congresspedia_url]
64
118
 
65
- # Find all legislators in a particular zip code
66
- def self.find_all_by_zip(zip)
119
+ # Find all Legislators who serve a particular zip code. This would include the Junior and Senior Senators, as well as the Representatives of any Districts in the zip code.
120
+ def self.all_by_zip(zip)
67
121
  raise ArgumentError, 'missing required parameter zip' if zip.nil?
122
+
68
123
  query = {
69
124
  :zip => zip,
70
125
  :apikey => Daywalker.api_key
@@ -74,38 +129,79 @@ module Daywalker
74
129
  handle_response(response)
75
130
  end
76
131
 
77
- # Find one or many legislators, based on a set of conditions. See
78
- # VALID_ATTRIBUTES for possible attributes you can search for.
79
- #
80
- # If you want one legislators, and you expect there is exactly one
81
- # legislator, use :one. An error will be raised if there are more than
82
- # one result. An ArgumentErrror will be raised if multiple results come back.
132
+ # Find a unique legislator matching a Hash of attribute name/values. See VALID_ATTRIBUTES for possible attributes.
83
133
  #
84
- # Daywalker::Legislator.find(:one, :state => 'NY', :district => 4)
134
+ # Daywalker::Legislator.unique(:state => 'NY', :district => 4)
85
135
  #
86
- # Otherwise, use :all.
87
- #
88
- # Daywalker::Legislator.find(:all, :state => 'NY', :title => :senator)
136
+ # Dynamic finders based on the attribute names are also possible. This query can be rewritten as:
89
137
  #
90
- # Additionally, dynamic finders based on these attributes are available:
138
+ # Daywalker::Legislator.unique_by_state_and_district('NY', 4)
91
139
  #
92
- # Daywalker::Legislator.find_by_state_and_district('NY', 4)
93
- # Daywalker::Legislator.find_all_by_state_and_senator('NY', :senator)
94
- def self.find(sym, conditions)
95
- url = case sym
96
- when :one then '/legislators.get.xml'
97
- when :all then '/legislators.getList.xml'
98
- else raise ArgumentError, "invalid argument #{sym.inspect}, only :one and :all are allowed"
99
- end
140
+ # Returns a Legislator. ArgumentError is raised if more than one result is found.
141
+ #
142
+ # Gotchas:
143
+ # * Results are case insensative (Richard and richard are equivilant)
144
+ # * Results are exact (Richard vs Rich are not the same)
145
+ def self.unique(conditions)
146
+ conditions = TypeConverter.normalize_conditions(conditions)
147
+ query = conditions.merge(:apikey => Daywalker.api_key)
148
+
149
+ response = get('/legislators.get.xml', :query => query)
150
+
151
+ handle_response(response).first
152
+ end
100
153
 
154
+ # Find all legislators matching a Hash of attribute name/values. See VALID_ATTRIBUTES for possible attributes.
155
+ #
156
+ # Daywalker::Legislator.all(:state => 'NY', :title => :senator)
157
+ #
158
+ # Dynamic finders based on the attribute names are also possible. This query can be rewritten as:
159
+ #
160
+ # Daywalker::Legislator.all_by_state_and_title('NY', :senator)
161
+ #
162
+ # Returns an Array of Legislators.
163
+ #
164
+ # Gotchas:
165
+ # * Results are case insensative (Richard and richard are equivilant)
166
+ # * Results are exact (Richard vs Rich)
167
+ # * nil attributes will match anything, not legislators without a value for the attribute
168
+ # * Passing an Array of values to match, ie <tt>:state => ['NH', 'MA']</tt> is not supported at this time
169
+ def self.all(conditions)
101
170
  conditions = TypeConverter.normalize_conditions(conditions)
102
171
  query = conditions.merge(:apikey => Daywalker.api_key)
103
- response = get(url, :query => query)
104
172
 
105
- case sym
106
- when :one then handle_response(response).first
107
- when :all then handle_response(response)
108
- end
173
+ response = get('/legislators.getList.xml', :query => query)
174
+
175
+ handle_response(response)
176
+ end
177
+
178
+ # Find all the legislators serving a specific latitude and longitude. This will include the district's Represenative, the Senior Senator, and the Junior Senator.
179
+ #
180
+ # Returns a Hash containing keys :representative, :junior_senator, and :senior_senator, with values corresponding to the appropriate Legislator.
181
+ #
182
+ def self.all_by_latitude_and_longitude(latitude, longitude)
183
+ district = District.unique_by_latitude_and_longitude(latitude, longitude)
184
+
185
+ representative = unique_by_state_and_district(district.state, district.number)
186
+ junior_senator = unique_by_state_and_district(district.state, :junior_seat)
187
+ senior_senator = unique_by_state_and_district(district.state, :senior_seat)
188
+
189
+ {
190
+ :representative => representative,
191
+ :junior_senator => junior_senator,
192
+ :senior_senator => senior_senator
193
+ }
194
+ end
195
+
196
+ # Find all the legislators serving a specific address. This will include the district's Represenative, the Senior Senator, and the Junior Senator.
197
+ #
198
+ # Returns a Hash containing keys :representative, :junior_senator, and :senior_senator, with values corresponding to the appropriate Legislator.
199
+ #
200
+ # Raises Daywalker::AddressError if the address can't be geocoded.
201
+ def self.all_by_address(address)
202
+ location = Daywalker.geocoder.locate(address)
203
+
204
+ all_by_latitude_and_longitude(location[:latitude], location[:longitude])
109
205
  end
110
206
 
111
207
  def self.method_missing(method_id, *args, &block) # :nodoc:
@@ -118,7 +214,7 @@ module Daywalker
118
214
  end
119
215
  end
120
216
 
121
- def self.respond_to?(method_id) # :nodoc:
217
+ def self.respond_to?(method_id, include_private = false) # :nodoc:
122
218
  match = DynamicFinderMatch.new(method_id)
123
219
  if match.match?
124
220
  true
@@ -132,6 +228,7 @@ module Daywalker
132
228
  def self.handle_bad_request(body) # :nodoc:
133
229
  case body
134
230
  when "Multiple Legislators Returned" then raise(ArgumentError, "The conditions provided returned multiple results, by only one is expected")
231
+ # FIXME need to catcfh when legislator (or any object, which would mean it goes in super) is not found
135
232
  else super
136
233
  end
137
234
  end
@@ -139,12 +236,12 @@ module Daywalker
139
236
 
140
237
  def self.create_finder_method(method, finder, attribute_names) # :nodoc:
141
238
  class_eval %{
142
- def self.#{method}(*args) # def self.find_all_by_district_number_and_state(*args)
239
+ def self.#{method}(*args) # def self.all_by_district_and_state(*args)
143
240
  conditions = args.last.kind_of?(Hash) ? args.pop : {} # conditions = args.last.kind_of?(Hash) ? args.pop : {}
144
- [:#{attribute_names.join(', :')}].each_with_index do |key, index| # [:district_number, :state].each_with_index do |key, index|
241
+ [:#{attribute_names.join(', :')}].each_with_index do |key, index| # [:district, :state].each_with_index do |key, index|
145
242
  conditions[key] = args[index] # conditions[key] = args[index]
146
243
  end # end
147
- find(#{finder.inspect}, conditions) # find(:all, conditions)
244
+ #{finder}(conditions) # all(conditions)
148
245
  end # end
149
246
  }
150
247
  end