luigi-sunlight 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.textile CHANGED
@@ -1,3 +1,11 @@
1
+ h3. 0.2.0 / 2009-03-01
2
+
3
+ * Add support for twitter_id and youtube_url on Legislator
4
+ * Add Legislator#search_by_name for fuzzy name searching
5
+ * Add Legislator#all_in_zipcode for better lookups on a five-digit zip code
6
+ * Raise exception when API Key isn't set
7
+ * Credit for various fixes goes to GitHub users pengwynn, hoverbird, and wilson
8
+
1
9
  h3. 0.1.0 / 2008-08-20
2
10
 
3
11
  * Initial version
data/README.textile CHANGED
@@ -38,18 +38,22 @@ First, register for an API key "here":http://services.sunlightlabs.com/api/regis
38
38
 
39
39
  Then, you'll want to stick the following lines somewhere in your Ruby environment.
40
40
 
41
- <code>
41
+ <pre><code>
42
42
  require 'rubygems'
43
43
  require 'sunlight'
44
44
  include Sunlight
45
45
  Sunlight.api_key = 'yourapikeyfromtheurlabove'
46
- </code>
46
+ </code></pre>
47
47
 
48
- If you're testing this out in IRB, you'll run them one at a time. Using Rails? Stick them in a file called @sunlight.rb@ in RAILS_ROOT/config/initializers.
48
+ If you're testing this out in IRB, you'll run them one at a time. If you're using Rails 2.1, stick them in a file called @sunlight.rb@ in @RAILS_ROOT/config/initializers@. They'll load on app startup.
49
49
 
50
50
  h2. Usage
51
51
 
52
- Now, it's time to get to the good stuff. The most useful method is @Legislator#all_for@:
52
+ The Sunlight gem currently supports the Legislator and District objects. The Lobbyist object is coming soon.
53
+
54
+ h3. Legislator
55
+
56
+ Time to get to the good stuff. The most useful method is @Legislator#all_for@:
53
57
 
54
58
  <pre><code>
55
59
  congresspeople = Legislator.all_for(:address => "123 Fifth Ave New York, NY 10003")
@@ -63,17 +67,28 @@ Now, it's time to get to the good stuff. The most useful method is @Legislator#a
63
67
  junior_senator.phone # returns "202-224-4451"
64
68
  </code></pre>
65
69
 
66
- Note that you should make the best attempt to get a full street address. A five-digit zip-code alone won't cut it, as there may be multiple congressional districts in a single zip code (there are ways to deal with this below). Make sure to review all the available fields from the "Sunlight Labs API":http://services.sunlightlabs.com/api/docs/legislators/.
70
+ Note that you should make the best attempt to get a full street address, as that is geocoded behind the scenes into a lat/long pair. If all you have is a five-digit zip code, you should not use @Legislator#all_for@, instead opting for @Legislator#all_in_zipcode@ (see below). If you pass in a zip+4, then go ahead and use @Legislator#all_for@.
67
71
 
68
- So @Legislator#all_for@ returns a hash of @Legislator@ objects, and the keys are @:senior_senator@, @:junior_senator@, and @:representative@. You can also pass in a lat/long pair:
72
+ So @Legislator#all_for@ returns a hash of @Legislator@ objects, and the keys are @:senior_senator@, @:junior_senator@, and @:representative@. Make sure to review all the available fields from the "Sunlight Labs API":http://services.sunlightlabs.com/api/docs/legislators/. You can also pass in a lat/long pair:
69
73
 
70
74
  <pre><code>
71
75
  congresspeople = Legislator.all_for(:latitude => 33.876145, :longitude => -84.453789)
72
76
  </code></pre>
73
77
 
