Skip to content

Commit

Permalink
Merge pull request #4 from Jeff-R/rdocs
Browse files Browse the repository at this point in the history
Lots of new specs; updated RSpec; new rdocs; and eachability of most resources.
  • Loading branch information
OpenGovernment committed Jun 3, 2011
2 parents 7bf8b3a + 241b77f commit 801019c
Show file tree
Hide file tree
Showing 20 changed files with 704 additions and 250 deletions.
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--colour
--format nested
--backtrace
2 changes: 1 addition & 1 deletion lib/gov_kit/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module GovKit
class Configuration
attr_accessor :sunlight_apikey, :openstates_base_url, :transparency_data_base_url, :transparency_data_categories_url
attr_accessor :votesmart_apikey, :votesmart_base_url
attr_accessor :ftm_apikey, :ftm_base_url
attr_accessor :openstates_apikey, :ftm_apikey, :ftm_base_url
attr_accessor :opencongress_apikey, :opencongress_base_url
attr_accessor :technorati_apikey, :technorati_base_url
attr_accessor :google_blog_base_url, :google_news_base_url
Expand Down
245 changes: 38 additions & 207 deletions lib/gov_kit/open_states.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ class OpenStatesResource < Resource
# http://rubydoc.info/gems/httparty/0.7.4/HTTParty/ClassMethods#default_params-instance_method
default_params :output => 'json', :apikey => GovKit::configuration.sunlight_apikey
base_uri GovKit::configuration.openstates_base_url

# Do a GET query, with optional parameters.
#
# OpenStates returns a 404 error when a query
# returns nothing.
#
# So, if a query result is a resource not found error,
# we return an empty set.
def self.get_uri(uri, options={})
begin
response = get(uri, options)
result = parse(response)
rescue ResourceNotFound
return []
end
result
end

end

# Ruby module for interacting with the Open States Project API
Expand All @@ -25,147 +43,33 @@ module OpenStates
CHAMBER_UPPER = "upper"
CHAMBER_LOWER = "lower"

# The State class represents the data returned from Open States.
# The State class represents the state data returned from Open States.
#
# From the Open States documentation, at http://openstates.sunlightlabs.com/api/metadata/,
# the fields returned are:
# For details about fields returned, see the Open States documentation, at
# http://openstates.sunlightlabs.com/api/metadata/,
#
# name
# The name of the state
# abbreviation
# The two-letter abbreviation of the state
# legislature_name
# The name of the state legislature
# upper_chamber_name
# The name of the 'upper' chamber of the state legislature (if applicable)
# lower_chamber_name
# The name of the 'lower' chamber of the state legislature (if applicable)
# upper_chamber_term
# The length, in years, of a term for members of the 'upper' chamber (if applicable)
# lower_chamber_term
# The length, in years, of a term for members of the 'lower' chamber (if applicable)
# upper_chamber_title
# The title used to refer to members of the 'upper' chamber (if applicable)
# lower_chamber_title
# The title used to refer to members of the 'lower' chamber (if applicable)
# latest_dump_url
# URL pointing to a download of all data for the state
# latest_dump_date
# datestamp of the file at latest_dump_url
# terms
#
# A list of terms that we have data available for. Each session will be an object with the following fields:
#
# * start_year: The year in which this session began.
# * end_year: The year in which this session ended.
# * name: The name of this session.
# * sessions: List of sessions that took place inside the given term.
#
# session_details
#
# Optional extra details about sessions.
#
# If present will be a dictionary with keys corresponding to sessions and values are dictionaries of extra metadata about a session.
#
# Fields that may be present include start_date and end_date, as well as type indicating whether the session was a normally scheduled or special session.
#
class State < OpenStatesResource
def self.find_by_abbreviation(abbreviation)
response = get("/metadata/#{abbreviation}/")
parse(response)
get_uri("/metadata/#{abbreviation}/")
end
end

