require 'cgi' unless defined?(CGI)
require 'uri' unless defined?(URI)
require 'net/http' unless defined?(Net) && defined?(Net::HTTP)
require 'xmlsimple' unless defined?(XmlSimple)
# = Prosper
# An interface to the Prosper.com API from
#
# Point of reference:
#
#
# Inspired by the Flickr interface from Scott Raymond
#
# Author:: Chad Mais
# Copyright:: Copyright (c) 2007 Chad Mais
# License:: MIT
#
# == Guide
# The Prosper.com API has two types of requests for accessing data: Query and Retreive.
# This wrapper supports both and has a naming convention for the respective methods.
# === Query Methods
# Uses POQL to request objects.
# - bids
# - listings
# - groups
# - members
# - loans
# === Retreive Methods
# Request objects by Key.
# - bids_by_key
# - listings_by_key
# - groups_by_key
# - members_by_key
# - loans_by_key
#
class Prosper
# API Host
DEFAULT_HOST = 'http://services.prosper.com'
# Retrieve Path
DEFAULT_RETREIVE_PATH = '/ProsperAPI.asmx/Retrieve'
# Query Path
DEFAULT_QUERY_PATH = '/ProsperAPI.asmx/Query'
# Get raw XML from last response
attr_accessor :xml
# Client hash
attr_accessor :client
# Set a client, currently Prosper.com does not require an authenticationToken
# p = Prosper.new
# or, if Prosper does require the authenticationToken
# p = Prosper.new(:authenticationToken => 'token')
def initialize(payload = {:authenticationToken => '', :host => DEFAULT_HOST, :r_path => DEFAULT_RETREIVE_PATH, :q_path => DEFAULT_QUERY_PATH, :authenticate => false})
raise "authenticationToken required" if payload[:authenticate] && payload[:authenticationToken].blank?
@host = payload[:host]
@r_path = payload[:r_path]
@q_path = payload[:q_path]
@authenticationToken = payload[:authenticationToken]
@client = payload
end
# Pass integer to get a specific ListingNumber
# listing = p.listings(54774)
# Otherwise, assumes you passed a conditionExpression.
# listings = p.listings("CreationDate >= '2007-01-01'")
#
# Possible response (0, 1, >1 results)
# - nil
# - Prosper::Listing
# - Array of Prosper::Listing
def listings(conditionExpression = '')
response = _request('Listing', if conditionExpression.kind_of?(Integer) then "ListingNumber=#{conditionExpression}" else conditionExpression end, true )
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Listing.new(object, self.client)
}
if result.size > 1 then result else result.first end # returns nil unless a listing is found
end
# Pass a spaceless string to get a member by ScreenName
# member = p.members('chado')
# Insert a space to interpret as a conditionExpression.
# members = p.members("CreationDate >= '2007-01-01'")
#
# Possible response (0, 1, >1 results)
# - nil
# - Prosper::Member
# - Array of Prosper::Member
def members(conditionExpression = '')
response = _request('Member', if _spaceless?(conditionExpression) then "ScreenName='#{conditionExpression}'" else conditionExpression end, true )
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Member.new(object, self.client)
}
if result.size > 1 then result else result.first end # returns nil unless a bid is found
end
# Pass a Listing Key to get all bids from that listing
# bids = p.bids('53193372835766846564CD6')
# Inserted a space or non-23 length string is interpreted as a conditionExpression.
# bids = p.bids("CreationDate >= '2007-01-01'")
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Bid
def bids(conditionExpression = '')
response = _request('Bid', if _is_key?(conditionExpression) then "ListingKey='#{conditionExpression}'" else conditionExpression end, true )
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Bid.new(object, self.client)
}
result #if result.size > 1 then result else result.first end # returns nil unless a bid is found
end
# Pass a spaceless string to find a group by ShortName
# group = p.groups('Promote')
# Pass a Group Key to get the group by Key
# group = p.groups('FEF83377364176536637E50')
# Inserted space or non-23 length string is interpreted as a conditionExpression.
# groups = p.groups("CreationDate >= '2007-01-01'")
#
# Possible response (0, 1, >1 results)
# - nil
# - Prosper::Group
# - Array of Prosper::Group
def groups(conditionExpression = '')
response = _request('Group', if _is_key?(conditionExpression) then "Key='#{conditionExpression}'" elsif _spaceless?(conditionExpression) then "ShortName='#{conditionExpression}'" else conditionExpression end, true )
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Group.new(object, self.client) unless /nogroup/i=~object['ShortName'].to_s
}
if result.size > 1 then result else result.first end # returns nil unless a group is found
end
# Pass a Loan Key to get a loan by Key
# loan = p.loans('30FD3365652573455326F15')
# Inserted space or non-23 length string is interpreted as a conditionExpression.
# loans = p.loans("CreationDate >= '2007-01-01'")
#
# Possible response (0, 1, >1 results)
# - nil
# - Prosper::Loan
# - Array of Prosper::Loan
def loans(conditionExpression = '')
response = _request('Loan', if _is_key?(conditionExpression) then "Key='#{conditionExpression}'" else conditionExpression end, true )
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Loan.new(object, self.client)
}
if result.size > 1 then result else result.first end # returns nil unless a group is found
end
# Pass a comma separated string of Loan Keys.
# @listings = Listing.find(:all, :limit=>50)
# listings = p.listings_by_key(@listings.collect{|l| l.key }.join(','))
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Listing
def listings_by_key(keys = '')
response = _request('Listing', keys)
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Listing.new(object, self.client)
}
result
end
# Pass a comma separated string of Member Keys.
# @members = Member.find(:all, :limit=>50)
# members = p.members_by_key(@members.collect{|m| m.key }.join(','))
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Member
def members_by_key(keys = '')
response = _request('Member', keys)
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Member.new(object, self.client)
}
result
end
# Pass a comma separated string of Bid Keys.
# @bids = Bid.find(:all, :limit=>50)
# bids = p.bids_by_key(@bids.collect{|b| b.key }.join(','))
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Bid
def bids_by_key(keys = '')
response = _request('Bid', keys)
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Bid.new(object, self.client)
}
result
end
# Pass a comma separated string of Group Keys.
# @groups = Group.find(:all, :limit=>50)
# groups = p.groups_by_key(@groups.collect{|g| g.key }.join(','))
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Group
def groups_by_key(keys = '')
response = _request('Group', keys)
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Group.new(object, self.client) unless /nogroup/i=~object['ShortName'].to_s
}
result
end
# Pass a comma separated string of Loan Keys.
# @loans = Loan.find(:all, :limit=>50)
# loans = p.loans_by_key(@loans.collect{|l| l.key }.join(','))
#
# Possible response (0, >0 results)
# - nil
# - Array of Prosper::Loan
def loans_by_key(keys = '')
response = _request('Loan', keys)
return nil if response['ProsperObjects']['ProsperObject'].nil?
result = Array.new
response['ProsperObjects']['ProsperObject'].each{|object|
result << Loan.new(object, self.client)
}
result
end
private
# spaceless and 23 in length
def _is_key?(conditionExpression)
conditionExpression.length==23 && _spaceless?(conditionExpression)
end
# has no space
def _spaceless?(conditionExpression)
(/\s/=~conditionExpression).nil?
end
# transacts with Prosper.com API.
# Raises an error if Success != true
def _request(objectType, keys_or_conditionExpression, query=nil)
url = _request_url(objectType, keys_or_conditionExpression, query)
response = XmlSimple.xml_in( self.xml = _http_get(url), {'ForceArray' => ['ProsperObject']} )
unless response['Success'].to_s == "true"
raise 'Prosper API: '+ response['Message'].to_s + " : url=#{url}"
end
response
end
# build the Prosper.com URL to request data from
def _request_url(objectType, keys_or_conditionExpression, query=nil)
fields = if objectType=='Listing' then Listing::FIELDS elsif objectType=='Member' then Member::FIELDS elsif objectType=='Bid' then Bid::FIELDS elsif objectType=='Group' then Group::FIELDS elsif objectType=='Loan' then Loan::FIELDS end
if query.nil?
url = "#{@host}#{@r_path}?objectType=#{objectType}&authenticationToken=#{@authenticationToken}&fields=#{CGI::escape(fields.to_s)}&keys=#{CGI::escape(keys_or_conditionExpression.to_s)}"
else
url = "#{@host}#{@q_path}?objectType=#{objectType}&authenticationToken=#{@authenticationToken}&fields=#{CGI::escape(fields.to_s)}&conditionExpression=#{CGI::escape(keys_or_conditionExpression.to_s)}"
end
url
end
# the actual http request
def _http_get(url)
Net::HTTP.get_response(URI.parse(url)).body.to_s
end
# = Listing
# Represents a Prosper Listing Object
#
# == Public Instance methods
# Caching occurs to reduce number of requests made to the API.
# - Pass true to have it reload
# - Be carful with method chaining your requests, they are in memory
# p.listing(54774).group.bids.map{|b| b.listing }.each{|l| l.member.group.bids } => ouch
class Listing
# Simplicity over performance. All Fields available are requested with every request.
FIELDS = 'AmountFunded,AmountRemaining,AmountRequested,BidMaximumRate,BorrowerAPR,BorrowerMaximumRate,BorrowerRate,BorrowerState,CreationDate,CreditGrade,DebtToIncomeRatio,Description,Duration,EndDate,FundingOption,GroupKey,GroupLeaderRewardRate,Key,LastModifiedDate,ListingNumber,MemberKey,PercentFunded,PrimaryImageURL,StartDate,Status,Title'
attr_reader :amount_funded, :amount_remaining, :amount_requested, :bid_maximum_rate, :borrower_apr, :borrower_maximum_rate, :borrower_rate, :borrower_state, :creation_date, :credit_grade, :debt_to_income_ratio, :description, :duration, :end_date, :funding_option, :group_key, :group_leader_reward_rate, :key, :last_modified_date, :listing_number, :member_key, :percent_funded, :primary_image_url, :start_date, :status, :title
def initialize(payload, client)
@client = client
@amount_funded = payload['AmountFunded'].to_f
@amount_remaining = payload['AmountRemaining'].to_f
@amount_requested = payload['AmountRequested'].to_f
@bid_maximum_rate = payload['BidMaximumRate'].to_f
@borrower_apr = payload['BorrowerAPR'].to_f
@borrower_maximum_rate = payload['BorrowerMaximumRate'].to_f
@borrower_rate = payload['BorrowerRate'].to_f
@borrower_state = payload['BorrowerState'].to_s
@creation_date = Time.parse(payload['CreationDate'])
@credit_grade = payload['CreditGrade'].to_i
@debt_to_income_ratio = payload['DebtToIncomeRatio'].to_f unless payload['DebtToIncomeRatio'].is_a?(Hash)
@description = payload['Description'].to_s
@duration = payload['Duration'].to_i
@end_date = Time.parse(payload['EndDate']) unless /^9999/=~payload['EndDate']
@funding_option = payload['FundingOption'].to_i
@group_key = payload['GroupKey'].to_s
@group_leader_reward_rate = payload['GroupLeaderRewardRate'].to_f
@key = payload['Key'].to_s
@last_modified_date = Time.parse(payload['LastModifiedDate'])
@listing_number = payload['ListingNumber'].to_i
@member_key = payload['MemberKey'].to_s
@percent_funded = payload['PercentFunded'].to_f
@primary_image_url = payload['PrimaryImageURL'].to_s
@start_date = Time.parse(payload['StartDate'])
@status = payload['Status'].to_i
@title = payload['Title'].to_s
end
# Get all the bids
# p.listings(54774).bids
def bids(reload=false)
@bids = nil if reload
@bids ||= _client.bids("ListingKey = '#{self.key}'").to_a
end
# The group the listing belongs to
# p.listings(54774).group
def group(reload=false)
@group = nil if reload
@group ||= _client.groups("Key = '#{self.group_key}'")
end
# The member who created the Listing
# p.listings(54774).member
def member(reload=false)
@member = nil if reload
@member ||= _client.members("Key = '#{self.member_key}'")
end
private
def _client
Prosper.new(@client)
end
end
# = Member
# Represents a Prosper Member Object
#
# == Public Instance methods
# Caching occurs to reduce number of requests made to the API.
# - Pass true to have it reload
# - Be carful with method chaining your requests, they are in memory
# p.listing(54774).group.bids.map{|b| b.listing }.each{|l| l.member.group.bids } => ouch
class Member
# Simplicity over performance. All Fields available are requested with every request.
FIELDS = 'CreationDate,Description,GroupKey,Key,Roles,ScreenName'
attr_reader :creation_date, :description, :group_key, :key, :roles, :screen_name
def initialize(payload, client)
@client = client
@creation_date = Time.parse(payload['CreationDate'])
@description = payload['Description'].to_s
@group_key = payload['GroupKey'].to_s
@key = payload['Key'].to_s
@roles = payload['Roles'].to_i
@screen_name = payload['ScreenName'].to_s
end
# All of is member's bids
# p.members('chado').bids
def bids(reload=false)
@bids = nil if reload
@bids ||= _client.bids("MemberKey = '#{self.key}'")
end
# The group to which this member belongs
# p.members('chado').group
def group(reload=false)
@group = nil if reload
@group ||= _client.groups("Key = '#{self.group_key}'")
end
# All of is member's listings
# p.members('chado').listings
def listings(reload=false)
@listings = nil if reload
@listings ||= _client.listings("MemberKey = '#{self.key}'")
end
private
def _client
Prosper.new(@client)
end
end
# = Bid
# Represents a Prosper Bid Object
#
# == Public Instance methods
# Caching occurs to reduce number of requests made to the API.
# - Pass true to have it reload
# - Be carful with method chaining your requests, they are in memory
# p.listing(54774).group.bids.map{|b| b.listing }.each{|l| l.member.group.bids } => ouch
class Bid
# Simplicity over performance. All Fields available are requested with every request.
FIELDS = 'Amount,CreationDate,Key,LastModifiedDate,ListingKey,MemberKey,MinimumRate,ParticipationAmount,Status'
attr_reader :amount, :creation_date, :key, :last_modified_date, :listing_key, :member_key, :minimum_rate, :participation_amount, :status
def initialize(payload, client)
@client = client
@amount = payload['Amount'].to_f
@creation_date = Time.parse(payload['CreationDate'])
@key = payload['Key'].to_s
@last_modified_date = Time.parse(payload['LastModifiedDate'])
@listing_key = payload['ListingKey'].to_s
@member_key = payload['MemberKey'].to_s
@minimum_rate = payload['MinimumRate'].to_f unless payload['MinimumRate'].is_a?(Hash)
@participation_amount = payload['ParticipationAmount'].to_f
@status = payload['Status'].to_i
end
# The listing to which this bid belongs
# p.bids_by_key('key,key,key').collect{|b| b.listing }
def listing(reload=false)
@listing = nil if reload
@listing ||= _client.listings("Key = '#{self.listing_key}'")
end
# The member to whom this bid belongs
# p.bids_by_key('key,key,key').collect{|b| b.member }
def member(reload=false)
@member = nil if reload
@member ||= _client.members("Key = '#{self.member_key}'")
end
private
def _client
Prosper.new(@client)
end
end
# = Group
# Represents a Prosper Group Object
#
# == Public Instance methods
# Caching occurs to reduce number of requests made to the API.
# - Pass true to have it reload
# - Be carful with method chaining your requests, they are in memory
# p.listing(54774).group.bids.map{|b| b.listing }.each{|l| l.member.group.bids } => ouch
class Group
# Simplicity over performance. All Fields available are requested with every request.
FIELDS = 'ApprovalDate,City,CreationDate,Description,GroupLeaderRewardPercentageOfBase,GroupRating,IsAcceptingNewMembers,Key,ListingReviewRequirement,MemberKey,Name,ShortDescription,ShortName,State,Status'
attr_reader :approval_date, :city, :creation_date, :description, :group_leader_reward_percentage_of_base, :group_rating, :is_accepting_new_members, :key, :listing_review_requirement, :member_key, :name, :short_description, :short_name, :state, :status
def initialize(payload, client)
@client = client
@approval_date = Time.parse(payload['ApprovalDate']) unless /^9999/=~payload['ApprovalDate']
@city = payload['City'].to_s
@creation_date = Time.parse(payload['CreationDate'])
@description = payload['Description'].to_s
@group_leader_reward_percentage_of_base = payload['GroupLeaderRewardPercentageOfBase'].to_f
@group_rating = payload['GroupRating'].to_i
@is_accepting_new_members = payload['IsAcceptingNewMembers'].to_s
@key = payload['Key'].to_s
@listing_review_requirement = payload['ListingReviewRequirement'].to_i
@member_key = payload['MemberKey'].to_s
@name = payload['Name'].to_s
@short_description = payload['ShortDescription'].to_s
@short_name = payload['ShortName'].to_s
@state = payload['State'].to_s
@status = payload['Status'].to_i
end
# The the group leader's member class
# p.groups('Promote').leader
def leader(reload=false)
@leader = nil if reload
@leader ||= _client.members("Key = '#{self.member_key}'")
end
# All of the loans which belong to this group
# p.groups('Promote').loans
def loans(reload=false)
@loans = nil if reload
@loans ||= _client.loans("GroupKey = '#{self.key}'")
end
# All of the members which belong to this group (not group leader)
# p.groups('Promote').members
def members(reload=false)
@members = nil if reload
@members ||= _client.members("GroupKey = '#{self.key}'")
end
private
def _client
Prosper.new(@client)
end
end
# = Loan
# Represents a Prosper Loan Object
#
# == Public Instance methods
# Caching occurs to reduce number of requests made to the API.
# - Pass true to have it reload
# - Be carful with method chaining your requests, they are in memory
# p.listing(54774).group.bids.map{|b| b.listing }.each{|l| l.member.group.bids } => ouch
class Loan
# Simplicity over performance. All Fields available are requested with every request.
FIELDS = 'AgeInMonths,AmountBorrowed,BorrowerRate,CreationDate,CreditGrade,DebtToIncomeRatio,GroupKey,Key,LastModifiedDate,LenderRate,LoanOutcome,MonthsPastDue,OriginationDate,ServicingStatus,Term'
attr_reader :age_in_months, :amount_borrowed, :borrower_rate, :creation_date, :credit_grade, :debt_to_income_ratio, :group_key, :key, :last_modified_date, :lender_rate, :loan_outcome, :months_past_due, :origination_date, :servicing_status, :term
def initialize(payload, client)
@client = client
@age_in_months = payload['AgeInMonths'].to_i
@amount_borrowed = payload['AmountBorrowed'].to_f
@borrower_rate = payload['BorrowerRate'].to_f
@creation_date = Time.parse(payload['CreationDate'])
@credit_grade = payload['CreditGrade'].to_i
@debt_to_income_ratio = payload['DebtToIncomeRatio'].to_f unless payload['DebtToIncomeRatio'].is_a?(Hash)
@group_key = payload['GroupKey'].to_s
@key = payload['Key'].to_s
@last_modified_date = Time.parse(payload['LastModifiedDate'])
@lender_rate = payload['LenderRate'].to_f
@loan_outcome = payload['LoanOutcome'].to_i
@months_past_due = payload['MonthsPastDue'].to_f
@origination_date = Time.parse(payload['OriginationDate'])
@servicing_status = payload['ServicingStatus'].to_i
@term = payload['Term'].to_i
end
# The group who has a member that took this loan
# p.loans('key').group
def group(reload=false)
@group = nil if reload
@group ||= _client.groups("Key = '#{self.group_key}'")
end
private
def _client
Prosper.new(@client)
end
end
end