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