aasmith-yodlee 0.0.1.20090227002742
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/History.txt +6 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +110 -0
- data/Rakefile +29 -0
- data/lib/yodlee.rb +26 -0
- data/lib/yodlee/account.rb +26 -0
- data/lib/yodlee/connection.rb +296 -0
- data/lib/yodlee/credentials.rb +41 -0
- data/lib/yodlee/exceptions.rb +6 -0
- data/lib/yodlee/monkeypatches.rb +9 -0
- data/lib/yodlee/version.rb +3 -0
- data/test/test_yodlee.rb +152 -0
- data/yodlee.gemspec +44 -0
- metadata +108 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.rdoc
|
4
|
+
Rakefile
|
5
|
+
lib/yodlee.rb
|
6
|
+
lib/yodlee/account.rb
|
7
|
+
lib/yodlee/connection.rb
|
8
|
+
lib/yodlee/credentials.rb
|
9
|
+
lib/yodlee/exceptions.rb
|
10
|
+
lib/yodlee/monkeypatches.rb
|
11
|
+
lib/yodlee/version.rb
|
12
|
+
test/test_yodlee.rb
|
13
|
+
yodlee.gemspec
|
data/README.rdoc
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
= yodlee
|
2
|
+
|
3
|
+
http://github.com/aasmith/yodlee
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Fetches accounts and their transaction details from the Yodlee
|
8
|
+
MoneyCenter (https://moneycenter.yodlee.com).
|
9
|
+
|
10
|
+
== NOTES:
|
11
|
+
|
12
|
+
* The strings returned by Yodlee::Account#{last_updated,next_update}
|
13
|
+
can be parsed with Chronic (http://chronic.rubyforge.org), if a
|
14
|
+
timestamp is needed.
|
15
|
+
|
16
|
+
* Raises exceptions when exceptional things happen. These are scenarios
|
17
|
+
where the connection probably needs to be re-instantiated with the
|
18
|
+
correct details after prompting the user or some external source for
|
19
|
+
more complete login or account details.
|
20
|
+
|
21
|
+
== BUGS / TODO:
|
22
|
+
|
23
|
+
* Does not handle lists containing bill pay accounts
|
24
|
+
|
25
|
+
* Does not handle cases where the session has timed out. To avoid this,
|
26
|
+
use the instantiated objects in short durations. Don't leave them
|
27
|
+
hanging around.
|
28
|
+
|
29
|
+
* Add support for investment holdings
|
30
|
+
|
31
|
+
* Update account transactions / polling
|
32
|
+
|
33
|
+
== SYNOPSIS:
|
34
|
+
|
35
|
+
require 'rubygems'
|
36
|
+
require 'yodlee'
|
37
|
+
|
38
|
+
# Create some credentials to login with.
|
39
|
+
cred = Yodlee::Credentials.new
|
40
|
+
cred.username = 'bob'
|
41
|
+
cred.password = 'weak'
|
42
|
+
|
43
|
+
# The word the remote system stores and shows back to you to prove
|
44
|
+
# they really are Yodlee.
|
45
|
+
cred.expectation = 'roflcopter'
|
46
|
+
|
47
|
+
# An array of questions and answers. Yodlee expects you to answer
|
48
|
+
# three of thirty defined in Yodlee::Credentials::QUESTIONS.
|
49
|
+
cred.answers = [[Yodlee::Credentials::QUESTIONS[1], "The Queen"], [...]]
|
50
|
+
|
51
|
+
# That's enough credentials. Create a connection.
|
52
|
+
conn = Yodlee::Connection.new(cred)
|
53
|
+
|
54
|
+
# Connect, and get an account list.
|
55
|
+
conn.accounts.each do |account|
|
56
|
+
puts account.institute_name, account.name, account.current_balance
|
57
|
+
|
58
|
+
# grab a handy list of transactions. Parseable as CSV.
|
59
|
+
puts account.simple_transactions
|
60
|
+
|
61
|
+
# Next line needs johnson.
|
62
|
+
# p account.transactions
|
63
|
+
|
64
|
+
# take a look in account.account_info for a hash of useful stuff.
|
65
|
+
# available keys vary by account type and institute.
|
66
|
+
end
|
67
|
+
|
68
|
+
# Should look something like this:
|
69
|
+
|
70
|
+
First Bank of Excess
|
71
|
+
Checking
|
72
|
+
$123.45
|
73
|
+
[...some csv...]
|
74
|
+
First Bank of Mattress
|
75
|
+
Savings
|
76
|
+
$1,234.56
|
77
|
+
[...more csv!...]
|
78
|
+
|
79
|
+
== REQUIREMENTS:
|
80
|
+
|
81
|
+
mechanize, nokogiri.
|
82
|
+
|
83
|
+
Optional dependency on johnson (http://github.com/jbarnette/johnson)
|
84
|
+
|
85
|
+
== INSTALL:
|
86
|
+
|
87
|
+
sudo gem install aasmith-yodlee --source http://gems.github.com
|
88
|
+
|
89
|
+
== LICENSE:
|
90
|
+
|
91
|
+
Copyright (c) 2009 Andrew A. Smith <andy@tinnedfruit.org>
|
92
|
+
|
93
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
94
|
+
a copy of this software and associated documentation files (the
|
95
|
+
'Software'), to deal in the Software without restriction, including
|
96
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
97
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
98
|
+
permit persons to whom the Software is furnished to do so, subject to
|
99
|
+
the following conditions:
|
100
|
+
|
101
|
+
The above copyright notice and this permission notice shall be
|
102
|
+
included in all copies or substantial portions of the Software.
|
103
|
+
|
104
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
105
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
106
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
107
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
108
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
109
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
110
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require './lib/yodlee/version.rb'
|
4
|
+
|
5
|
+
HOE = Hoe.new('yodlee', Yodlee::VERSION) do |p|
|
6
|
+
p.developer 'Andrew A. Smith', 'andy@tinnedfruit.org'
|
7
|
+
p.readme_file = "README.rdoc"
|
8
|
+
p.extra_rdoc_files = [p.readme_file]
|
9
|
+
p.extra_deps = %w(mechanize nokogiri)
|
10
|
+
p.extra_dev_deps = %w(flexmock)
|
11
|
+
p.summary = "Fetches financial data from Yodlee MoneyCenter."
|
12
|
+
end
|
13
|
+
|
14
|
+
missing = (HOE.extra_deps + HOE.extra_dev_deps).
|
15
|
+
reject { |d| Gem.available? *d }
|
16
|
+
|
17
|
+
unless missing.empty?
|
18
|
+
puts "You may be missing gems. Try:\ngem install #{missing.join(' ')}"
|
19
|
+
end
|
20
|
+
|
21
|
+
namespace :gem do
|
22
|
+
desc 'Generate a gem spec'
|
23
|
+
task :spec do
|
24
|
+
File.open("#{HOE.name}.gemspec", 'w') do |f|
|
25
|
+
HOE.spec.version = "#{HOE.version}.#{Time.now.strftime("%Y%m%d%H%M%S")}"
|
26
|
+
f.write(HOE.spec.to_ruby)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/yodlee.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mechanize'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'enumerator'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'johnson'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'yodlee/account'
|
13
|
+
require 'yodlee/connection'
|
14
|
+
require 'yodlee/credentials'
|
15
|
+
require 'yodlee/exceptions'
|
16
|
+
require 'yodlee/monkeypatches'
|
17
|
+
require 'yodlee/version'
|
18
|
+
|
19
|
+
module Yodlee
|
20
|
+
class Transaction < Struct.new(
|
21
|
+
:account_name, :account_id,
|
22
|
+
:currency, :amount, :date,
|
23
|
+
:fit_id, :status, :description
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Yodlee
|
2
|
+
class Account
|
3
|
+
attr_accessor :id, :name, :institute_id, :institute_name, :account_info
|
4
|
+
|
5
|
+
def initialize(connection)
|
6
|
+
@connection = connection
|
7
|
+
@account_info, @transactions = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
[:simple_transactions, :current_balance, :account_info, :last_updated, :next_update].each do |m|
|
11
|
+
define_method m do
|
12
|
+
@account_info = @connection.account_info(self) unless @account_info
|
13
|
+
m == :account_info ? @account_info : @account_info[m]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def transactions
|
18
|
+
return @transactions if @transactions
|
19
|
+
@transactions = @connection.transactions(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{@institute_name} - #{@name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
module Yodlee
|
2
|
+
class Connection
|
3
|
+
def initialize(credentials, logger = nil)
|
4
|
+
@credentials = credentials
|
5
|
+
@logger = logger
|
6
|
+
|
7
|
+
@connected = false
|
8
|
+
@accounts = nil
|
9
|
+
|
10
|
+
@agent = WWW::Mechanize.new
|
11
|
+
@agent.user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5'
|
12
|
+
|
13
|
+
@uris = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def accounts
|
17
|
+
return @accounts if @accounts
|
18
|
+
|
19
|
+
handle_connection!
|
20
|
+
|
21
|
+
page = @agent.get(@uris[:accounts])
|
22
|
+
doc = Nokogiri::HTML.parse(page.body)
|
23
|
+
|
24
|
+
@accounts = doc.search(".acctbean a").map{|e|
|
25
|
+
acct = Account.new(self)
|
26
|
+
|
27
|
+
e['href'].scan(/(\w+Id)=(\d+)/).each do |k,v|
|
28
|
+
case k
|
29
|
+
when /itemAccountId/ then acct.id = v
|
30
|
+
when /itemId/ then acct.institute_id = v
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
acct.institute_name = e.at('strong').text
|
35
|
+
acct.name = e.children.last.text.sub(/^\s*-\s*/,'')
|
36
|
+
acct
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def account_info(acct)
|
41
|
+
page = @agent.get(@uris[:accounts])
|
42
|
+
|
43
|
+
link = page.links.detect{|lnk| lnk.href =~ /itemAccountId=#{acct.id}/ } or raise AccountNotFound, "Could not find account in list"
|
44
|
+
link.href << "&dateRangeId=-1"
|
45
|
+
page = link.click
|
46
|
+
|
47
|
+
doc = Nokogiri::HTML.parse(page.body)
|
48
|
+
|
49
|
+
last_upd, next_upd = doc.at(".accountlinks").text.scan(/Last updated (.*?)\s*\(next scheduled update (.*)\)/).flatten
|
50
|
+
|
51
|
+
# Regular accounts have a heading + div, investments have heading + table
|
52
|
+
regular_acct = doc.at("h2[contains('Account Overview')] + div")
|
53
|
+
|
54
|
+
account_info = regular_acct ? regular_account_info(doc) : investment_account_info(doc)
|
55
|
+
account_info[:next_update] = next_upd
|
56
|
+
account_info[:last_updated] = last_upd
|
57
|
+
|
58
|
+
csv_page = page.form_with(:name => 'rep').submit
|
59
|
+
account_info[:simple_transactions] = csv_page.response['content-type'] =~ /csv/ ? csv_page.body : []
|
60
|
+
|
61
|
+
account_info
|
62
|
+
end
|
63
|
+
|
64
|
+
def regular_account_info(doc)
|
65
|
+
info_block = doc.at("h2[contains('Account Overview')] + div")
|
66
|
+
|
67
|
+
Hash[*info_block.search("dt").
|
68
|
+
zip(info_block.search("dt+dd")).
|
69
|
+
map{|a,b|[a.text.gsub(/\W/,'').underscore.to_sym, b.text]}.
|
70
|
+
flatten
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
def investment_account_info(doc)
|
75
|
+
account_info = {}
|
76
|
+
account_info[:holdings] = [] # TODO
|
77
|
+
account_info[:current_balance] =
|
78
|
+
doc.search("h2[contains('Account Overview')] + table tr[last()] td[last()]").text
|
79
|
+
account_info
|
80
|
+
end
|
81
|
+
|
82
|
+
# This method returns each transaction as an object, based on the underyling javascript
|
83
|
+
# structures used to build the transactions as displayed in the Yodlee UI. These objects
|
84
|
+
# are able to access more information than the CSV Yodlee provides, such as the finanical
|
85
|
+
# institute's transaction id, useful for tracking duplicates.
|
86
|
+
#
|
87
|
+
# Calling this method requires Johnson to be installed, otherwise an exception is raised.
|
88
|
+
def transactions(acct)
|
89
|
+
unless Object.const_defined? "Johnson"
|
90
|
+
raise "Johnson not found. Install the johnson gem, or use simple_transactions instead."
|
91
|
+
end
|
92
|
+
|
93
|
+
post_headers = {
|
94
|
+
"c0-scriptName"=>"TxnService",
|
95
|
+
"c0-methodName"=>"searchTransactions",
|
96
|
+
"c0-id"=>"#{rand(5000)}_#{Time.now.to_i}#{Time.now.usec / 1000}}",
|
97
|
+
"c0-e1"=>"number:10000004",
|
98
|
+
"c0-e2"=>"string:17CBE222A42161A3FF450E47CF4C1A00",
|
99
|
+
"c0-e3"=>"null:null",
|
100
|
+
"c0-e4"=>"number:1",
|
101
|
+
"c0-e5"=>"boolean:false",
|
102
|
+
"c0-e6"=>"string:#{acct.id}",
|
103
|
+
"c0-e7"=>"string:-1",
|
104
|
+
"c0-e8"=>"null:null",
|
105
|
+
"c0-e9"=>"string:-1",
|
106
|
+
"c0-e10"=>"null:null",
|
107
|
+
"c0-e11"=>"null:null",
|
108
|
+
"c0-e12"=>"null:null",
|
109
|
+
"c0-e13"=>"string:-1",
|
110
|
+
"c0-e14"=>"null:null",
|
111
|
+
"c0-e15"=>"number:-1",
|
112
|
+
"c0-e16"=>"number:-1",
|
113
|
+
"c0-e17"=>"boolean:false",
|
114
|
+
"c0-e18"=>"Boolean:false",
|
115
|
+
"c0-e19"=>"boolean:false",
|
116
|
+
"c0-e20"=>"Boolean:false",
|
117
|
+
"c0-e21"=>"string:",
|
118
|
+
"c0-e22"=>"string:",
|
119
|
+
"c0-e23"=>"Boolean:false",
|
120
|
+
"c0-e24"=>"Boolean:false",
|
121
|
+
"c0-e25"=>"boolean:false",
|
122
|
+
"c0-e26"=>"Number:0",
|
123
|
+
"c0-e27"=>"string:0",
|
124
|
+
"c0-e28"=>"null:null",
|
125
|
+
"c0-e29"=>"null:null",
|
126
|
+
"c0-e30"=>"string:allTransactions",
|
127
|
+
"c0-e31"=>"string:InProgressAndCleared",
|
128
|
+
"c0-e32"=>"number:999",
|
129
|
+
"c0-e33"=>"string:",
|
130
|
+
"c0-e34"=>"null:null",
|
131
|
+
"c0-e35"=>"null:null",
|
132
|
+
"c0-e36"=>"string:",
|
133
|
+
"c0-e37"=>"null:null",
|
134
|
+
"c0-e38"=>"string:ALL",
|
135
|
+
"c0-e39"=>"string:false",
|
136
|
+
"c0-e40"=>"string:0.0",
|
137
|
+
"c0-e41"=>"string:0.0",
|
138
|
+
|
139
|
+
"c0-param0"=>"Object:{
|
140
|
+
cobrandId:reference:c0-e1,
|
141
|
+
applicationId:reference:c0-e2,
|
142
|
+
csit:reference:c0-e3,
|
143
|
+
loggingLevel:reference:c0-e4,
|
144
|
+
loggingEnabled:reference:c0-e5}",
|
145
|
+
|
146
|
+
"c0-param1"=>"Object:{
|
147
|
+
itemAccountId:reference:c0-e6,
|
148
|
+
categoryId:reference:c0-e7,
|
149
|
+
categoryLevelId:reference:c0-e8,
|
150
|
+
dateRangeId:reference:c0-e9,
|
151
|
+
fromDate:reference:c0-e10,
|
152
|
+
toDate:reference:c0-e11,
|
153
|
+
groupBy:reference:c0-e12,
|
154
|
+
groupAccountId:reference:c0-e13,
|
155
|
+
filterTranasctions:reference:c0-e14,
|
156
|
+
transactionTypeId:reference:c0-e15,
|
157
|
+
transactionStatusId:reference:c0-e16,
|
158
|
+
ignorePendingTransactions:reference:c0-e17,
|
159
|
+
includeBusinessExpense:reference:c0-e18,
|
160
|
+
includeTransfer:reference:c0-e19,
|
161
|
+
includeReimbursableExpense:refrence:c0-e20,
|
162
|
+
fromDate1:reference:c0-e21,
|
163
|
+
toDate1:reference:c0-e22,
|
164
|
+
includeMedicalExpense:reference:c0-e23,
|
165
|
+
includeTaxDeductible:reference:c0-e24,
|
166
|
+
includePersonalExpense:reference:c0-e25,
|
167
|
+
transactionAmount:reference:c0-e26,
|
168
|
+
transactionAmountRange:reference:c0-e27,
|
169
|
+
billStatementRange:reference:c0-e28,
|
170
|
+
criteria:reference:c0-e29,
|
171
|
+
module:reference:c0-e30,
|
172
|
+
transactionType:reference:c0-e31,
|
173
|
+
pageSize:reference:c0-e32,
|
174
|
+
sharedMemId:reference:c0-e33,
|
175
|
+
overRideDateRangeId:reference:c0-e34,
|
176
|
+
overRideContainer:referencec0-e35,
|
177
|
+
searchString:reference:c0-e36,
|
178
|
+
pageId:reference:c0-e37,
|
179
|
+
splitTypeTransaction:reference:c0-e38,
|
180
|
+
isAvailableBalance:reference:c0-e39,
|
181
|
+
currentBalance:reference:c0-e40,
|
182
|
+
availableBalance:reference:c0-e41}",
|
183
|
+
|
184
|
+
"c0-param2"=>"boolean:false",
|
185
|
+
|
186
|
+
"callCount"=>"1",
|
187
|
+
"xml"=>"true",
|
188
|
+
}
|
189
|
+
page = @agent.post(
|
190
|
+
'https://moneycenter.yodlee.com/moneycenter/dwr/exec/TxnService.searchTransactions.dwr',
|
191
|
+
post_headers
|
192
|
+
)
|
193
|
+
|
194
|
+
j = Johnson::Runtime.new
|
195
|
+
|
196
|
+
# Remove the last line (a call to DWREngine), and execute
|
197
|
+
j.evaluate page.body.strip.sub(/\n[^\n]+\Z/m, '')
|
198
|
+
|
199
|
+
if x = j['s0']
|
200
|
+
transactions = x.transactions.map do |e|
|
201
|
+
transaction = Yodlee::Transaction.new
|
202
|
+
transaction.account_name = e.accountName
|
203
|
+
transaction.currency = e.amount.cobCurrencyCode
|
204
|
+
transaction.amount = e.amount.cobPreciseAmount
|
205
|
+
transaction.description = e.description
|
206
|
+
transaction.account_id = e.itemAccountId
|
207
|
+
transaction.fit_id = e.transactionId
|
208
|
+
transaction.status = e['type']['type']
|
209
|
+
|
210
|
+
# Re-parse in order to get a real Time, not a Johnson::SpiderMonkey::RubyLandProxy.
|
211
|
+
transaction.date = Time.parse(e.date.to_s)
|
212
|
+
transaction
|
213
|
+
end
|
214
|
+
|
215
|
+
return transactions
|
216
|
+
end
|
217
|
+
|
218
|
+
return []
|
219
|
+
end
|
220
|
+
|
221
|
+
def handle_connection!
|
222
|
+
login unless connected?
|
223
|
+
end
|
224
|
+
|
225
|
+
def login
|
226
|
+
@connected = false
|
227
|
+
page = nil
|
228
|
+
|
229
|
+
%w(provide_username answer_question check_expectation provide_password).each do |m|
|
230
|
+
page = send(*[m, page].compact)
|
231
|
+
end
|
232
|
+
|
233
|
+
@connected = true
|
234
|
+
end
|
235
|
+
|
236
|
+
def connected?
|
237
|
+
@connected
|
238
|
+
end
|
239
|
+
|
240
|
+
def log(level, msg)
|
241
|
+
@logger.__send__(level, question) if @logger
|
242
|
+
end
|
243
|
+
|
244
|
+
# login scrapers
|
245
|
+
|
246
|
+
def provide_username
|
247
|
+
p = @agent.get 'https://moneycenter.yodlee.com/'
|
248
|
+
f = p.form_with(:name => 'loginForm')
|
249
|
+
f['loginName'] = @credentials.username
|
250
|
+
@agent.submit(f)
|
251
|
+
end
|
252
|
+
|
253
|
+
def answer_question(page)
|
254
|
+
question = Nokogiri::HTML.parse(page.body).at("label[@for=answer]").text
|
255
|
+
log(:debug, question)
|
256
|
+
|
257
|
+
begin
|
258
|
+
answer = @credentials.answers.detect{|q, a| question =~ /^#{Regexp.escape(q)}/}.last
|
259
|
+
rescue
|
260
|
+
raise NoAnswerForQuestion, "No answer found for #{question}"
|
261
|
+
end
|
262
|
+
|
263
|
+
f = page.form_with(:name => 'loginForm')
|
264
|
+
f['answer'] = answer
|
265
|
+
|
266
|
+
@agent.submit(f)
|
267
|
+
end
|
268
|
+
|
269
|
+
def check_expectation(page)
|
270
|
+
d = Nokogiri::HTML.parse(page.body)
|
271
|
+
node = d.at("dl > dt[contains('Secret Phrase')] + dd .caption")
|
272
|
+
|
273
|
+
if node
|
274
|
+
if @credentials.expectation == node.previous.text.strip
|
275
|
+
return page
|
276
|
+
else
|
277
|
+
raise ExpectationMismatch, "Expectation found, but was incorrect"
|
278
|
+
end
|
279
|
+
else
|
280
|
+
raise ExpectationNotFound, "Didn't find expectation"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def provide_password(page)
|
285
|
+
f = page.form_with(:name => 'loginForm')
|
286
|
+
f['password'] = @credentials.password
|
287
|
+
page = @agent.submit(f)
|
288
|
+
|
289
|
+
# ack javascript disabled
|
290
|
+
f = page.form_with(:name => 'updateForm')
|
291
|
+
page = @agent.submit(f)
|
292
|
+
|
293
|
+
@uris[:accounts] = page.uri.to_s << "&filter_id=-1"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Yodlee
|
2
|
+
class Credentials
|
3
|
+
QUESTIONS = <<-EOS.split(/\n\s*\n?/).map{|e|e.strip}
|
4
|
+
In what city was your high school? (full name of city only)
|
5
|
+
What is your maternal grandmother's first name?
|
6
|
+
What is your father's middle name?
|
7
|
+
What was the name of your High School?
|
8
|
+
What is the name of the first company you worked for?
|
9
|
+
What is the first name of the maid of honor at your wedding?
|
10
|
+
What is the first name of your oldest nephew?
|
11
|
+
What is your maternal grandfather's first name?
|
12
|
+
What is your best friend's first name?
|
13
|
+
In what city were you married? (Enter full name of city)
|
14
|
+
|
15
|
+
What is the first name of the best man at your wedding?
|
16
|
+
What was your high school mascot?
|
17
|
+
What was the first name of your first manager?
|
18
|
+
In what city was your father born? (Enter full name of city only)
|
19
|
+
What was the name of your first girlfriend/boyfriend?
|
20
|
+
What was the name of your first pet?
|
21
|
+
What is the first name of your oldest niece?
|
22
|
+
What is your paternal grandmother's first name?
|
23
|
+
In what city is your vacation home? (Enter full name of city only)
|
24
|
+
What was the nickname of your grandfather?
|
25
|
+
|
26
|
+
In what city was your mother born? (Enter full name of city only)
|
27
|
+
What is your mother's middle name?
|
28
|
+
In what city were you born? (Enter full name of city only)
|
29
|
+
Where did you meet your spouse for the first time? (Enter full name of city only)
|
30
|
+
What was your favorite restaurant in college?
|
31
|
+
What is your paternal grandfather's first name?
|
32
|
+
What was the name of your junior high school? (Enter only \"Riverdale\" for Riverdale Junior High School)
|
33
|
+
What was the last name of your favorite teacher in final year of high school?
|
34
|
+
What was the name of the town your grandmother lived in? (Enter full name of town only)
|
35
|
+
What street did your best friend in high school live on? (Enter full name of street only)
|
36
|
+
EOS
|
37
|
+
|
38
|
+
# answers = [[QUESTIONS[n], "answer"], ... ]
|
39
|
+
attr_accessor :username, :password, :answers, :expectation
|
40
|
+
end
|
41
|
+
end
|
data/test/test_yodlee.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
%w(../lib).each do |path|
|
2
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), path)))
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'flexmock/test_unit'
|
8
|
+
|
9
|
+
require 'yodlee'
|
10
|
+
|
11
|
+
class TestYodlee < Test::Unit::TestCase
|
12
|
+
def setup
|
13
|
+
@cred = Yodlee::Credentials.new
|
14
|
+
@cred.username = "bob"
|
15
|
+
@cred.password = "foo"
|
16
|
+
@cred.expectation = "bar"
|
17
|
+
@cred.answers = [
|
18
|
+
[Yodlee::Credentials::QUESTIONS[0], "aa"],
|
19
|
+
[Yodlee::Credentials::QUESTIONS[10], "ab"],
|
20
|
+
[Yodlee::Credentials::QUESTIONS[20], "ac"]
|
21
|
+
]
|
22
|
+
|
23
|
+
@conn = Yodlee::Connection.new(@cred)
|
24
|
+
|
25
|
+
# prevent accidental connections
|
26
|
+
inject_agent(nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_not_connected
|
30
|
+
assert !@conn.connected?
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_valid_session
|
34
|
+
# flunk
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_provide_username
|
38
|
+
inject_agent(mock = flexmock("mechanize"))
|
39
|
+
|
40
|
+
mock.should_receive(:get).once.and_return(flexmock(:form_with => Hash.new))
|
41
|
+
mock.should_receive(:submit).with('loginName' => "bob")
|
42
|
+
|
43
|
+
@conn.provide_username
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_answer_question
|
47
|
+
inject_agent(agent = flexmock("mechanize"))
|
48
|
+
|
49
|
+
page = flexmock("page")
|
50
|
+
page.should_receive(:body).once.and_return("<label for='answer'>#{Yodlee::Credentials::QUESTIONS[0]}</label>")
|
51
|
+
page.should_receive(:form_with).with(:name => 'loginForm').once.and_return(Hash.new)
|
52
|
+
|
53
|
+
agent.should_receive(:submit).with('answer' => "aa")
|
54
|
+
|
55
|
+
@conn.answer_question(page)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_answer_question_fails
|
59
|
+
inject_agent(agent = flexmock("mechanize"))
|
60
|
+
|
61
|
+
page = flexmock("page")
|
62
|
+
page.should_receive(:body).once.and_return("<label for='answer'>#{Yodlee::Credentials::QUESTIONS[9]}</label>")
|
63
|
+
|
64
|
+
assert_raises Yodlee::NoAnswerForQuestion do
|
65
|
+
@conn.answer_question(page)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_check_expectation
|
70
|
+
page = flexmock("page")
|
71
|
+
page.should_receive(:body).once.and_return("
|
72
|
+
<dl>
|
73
|
+
<dt>Secret Phrase:</dt>
|
74
|
+
<dd> bar
|
75
|
+
<div class=\"caption\">Ensure etc etc blah blah</div>
|
76
|
+
</dd></dl>")
|
77
|
+
|
78
|
+
assert_equal page, @conn.check_expectation(page)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_check_expectation_fails
|
82
|
+
page = flexmock("page")
|
83
|
+
page.should_receive(:body).once.and_return("
|
84
|
+
<dl>
|
85
|
+
<dt>Secret Phrase:</dt>
|
86
|
+
<dd> wrong expectation!
|
87
|
+
<div class=\"caption\">Ensure etc etc trick it by putting it here bar blah blah</div>
|
88
|
+
</dd></dl>")
|
89
|
+
|
90
|
+
assert_raises Yodlee::ExpectationMismatch do
|
91
|
+
@conn.check_expectation(page)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_provide_password
|
96
|
+
inject_agent(agent = flexmock("mechanize"))
|
97
|
+
|
98
|
+
page = flexmock("page")
|
99
|
+
page.should_receive(:form_with).with(:name => 'loginForm').once.and_return(Hash.new)
|
100
|
+
page.should_receive(:form_with).with(:name => 'updateForm').once.and_return(Hash.new)
|
101
|
+
page.should_receive(:uri).and_return(URI.parse("http://example.com/accounts.page?x=y"))
|
102
|
+
|
103
|
+
agent.should_receive(:submit).with('password' => "foo").and_return(page)
|
104
|
+
agent.should_receive(:submit).with(Hash.new).and_return(page)
|
105
|
+
|
106
|
+
@conn.provide_password(page)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_login
|
110
|
+
mock = flexmock(@conn)
|
111
|
+
mock.should_receive(:provide_username).ordered.with_no_args.once.and_return(page = flexmock("page"))
|
112
|
+
mock.should_receive(:answer_question, :check_expectation, :provide_password).ordered.once.and_return(page)
|
113
|
+
|
114
|
+
assert !mock.connected?
|
115
|
+
mock.login
|
116
|
+
assert mock.connected?
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_accounts
|
120
|
+
mock = flexmock(@conn)
|
121
|
+
inject_agent(agent = flexmock("mechanize"))
|
122
|
+
|
123
|
+
mock.should_receive(:handle_connection!).once
|
124
|
+
agent.should_receive(:get).once.and_return(
|
125
|
+
flexmock(:body => "
|
126
|
+
<div class='acctbean'>
|
127
|
+
<a href='u?itemId=1&itemAccountId=2'><strong>x</strong> - y</a>
|
128
|
+
<a href='u?itemId=8&itemAccountId=9'><strong>a</strong> - b</a>
|
129
|
+
</div>")
|
130
|
+
)
|
131
|
+
|
132
|
+
assert_equal "x", @conn.accounts.first.institute_name
|
133
|
+
assert_equal "y", @conn.accounts.first.name
|
134
|
+
assert_equal "1", @conn.accounts.first.institute_id
|
135
|
+
assert_equal "2", @conn.accounts.first.id
|
136
|
+
|
137
|
+
assert_equal "a", @conn.accounts.last.institute_name
|
138
|
+
assert_equal "b", @conn.accounts.last.name
|
139
|
+
assert_equal "8", @conn.accounts.last.institute_id
|
140
|
+
assert_equal "9", @conn.accounts.last.id
|
141
|
+
|
142
|
+
assert_equal 2, @conn.accounts.size
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_account_transactions
|
146
|
+
end
|
147
|
+
|
148
|
+
def inject_agent(mock)
|
149
|
+
@conn.instance_eval { @agent = mock }
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
data/yodlee.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{yodlee}
|
5
|
+
s.version = "0.0.1.20090227002742"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Andrew A. Smith"]
|
9
|
+
s.date = %q{2009-02-27}
|
10
|
+
s.description = %q{Fetches accounts and their transaction details from the Yodlee MoneyCenter (https://moneycenter.yodlee.com).}
|
11
|
+
s.email = ["andy@tinnedfruit.org"]
|
12
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc"]
|
13
|
+
s.files = ["History.txt", "Manifest.txt", "README.rdoc", "Rakefile", "lib/yodlee.rb", "lib/yodlee/account.rb", "lib/yodlee/connection.rb", "lib/yodlee/credentials.rb", "lib/yodlee/exceptions.rb", "lib/yodlee/monkeypatches.rb", "lib/yodlee/version.rb", "test/test_yodlee.rb", "yodlee.gemspec"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.homepage = %q{http://github.com/aasmith/yodlee}
|
16
|
+
s.rdoc_options = ["--main", "README.rdoc"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = %q{yodlee}
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
20
|
+
s.summary = %q{Fetches financial data from Yodlee MoneyCenter.}
|
21
|
+
s.test_files = ["test/test_yodlee.rb"]
|
22
|
+
|
23
|
+
if s.respond_to? :specification_version then
|
24
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
25
|
+
s.specification_version = 2
|
26
|
+
|
27
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
28
|
+
s.add_runtime_dependency(%q<mechanize>, [">= 0"])
|
29
|
+
s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
|
30
|
+
s.add_development_dependency(%q<flexmock>, [">= 0"])
|
31
|
+
s.add_development_dependency(%q<hoe>, [">= 1.9.0"])
|
32
|
+
else
|
33
|
+
s.add_dependency(%q<mechanize>, [">= 0"])
|
34
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
35
|
+
s.add_dependency(%q<flexmock>, [">= 0"])
|
36
|
+
s.add_dependency(%q<hoe>, [">= 1.9.0"])
|
37
|
+
end
|
38
|
+
else
|
39
|
+
s.add_dependency(%q<mechanize>, [">= 0"])
|
40
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
41
|
+
s.add_dependency(%q<flexmock>, [">= 0"])
|
42
|
+
s.add_dependency(%q<hoe>, [">= 1.9.0"])
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aasmith-yodlee
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.20090227002742
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew A. Smith
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-27 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mechanize
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: nokogiri
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: flexmock
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: hoe
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.9.0
|
54
|
+
version:
|
55
|
+
description: Fetches accounts and their transaction details from the Yodlee MoneyCenter (https://moneycenter.yodlee.com).
|
56
|
+
email:
|
57
|
+
- andy@tinnedfruit.org
|
58
|
+
executables: []
|
59
|
+
|
60
|
+
extensions: []
|
61
|
+
|
62
|
+
extra_rdoc_files:
|
63
|
+
- History.txt
|
64
|
+
- Manifest.txt
|
65
|
+
- README.rdoc
|
66
|
+
files:
|
67
|
+
- History.txt
|
68
|
+
- Manifest.txt
|
69
|
+
- README.rdoc
|
70
|
+
- Rakefile
|
71
|
+
- lib/yodlee.rb
|
72
|
+
- lib/yodlee/account.rb
|
73
|
+
- lib/yodlee/connection.rb
|
74
|
+
- lib/yodlee/credentials.rb
|
75
|
+
- lib/yodlee/exceptions.rb
|
76
|
+
- lib/yodlee/monkeypatches.rb
|
77
|
+
- lib/yodlee/version.rb
|
78
|
+
- test/test_yodlee.rb
|
79
|
+
- yodlee.gemspec
|
80
|
+
has_rdoc: true
|
81
|
+
homepage: http://github.com/aasmith/yodlee
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options:
|
84
|
+
- --main
|
85
|
+
- README.rdoc
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: "0"
|
93
|
+
version:
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: "0"
|
99
|
+
version:
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project: yodlee
|
103
|
+
rubygems_version: 1.2.0
|
104
|
+
signing_key:
|
105
|
+
specification_version: 2
|
106
|
+
summary: Fetches financial data from Yodlee MoneyCenter.
|
107
|
+
test_files:
|
108
|
+
- test/test_yodlee.rb
|