74
- This bypasses the geocoding necessary by the Google Maps API. For social networks and other applications with a User object, it makes sense to geocode the address up front and save the lat/long data in the local database. Then, use the lat/long pair instead of address, which cuts a substantial bit time from the @Legislator#all_for@ request since the Google Maps API Geocoding function doesn't have to be called.
78
+ This bypasses the geocoding necessary by the Google Maps API. For social networks and other applications with a User object, it makes sense to geocode the user's address up front and save the lat/long data in the local database. Then, use the lat/long pair instead of address, which cuts a substantial bit of time from the @Legislator#all_for@ request since the Google Maps API Geocoding function doesn't have to be called.
79
+
80
+ Have a five-digit zip code only? You can use the @Legislator#all_in_zipcode@ method, but keep in mind that a single zip may have multiple U.S. Representatives, as congressional district lines frequently divide populous zip codes. Unlike @Legislator#all_for@, this method returns an array of legislators, and it'll be up to you to parse through them (there will be a senior senator, a junior senator, and one or more representatives).
81
+
82
+ <pre><code>
83
+ members_of_congress = Legislator.all_in_zipcode(90210)
84
+
85
+ members_of_congress.each do |member|
86
+ # do stuff
87
+ end
88
+
89
+ </code></pre>
75
90
 
76
- You can also use the @Legislator#all_where@ method for searching based on available fields. You'll get back an array of Legislator objects:
91
+ You can also use the @Legislator#all_where@ method for searching based on available fields. Again, you'll get back an array of @Legislator@ objects:
77
92
 
78
93
  <pre><code>
79
94
  johns = Legislator.all_where(:firstname => "John")
@@ -85,7 +100,18 @@ You can also use the @Legislator#all_where@ method for searching based on availa
85
100
  end
86
101
  </code></pre>
87
102
 
88
- There's also the @District@ object. @District#get@ takes in either lat/long or an address and does it's best to return correct Congressional District:
103
+ Lastly, to provide your users with a name search functionality, use @Legislator#search_by_name@, which uses fuzzy matching to compensate for nicknames and misspellings. So "Teddy Kennedey" (real name Edward Kennedy) and "Jack Murtha" (real name John Murtha) will return the correct matches. You can specify a higher confidence threshold (default set to 0.80) if you feel that the matches being returned aren't accurate enough. This also returns an array of @Legislator@ objects:
104
+
105
+ <pre><code>
106
+ legislators = Legislator.search_by_name("Teddy Kennedey")
107
+ legislators = Legislator.search_by_name("Johnny Boy Kerry", 0.91)
108
+ </code></pre>
109
+
110
+
111
+ h3. District
112
+
113
+
114
+ There's also the @District@ object. @District#get@ takes in either lat/long or an address and does it's best to return the correct Congressional District:
89
115
 
90
116
  <pre><code>
91
117
  district = District.get(:latitude => 33.876145, :longitude => -84.453789)
@@ -95,7 +121,7 @@ There's also the @District@ object. @District#get@ takes in either lat/long or a
95
121
  district = District.get(:address => "123 Fifth Ave New York, NY")
96
122
  </code></pre>
97
123
 
98
- Finally, two more methods, @District.all_from_zipcode@ and @District.zipcodes_in@, help you out when all you have is a five-digit zip code and want to make sure you account for all the districts in a zip code, or if you want to get back all zipcodes in a given district.
124
+ Finally, two more methods, @District.all_from_zipcode@ and @District.zipcodes_in@, help you out when you want to get all districts in a given zip code, or if you want to get back all zip codes in a given district.
99
125
 
100
126
  <pre><code>
101
127
  districts = District.all_from_zipcode(90210) # returns array of District objects
@@ -107,7 +133,7 @@ h2. License
107
133
 
108
134
  See the terms of usage for the "Sunlight Labs API":http://services.sunlightlabs.com/api/ and the "Google Maps API":http://code.google.com/apis/maps/terms.html.
109
135
 
110
- Copyright &copy; 2008 by Luigi Montanez under the MIT License.
136
+ Copyright &copy; 2009 by Luigi Montanez under the MIT License.
111
137
 
112
138
  Permission is hereby granted, free of charge, to any person
113
139
  obtaining a copy of this software and associated documentation
data/lib/sunlight.rb CHANGED
@@ -2,58 +2,61 @@ require 'rubygems'
2
2
  require 'json'
3
3
  require 'cgi'
4
4
  require 'ym4r/google_maps/geocoding'
5
+ require 'net/http'
5
6
  include Ym4r::GoogleMaps
6
7
 
7
8
  module Sunlight
8
-
9
+
9
10
  API_URL = "http://services.sunlightlabs.com/api/"
10
11
  API_FORMAT = "json"
11
12
  attr_accessor :api_key
12
-
13
+
13
14
  # Houses general methods to work with the Sunlight and Google Maps APIs
14
15
  class SunlightObject