# The Bill class represents the data returned from Open States.
# The Bill class represents the bill data returned from Open States.
#
# From the Open States documentation, at http://openstates.sunlightlabs.com/api/bills/,
# the fields returned are:
# For details about fields returned, see the Open States documentation, at
# http://openstates.sunlightlabs.com/api/bills/,
#
# title
# The title given to the bill by the state legislature.
# state
# The 2-letter abbreviation of the state this bill is from (e.g. ny).
# session
# The session this bill was introduced in.
# chamber
# The chamber this bill was introduced in (e.g. 'upper', 'lower')
# bill_id
# The identifier given to this bill by the state legislature (e.g. 'AB6667')
# type
# Bill type (see bill categorization).
# alternate_titles
# A list of alternate titles that this bill is/was known by, if available.
# updated_at
# Timestamp representing when bill was last updated in our system.
# actions
#
# A list of legislative actions performed on this bill. Each action will be an object with at least the following fields:
#
# * date: The date/time the action was performed
# * actor: The chamber, person, committee, etc. responsible for this action
# * action: A textual description of the action performed
# * type: A normalized type for the action, see see action categorization.
#
# sponsors
#
# A list of sponsors of this bill. Each sponsor will be an object with at least the following fields:
#
# * leg_id: An Open State Project legislator ID.
# * full_name: The name of the sponsor
# * type: The type of sponsorship (state specific, examples include 'Primary Sponsor', 'Co-Sponsor')
#
# votes
#
# A list of votes relating to this bill. Each vote will be an object with at least the following fields:
#
# * date: The date/time the vote was taken
# * chamber: The chamber that the vote was taken in
# * motion: The motion being voted on
# * yes_count, no_count, other_count: The number of 'yes', 'no', and other votes
# * yes_votes, no_votes, other_votes: The legislators voting 'yes', 'no', and other
# * passed: Whether or not the vote passed
# * type: The normalized type for the vote. See vote categorization).
#
# versions
#
# A list of versions of the text of this bill. Each version will be an object with at least the following fields:
#
# * url: The URL for an official source of this version of the bill text
# * name: A name for this version of the bill text
#
# documents
#
# A list of documents related to this bill. Each document will be an object with at least the following fields:
#
# * url: The URL for an official source of this document
# * name: A name for this document (eg. 'Fiscal Statement', 'Education Committee Report')
#
# sources
#
# List of sources that this data was collected from.
#
# * url: URL of the source
# * retrieved: time at which the source was last retrieved
#
# Note
#
# actions, sponsors, votes, versions, documents, alternate_title and sources are not returned via the search API.
#
# Note
#
# Keep in mind that these documented fields may be a subset of the fields provided for a given state.
class Bill < OpenStatesResource
# http://openstates.sunlightlabs.com/api/v1/bills/ca/20092010/AB 667/
def self.find(state_abbrev, session, bill_id, chamber = '')
escaped_bill_id = bill_id.gsub(/ /, '%20')
escaped_session = session.gsub(/ /, '%20')
response = get("/bills/#{state_abbrev.downcase}/#{escaped_session}/#{chamber.blank? ? '' : chamber + '/'}#{escaped_bill_id}/")
parse(response)

get_uri("/bills/#{state_abbrev.downcase}/#{escaped_session}/#{chamber.blank? ? '' : chamber + '/'}#{escaped_bill_id}/")
end

def self.search(query, options = {})
response = get('/bills/', :query => {:q => query}.merge(options))
parse(response)
get_uri('/bills/', :query => {:q => query}.merge(options))
end

def self.latest(updated_since, ops = {})
Expand All @@ -174,106 +78,33 @@ def self.latest(updated_since, ops = {})
end
end

# The Legislator class represents the data returned from Open States.
# The Legislator class represents the legislator data returned from Open States.
#
# From the Open States documentation, at http://openstates.sunlightlabs.com/api/legislators/,
# the fields returned are:
# For details about fields returned, see the Open States documentation, at
# http://openstates.sunlightlabs.com/api/legislators/,
#
# * leg_id: A permanent, unique identifier for this legislator within the Open State Project system.
# * full_name
# * first_name
# * last_name
# * middle_name
# * suffixes
# * photo_url
# * active - Boolean indicating whether or not this legislator is currently serving.
# * state, chamber, district, party (only present if the legislator is currently serving)
# * roles: A list of objects representing roles this legislator currently holds. Each role will contain at least the type and term roles:
# o type the type of role - e.g. "member", "committee member", "Lt. Governor"
# o term the term the role was held during
# o chamber
# o district
# o party
# o committee
# o term
# * old_roles: A dictionary mapping term names for past terms to lists of roles held. (Sub-objects have same fields as roles.)
# * sources List of sources that this data was collected from.
# o url: URL of the source
# o retrieved: time at which the source was last retrieved
# Note
#
# sources, roles and old_roles are not included in the legislator search response.
#
# Note
#
# Keep in mind that these documented fields may be a subset of the fields provided for a given state.
class Legislator < OpenStatesResource
def self.find(legislator_id)
response = get("/legislators/#{legislator_id}/")
parse(response)
get_uri("/legislators/#{legislator_id}/")
end