15
-
16
-
16
+
17
+
17
18
  # Constructs a Sunlight API-friendly URL
18
19
  def self.construct_url(api_method, params)
19
- "#{API_URL}#{api_method}.#{API_FORMAT}?apikey=#{Sunlight.api_key}#{hash2get(params)}"
20
+ if Sunlight.api_key == nil or Sunlight.api_key == ''
21
+ raise "Failed to provide Sunlight API Key"
22
+ else
23
+ "#{API_URL}#{api_method}.#{API_FORMAT}?apikey=#{Sunlight.api_key}#{hash2get(params)}"
24
+ end
20
25
  end
21
-
22
-
23
-
26
+
27
+
24
28
  # Converts a hash to a GET string
25
29
  def self.hash2get(h)
26
-
30
+
27
31
  get_string = ""
28
-
32
+
29
33
  h.each_pair do |key, value|
30
34
  get_string += "&#{key.to_s}=#{CGI::escape(value.to_s)}"
31
35
  end
32
-
36
+
33
37
  get_string
34
-
38
+
35
39
  end # def hash2get
36
40
 
37
-
41
+
38
42
  # Use the Net::HTTP and JSON libraries to make the API call
39
43
  #
40
44
  # Usage:
41
45
  # District.get_json_data("http://someurl.com") # returns Hash of data or nil
42
46
  def self.get_json_data(url)
43
-
47
+
44
48
  response = Net::HTTP.get_response(URI.parse(url))
45
49
  if response.class == Net::HTTPOK
46
50
  result = JSON.parse(response.body)
47
51
  else
48
52
  nil
49
53
  end
50
-
54
+
51
55
  end # self.get_json_data
52
-
53
-
54
-
56
+
57
+
55
58
  end # class SunlightObject
56
-
59
+
57
60
  end # module Sunlight
58
61
 
59
62
  Dir["#{File.dirname(__FILE__)}/sunlight/*.rb"].each { |source_file| require source_file }
@@ -1,9 +1,9 @@
1
1
  module Sunlight
2
2
 
3
3
  class District < SunlightObject
4
-
4
+
5
5
  attr_accessor :state, :number
6
-
6
+
7
7
  def initialize(state, number)
8
8
  @state = state
9
9
  @number = number
@@ -17,14 +17,14 @@ module Sunlight
17
17
  def self.get(params)
18
18
 
19
19
  if (params[:latitude] and params[:longitude])
20
-
20
+
21
21
  get_from_lat_long(params[:latitude], params[:longitude])
22
-
22
+
23
23
  elsif (params[:address])
24
24
 
25
25
  # get the lat/long from Google
26
26
  placemarks = Geocoding::get(params[:address])
27
-
27
+
28
28
  unless placemarks.empty?
29
29
  placemark = placemarks[0]
30
30
  get_from_lat_long(placemark.latitude, placemark.longitude)
@@ -33,7 +33,7 @@ module Sunlight
33
33
  else
34
34
  nil # appropriate params not found
35
35
  end
36
-
36
+
37
37
  end
38
38
 
39
39
 
@@ -42,46 +42,46 @@ module Sunlight
42
42
  # District.get_from_lat_long(-123, 123) # returns District object or nil
43
43
  #
44
44
  def self.get_from_lat_long(latitude, longitude)
45
-
45
+
46
46
  url = construct_url("districts.getDistrictFromLatLong", {:latitude => latitude, :longitude => longitude})
47
-
47
+
48
48
  if (result = get_json_data(url))
49
-
49
+
50
50
  districts = []
51
51
  result["response"]["districts"].each do |district|
52
52
  districts << District.new(district["district"]["state"], district["district"]["number"])
53
53
  end
54
-
54
+
55
55
  districts.first
56
-
56
+
57
57
  else
58
58
  nil
59
59
  end # if response.class
60
-
60
+
61
61
  end
62
-
62
+
63
63
 
64
64
 
65
65
  # Usage:
66
66
  # District.all_from_zipcode(90210) # returns array of District objects
67
67
  #
68
68
  def self.all_from_zipcode(zipcode)
69
-
69
+
70
70
  url = construct_url("districts.getDistrictsFromZip", {:zip => zipcode})
71
-
71
+
72
72
  if (result = get_json_data(url))
73
-
73
+
74
74
  districts = []
75
75
  result["response"]["districts"].each do |district|
76
76
  districts << District.new(district["district"]["state"], district["district"]["number"])
77
77
  end
78
-
78
+
79
79
  districts
80
-
80
+
81
81
  else
82
82
  nil
83
83
  end # if response.class
84
-
84
+
85
85
  end
86
86
 
87
87
 
@@ -90,9 +90,9 @@ module Sunlight
90
90
  # District.zipcodes_in("NY", 29) # returns ["14009", "14024", "14029", ...]
91
91
  #
92
92
  def self.zipcodes_in(state, number)
93
-
93
+
94
94
  url = construct_url("districts.getZipsFromDistrict", {:state => state, :district => number})
95
-
95
+
96
96
  if (result = get_json_data(url))
97
97
  result["response"]["zips"]
98
98
  else
@@ -100,9 +100,9 @@ module Sunlight
100
100
  end # if response.class
101
101
 
102
102
  end
103
-
104
-
103
+
104
+
105
105
 
106
106
  end # class District
107
107
 
108
- end # module Sunlight
108
+ end # module Sunlight
@@ -1,13 +1,14 @@
1
1
  module Sunlight
2
2
 
3
3
  class Legislator < SunlightObject
4
-
4
+
5
5
 
6
6
  attr_accessor :title, :firstname, :middlename, :lastname, :name_suffix, :nickname,
7
7
  :party, :state, :district, :gender, :phone, :fax, :website, :webform,
8
8
  :email, :congress_office, :bioguide_id, :votesmart_id, :fec_id,
9
- :govtrack_id, :crp_id, :event_id, :congresspedia_url
10
-
9
+ :govtrack_id, :crp_id, :event_id, :congresspedia_url, :youtube_url,
10
+ :twitter_id, :fuzzy_score
11
+
11
12
  # Takes in a hash where the keys are strings (the format passed in by the JSON parser)
12
13
  #
13
14
  def initialize(params)
@@ -15,9 +16,9 @@ module Sunlight
15
16
  instance_variable_set("@#{key}", value) if Legislator.instance_methods.include? key
16
17
  end
17
18
  end
18
-
19
-
20
-
19
+
20
+
21
+
21
22
  #
22
23
  # Useful for getting the exact Legislators for a given district.
23
24
  #
@@ -35,10 +36,10 @@ module Sunlight
35
36
  # rep = officials[:representative]
36
37
  #
37
38
  # Legislator.all_for(:address => "123 Fifth Ave New York, NY 10003")
38
- # Legislator.all_for(:address => "90210") # not recommended, but it'll work
39
+ # Legislator.all_for(:address => "90210") # it'll work, but use all_in_zip instead
39
40
  #
40
41
  def self.all_for(params)
41
-
42
+
42
43
  if (params[:latitude] and params[:longitude])
43
44
  Legislator.all_in_district(District.get(:latitude => params[:latitude], :longitude => params[:longitude]))
44
45
  elsif (params[:address])
@@ -46,10 +47,10 @@ module Sunlight
46
47
  else
47
48
  nil # appropriate params not found
48
49
  end
49
-
50
+
50
51
  end
51
-
52
-
52
+
53
+
53
54
  #
54
55
  # A helper method for all_for. Use that instead, unless you
55
56
  # already have the district object, then use this.
@@ -59,16 +60,16 @@ module Sunlight
59
60
  # officials = Legislator.all_in_district(District.new("NJ", "7"))
60
61
  #
61
62
  def self.all_in_district(district)
62
-
63
+
63
64
  senior_senator = Legislator.all_where(:state => district.state, :district => "Senior Seat").first
64
65
  junior_senator = Legislator.all_where(:state => district.state, :district => "Junior Seat").first
65
66
  representative = Legislator.all_where(:state => district.state, :district => district.number).first
66
-
67
+
67
68
  {:senior_senator => senior_senator, :junior_senator => junior_senator, :representative => representative}
68
-
69
+
69
70
  end
70
-
71
-
71
+
72
+
72
73
  #
73
74
  # A more general, open-ended search on Legislators than #all_for.
74
75
  # See the Sunlight API for list of conditions and values:
@@ -86,26 +87,109 @@ module Sunlight
86
87
  # dudes = Legislator.all_where(:gender => "M")
87
88
  #
88
89
  def self.all_where(params)