def self.search(options = {})
response = get('/legislators/', :query => options)
parse(response)
get_uri('/legislators/', :query => options)
end
end

# The Committeer class represents the data returned from Open States.
# The Committee class represents the committee data returned from Open States.
#
# From the Open States documentation, at http://openstates.sunlightlabs.com/api/committees/,
# the fields returned are:
# For details about fields returned, see the Open States documentation, at
# http://openstates.sunlightlabs.com/api/committees/,
#
# Committee methods return objects with the following fields:
#
# id
# Open State Project Committee ID.
# chamber
# Associated chamber (upper, lower, or joint).
# state
# State abbreviation (eg. ny).
# committee
# Name of committee.
# subcommittee
# Name of subcommittee (null if record describes a top level committee).
# parent_id
# For subcommittees, the committee ID of its parent. null otherwise.
# members
#
# Listing of the current committee membership.
#
# legislator
# Name of legislator (as captured from source).
# role
# Role of this member on the committee (usually 'member' but may indicate charimanship or other special status)
# leg_id
# Legislator's Open State Project ID
#
# sources
#
# List of sources that this data was collected from.
#
# url
# URL of the source
# retrieved
# time at which the source was last retrieved
#
# Note
#
# members and sources are not included in the committee search API results
#
# Note
#
# Keep in mind that these documented fields may be a subset of the fields provided for a given state. (See extra fields.)
class Committee < OpenStatesResource
def self.find(committee_id)
response = get("/committees/#{committee_id}/")
parse(response)
get_uri("/committees/#{committee_id}/")
end

def self.search(options = {})
response = get('/committees/', :query => options)
parse(response)
get_uri('/committees/', :query => options)
end
end

Expand Down
39 changes: 31 additions & 8 deletions lib/gov_kit/resource.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
module GovKit

# Parent class of resources returned by GovKit.

# This is the parent class to the classes that wrap
# the data returned to govkit.
#
# The subclasses are responsible for fetching the data as json from
# different web services; Resource will then parse the json,
# converting returned fields to instance methods.
#
# Initialize a Resource with a hash of attributes, or an array of hashes.
# For each attribute, add a getter and setter to this instance.
# So if
# res = Resource.new { "aaa" => "111", "bbb" => "222", "ccc" => "333" }
# then
# res.aaa == "111"
# res.bbb == "222"
# res.ccc == "333"
#
# Includes HTTParty, which provides convenience methods like get().
#
# See http://rdoc.info/github/jnunemaker/httparty/master/HTTParty/ClassMethods
class Resource
include HTTParty
format :json
Expand Down Expand Up @@ -32,12 +49,13 @@ def to_md5
# you'll need to handle that in the subclass.
#
def self.parse(response)
raise ResourceNotFound, "Resource not found" unless !response.blank?

if response.class == HTTParty::Response
case response.response
when Net::HTTPNotFound
raise ResourceNotFound, "404 Not Found"
when Net::HTTPGone
raise ResourceNotFound, "404 Not Found"
when Net::HTTPUnauthorized
raise NotAuthorized, "401 Not Authorized; have you set up your API key?"
when Net::HTTPServerError
Expand All @@ -47,6 +65,8 @@ def self.parse(response)
end
end

return [] unless !response.blank?

instantiate(response)
end

Expand All @@ -55,24 +75,27 @@ def self.parse(response)
# +record+ is a hash of values returned by a service,
# or an array of hashes.
#
# If +record+ is a hash, return a single GovKit::Resource.
# If +record+ is a hash, return an array containing a single GovKit::Resource.
# If it is an array, return an array of GovKit::Resources.
#
def self.instantiate(record)
if record.is_a?(Array)
instantiate_collection(record)
else
new(record)
[new(record)]
end
end

def self.instantiate_collection(collection)
collection.collect! { |record| new(record) }
end

# Fills the @attributes hash with new resources generated from the
# members of the +attributes+ hash,
#
# Given a hash of attributes, assign it to the @attributes member,
# then for each attribute, create or set a pair of member accessors with the name
# of the attribute's key.
# If the value of the attribute is itself an array or a hash,
# then create a new class with the (singularized) key as a name, and with a parent class of Resource,
# and initialize it with the hash.
def unload(attributes)
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)

Expand Down
Loading

0 comments on commit 801019c

Please sign in to comment.