89
-
90
+
90
91
  url = construct_url("legislators.getList", params)
91
-
92
+
92
93
  if (result = get_json_data(url))
93
-
94
+
94
95
  legislators = []
95
96
  result["response"]["legislators"].each do |legislator|
96
97
  legislators << Legislator.new(legislator["legislator"])
97
98
  end
98
-
99
+
99
100
  legislators
100
-
101
+
101
102
  else
102
103
  nil
103
104
  end # if response.class
104
-
105
+
105
106
  end
106
107
 
108
+ #
109
+ # When you only have a zipcode (and could not get address from the user), use this.
110
+ # It specifically accounts for the case where more than one Representative's district
111
+ # is in a zip code.
112
+ #
113
+ # If possible, ask for full address for proper geocoding via Legislator#all_for, which
114
+ # gives you a nice hash.
115
+ #
116
+ # Returns:
117
+ #
118
+ # An array of Legislator objects
119
+ #
120
+ # Usage:
121
+ #
122
+ # legislators = Legislator.all_in_zipcode(90210)
123
+ #
124
+ def self.all_in_zipcode(zipcode)
125
+
126
+ url = construct_url("legislators.allForZip", {:zip => zipcode})
127
+
128
+ if (result = get_json_data(url))
129
+
130
+ legislators = []
131
+ result["response"]["legislators"].each do |legislator|
132
+ legislators << Legislator.new(legislator["legislator"])
133
+ end
134
+
135
+ legislators
136
+
137
+ else
138
+ nil
139
+ end # if response.class
140
+
141
+ end # def self.all_in_zipcode
142
+
107
143
 
144
+ #
145
+ # Fuzzy name searching. Returns possible matching Legislators
146
+ # along with a confidence score. Confidence scores below 0.8
147
+ # mean the Legislator should not be used.
148
+ #
149
+ # The API documentation explains it best:
150
+ #
151
+ # http://wiki.sunlightlabs.com/index.php/Legislators.search
152
+ #
153
+ # Returns:
154
+ #
155
+ # An array of Legislators, with the fuzzy_score set as an attribute
156
+ #
157
+ # Usage:
158
+ #
159
+ # legislators = Legislator.search_by_name("Teddy Kennedey")
160
+ # legislators = Legislator.search_by_name("Johnny Kerry", 0.9)
161
+ #
162
+ def self.search_by_name(name, threshold='0.8')
163
+
164
+ url = construct_url("legislators.search", {:name => name, :threshold => threshold})
165
+
166
+ if (response = get_json_data(url))
167
+
168
+ legislators = []
169
+ response["response"]["results"].each do |result|
170
+ if result
171
+ legislator = Legislator.new(result["result"]["legislator"])
172
+ fuzzy_score = result["result"]["score"]
173
+
174
+ if threshold.to_f < fuzzy_score.to_f
175
+ legislator.fuzzy_score = fuzzy_score.to_f
176
+ legislators << legislator
177
+ end
178
+ end
179
+ end
180
+
181
+ if legislators.empty?
182
+ nil
183
+ else
184
+ legislators
185
+ end
186
+
187
+ else
188
+ nil
189
+ end
190
+
191
+ end # def self.search_by_name
108
192
 
109
193
  end # class Legislator
110
194
 
111
- end # module Sunlight
195
+ end # module Sunlight
data/sunlight.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "sunlight"
3
- s.version = "0.1.0"
4
- s.date = "2008-08-28"
3
+ s.version = "0.2.0"
4
+ s.date = "2009-03-01"
5
5
  s.summary = "Library for accessing the Sunlight Labs API."
6
6
  s.email = "luigi.montanez@gmail.com"
7
7
  s.homepage = "http://github.com/luigi/sunlight"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: luigi-sunlight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luigi Montanez
@@ -9,11 +9,12 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-08-28 00:00:00 -07:00
12
+ date: 2009-03-01 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json
17
+ type: :runtime
17
18
  version_requirement:
18
19
  version_requirements: !ruby/object:Gem::Requirement
19
20
  requirements:
@@ -23,6 +24,7 @@ dependencies:
23
24
  version:
24
25
  - !ruby/object:Gem::Dependency
25
26
  name: ym4r
27
+ type: :runtime
26
28
  version_requirement:
27
29
  version_requirements: !ruby/object:Gem::Requirement
28
30
  requirements: