wesabe 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Wesabe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ wesabe
2
+ ======
3
+
4
+ Access the Wesabe API. See the examples directory for usage examples.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ Make sure you have bundler installed, then run this:
10
+
11
+ $ gem bundle --cached
12
+ $ rake spec gem
13
+
14
+ Assuming the specs pass and the gem is generated, run this (with `sudo` if required):
15
+
16
+ $ gem install pkg/wesabe*.gem
@@ -0,0 +1,75 @@
1
+ require 'rubygems'
2
+ require 'rubygems/specification'
3
+ require 'rake/gempackagetask'
4
+ require 'bundler'
5
+ require File.dirname(__FILE__)+'/lib/wesabe'
6
+
7
+ GEM = "wesabe"
8
+ GEM_VERSION = Wesabe::VERSION
9
+ AUTHOR = "Brian Donovan"
10
+ EMAIL = "brian@wesabe.com"
11
+ HOMEPAGE = "https://www.wesabe.com/page/api"
12
+ SUMMARY = "Wraps communication with the Wesabe API"
13
+ PROJECT = "wesabe"
14
+
15
+ SPEC = Gem::Specification.new do |s|
16
+ s.name = GEM
17
+ s.version = GEM_VERSION
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["README.markdown", "LICENSE"]
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.author = AUTHOR
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.rubyforge_project = PROJECT
27
+
28
+ env = Bundler::Bundle.load.environment
29
+ env.dependencies.each do |dep|
30
+ s.add_dependency(dep.name, dep.version.to_s)
31
+ end
32
+
33
+ s.require_path = 'lib'
34
+ # s.bindir = "bin"
35
+ # s.executables = %w( wesabe )
36
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{bin,lib,specs}/**/*")
37
+ end
38
+
39
+ Rake::GemPackageTask.new(SPEC) do |pkg|
40
+ pkg.gem_spec = SPEC
41
+ end
42
+
43
+ require 'spec/rake/spectask'
44
+ desc "Run specs"
45
+ Spec::Rake::SpecTask.new(:spec) do |t|
46
+ t.spec_opts << %w(-fs --color) << %w(-O spec/spec.opts)
47
+ t.spec_opts << '--loadby' << 'random'
48
+ t.spec_files = Dir["spec/**/*_spec.rb"]
49
+ end
50
+
51
+ desc "Generates the documentation for this project"
52
+ task :doc do
53
+ `yardoc 'lib/**/*.rb' 2>/dev/null`
54
+ `open doc/index.html 2>/dev/null`
55
+ end
56
+
57
+ namespace :wesabe do
58
+ desc "Downloads and installs an updated PEM file (requires openssl)"
59
+ task :update_pem do
60
+ # get the certificate
61
+ certs = `echo QUIT | openssl s_client -showcerts -connect www.wesabe.com:443 2>/dev/null`
62
+ pem = certs[/-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----/, 0]
63
+
64
+ # write it to our pem file
65
+ dir = File.expand_path("~/.wesabe")
66
+ path = File.join(dir, "cacert.pem")
67
+ FileUtils.mkdir_p(dir)
68
+ File.open(path, 'w') do |file|
69
+ file.puts pem
70
+ end
71
+ puts "Wrote PEM file to #{path}"
72
+ end
73
+ end
74
+
75
+ task :default => :spec
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + "/../lib"
4
+ require 'rubygems'
5
+ require 'wesabe'
6
+
7
+ require 'yaml'
8
+
9
+ def usage(why = nil)
10
+ $stderr.puts "failed for reason: #{why}" if why
11
+ $stderr.puts "usage: console [url|name] [username] [password]"
12
+ exit(1)
13
+ end
14
+
15
+ @url = ARGV.shift if ARGV.size != 2
16
+ @username, @password = ARGV.shift, ARGV.shift
17
+
18
+ @url ||= "https://www.wesabe.com/"
19
+
20
+ config = YAML.load(File.read(ENV['HOME'] + "/.wesabe.console")) rescue {}
21
+
22
+ if c = config[@url]
23
+ @url = c["url"]
24
+ @username = c["username"] || @username
25
+ @password = c["password"] || @password
26
+ end
27
+
28
+ usage("invalid url #{@url.inspect}") unless @url =~ /^https?/
29
+ usage("missing username") unless @username
30
+ usage("missing password") unless @password
31
+
32
+ def w
33
+ @w ||= begin
34
+ Wesabe::Request.base_url = @url
35
+ Wesabe.new(@username, @password)
36
+ end
37
+ end
38
+
39
+ w # force it to load
40
+
41
+ def method_missing(s, *args, &b)
42
+ super unless w.respond_to?(s)
43
+ begin
44
+ w.send(s, *args, &b)
45
+ rescue Wesabe::Request::RequestFailed => e
46
+ puts e.response.body
47
+ raise e
48
+ end
49
+ end
50
+
51
+ require 'irb'
52
+ require 'irb/completion'
53
+
54
+ if File.exists? ".irbrc"
55
+ ENV['IRBRC'] = ".irbrc"
56
+ end
57
+
58
+ ARGV.clear
59
+
60
+ puts "Starting the Wesabe API console for #{@username} at #{@url}"
61
+ puts "(try 'accounts', 'credentials', or 'targets')"
62
+
63
+ IRB.start
64
+ exit!
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx
3
+ FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
4
+ VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
5
+ biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm
6
+ MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx
7
+ MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
8
+ DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3
9
+ dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl
10
+ cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3
11
+ DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD
12
+ gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
13
+ yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
14
+ L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj
15
+ EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG
16
+ 7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e
17
+ QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ
18
+ qdq5snUb9kLy78fyGPmJvKP/iiMucEc=
19
+ -----END CERTIFICATE-----
@@ -0,0 +1,164 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ # no rubygems, just keep going
7
+ end
8
+
9
+ require 'hpricot'
10
+ require 'net/https'
11
+ require 'yaml'
12
+ require 'time'
13
+
14
+ # Provides an object-oriented interface to the Wesabe API.
15
+ class Wesabe
16
+ attr_accessor :username, :password
17
+
18
+ VERSION = '0.0.3'
19
+
20
+ # Initializes access to the Wesabe API with a certain user. All requests
21
+ # will be made in the context of this user.
22
+ #
23
+ # @param [String] username
24
+ # The username of an active Wesabe user.
25
+ #
26
+ # @param [String] password
27
+ # The password of an active Wesabe user.
28
+ def initialize(username, password)
29
+ self.username = username
30
+ self.password = password
31
+ end
32
+
33
+ # Fetches the user's accounts list from Wesabe or, if the list was already
34
+ # fetched, returns the cached result.
35
+ #
36
+ # pp wesabe.accounts
37
+ # [#<Wesabe::Account:0x106105c
38
+ # @balance=-393.42,
39
+ # @currency=
40
+ # #<Wesabe::Currency:0x104fdc0
41
+ # @decimal_places=2,
42
+ # @delimiter=",",
43
+ # @separator=".",
44
+ # @symbol="$">,
45
+ # @financial_institution=
46
+ # #<Wesabe::FinancialInstitution:0x104b054
47
+ # @homepage_url=nil,
48
+ # @id="us-003383",
49
+ # @login_url=nil,
50
+ # @name="American Express Card">,
51
+ # @id=4,
52
+ # @name="Amex Blue">]
53
+ #
54
+ # @return [Array<Wesabe::Account>]
55
+ # A list of the user's active accounts.
56
+ def accounts
57
+ @accounts ||= load_accounts
58
+ end
59
+
60
+ # Returns an account with the given id or +nil+ if the account is not found.
61
+ #
62
+ # wesabe.account(4).name # => "Amex Blue"
63
+ #
64
+ # @param [#to_s] id
65
+ # Something whose +to_s+ result matches the +to_s+ result of the account id.
66
+ #
67
+ # @return [Wesabe::Account, nil]
68
+ # The account whose user-scoped id is +id+ or +nil+ if there is no account
69
+ # with that +id+.
70
+ def account(id)
71
+ accounts.find {|a| a.id.to_s == id.to_s}
72
+ end
73
+
74
+ # Fetches the user's accounts list from Wesabe or, if the list was already
75
+ # fetched, returns the cached result.
76
+ #
77
+ # pp wesabe.credentials
78
+ # [#<Wesabe::Credential:0x10ae870
79
+ # @accounts=[],
80
+ # @financial_institution=
81
+ # #<Wesabe::FinancialInstitution:0x1091928
82
+ # @homepage_url=nil,
83
+ # @id="us-003383",
84
+ # @login_url=nil,
85
+ # @name="American Express Card">,
86
+ # @id=3>]
87
+ #
88
+ # @return [Array<Wesabe::Account>]
89
+ # A list of the user's active accounts.
90
+ def credentials
91
+ @credentials ||= load_credentials
92
+ end
93
+
94
+ # Fetches the user's targets list from Wesabe or, if the list was already
95
+ # fetched, returns the cached result.
96
+ def targets
97
+ @targets ||= load_targets
98
+ end
99
+
100
+ # Executes a request via POST with the initial username and password.
101
+ #
102
+ # @see Wesabe::Request::execute
103
+ def post(options)
104
+ Request.execute({:method => :post, :username => username, :password => password}.merge(options))
105
+ end
106
+
107
+ # Executes a request via GET with the initial username and password.
108
+ #
109
+ # @see Wesabe::Request::execute
110
+ def get(options)
111
+ Request.execute({:method => :get, :username => username, :password => password}.merge(options))
112
+ end
113
+
114
+ def inspect
115
+ "#<#{self.class.name} username=#{username.inspect} password=#{password.gsub(/./, '*').inspect} url=#{Wesabe::Request.base_url.inspect}>"
116
+ end
117
+
118
+ private
119
+
120
+ def load_accounts
121
+ process_accounts( Hpricot::XML( get(:url => '/accounts.xml') ) )
122
+ end
123
+
124
+ def process_accounts(xml)
125
+ associate((xml / :accounts / :account).map do |element|
126
+ Account.from_xml(element)
127
+ end)
128
+ end
129
+
130
+ def load_credentials
131
+ process_credentials( Hpricot::XML( get(:url => '/credentials.xml') ) )
132
+ end
133
+
134
+ def process_credentials(xml)
135
+ associate((xml / :credentials / :credential).map do |element|
136
+ Credential.from_xml(element)
137
+ end)
138
+ end
139
+
140
+ def load_targets
141
+ process_targets( Hpricot::XML( get(:url => '/targets.xml') ) )
142
+ end
143
+
144
+ def process_targets(xml)
145
+ associate((xml / :targets / :target).map do |element|
146
+ Target.from_xml(element)
147
+ end)
148
+ end
149
+
150
+ def associate(what)
151
+ Wesabe::Util.all_or_one(what) {|obj| obj.wesabe = self}
152
+ end
153
+ end
154
+
155
+ require 'wesabe/util'
156
+ require 'wesabe/request'
157
+ require 'wesabe/base_model'
158
+ require 'wesabe/account'
159
+ require 'wesabe/upload'
160
+ require 'wesabe/financial_institution'
161
+ require 'wesabe/currency'
162
+ require 'wesabe/credential'
163
+ require 'wesabe/job'
164
+ require 'wesabe/target'
@@ -0,0 +1,62 @@
1
+ # Encapsulates an account from Wesabe's API.
2
+ class Wesabe::Account < Wesabe::BaseModel
3
+ # The user-scoped account id, used to identify the account in URLs.
4
+ attr_accessor :id
5
+ # The application-scoped account id, used in upload
6
+ attr_accessor :number
7
+ # The user-provided account name ("Bank of America - Checking")
8
+ attr_accessor :name
9
+ # The account type ("Credit Card", "Savings" ...)
10
+ attr_accessor :type
11
+ # This account's balance or +nil+ if the account is a cash account.
12
+ attr_accessor :balance
13
+ # This account's currency.
14
+ attr_accessor :currency
15
+ # The financial institution this account is held at.
16
+ attr_accessor :financial_institution
17
+
18
+ # Initializes a +Wesabe::Account+ and yields itself.
19
+ #
20
+ # @yieldparam [Wesabe::Account] account
21
+ # The newly-created account.
22
+ def initialize
23
+ yield self if block_given?
24
+ end
25
+
26
+ # Creates a +Wesabe::Upload+ that can be used to upload to this account.
27
+ #
28
+ # @return [Wesabe::Upload]
29
+ # The newly-created upload, ready to be used to upload a statement.
30
+ def new_upload
31
+ Wesabe::Upload.new do |upload|
32
+ upload.accounts = [self]
33
+ upload.financial_institution = financial_institution
34
+ associate upload
35
+ end
36
+ end
37
+
38
+ # Returns a +Wesabe::Account+ generated from Wesabe's API XML.
39
+ #
40
+ # @param [Hpricot::Element] xml
41
+ # The <account> element from the API.
42
+ #
43
+ # @return [Wesabe::Account]
44
+ # The newly-created account populated by +xml+.
45
+ def self.from_xml(xml)
46
+ new do |account|
47
+ account.id = xml.at("id").inner_text.to_i
48
+ account.name = xml.at("name").inner_text
49
+ account.type = xml.at("account-type").inner_text
50
+ account.number = xml.at("account-number").inner_text if xml.at("account-number")
51
+ balance = xml.at("current-balance")
52
+ account.balance = balance.inner_text.to_f if balance
53
+ account.currency = Wesabe::Currency.from_xml(xml.at("currency"))
54
+ fi = xml.at("financial-institution")
55
+ account.financial_institution = Wesabe::FinancialInstitution.from_xml(fi) if fi
56
+ end
57
+ end
58
+
59
+ def inspect
60
+ inspect_these :id, :number, :type, :name, :balance, :financial_institution, :currency
61
+ end
62
+ end
@@ -0,0 +1,31 @@
1
+ class Wesabe::BaseModel
2
+ include Wesabe::Util
3
+ # The +Wesabe+ instance this model uses.
4
+ #
5
+ # @return [Wesabe] The object containing the username/password to use.
6
+ attr_accessor :wesabe
7
+
8
+ # Requests via POST using the given options.
9
+ #
10
+ # @see Wesabe#post
11
+ def post(options)
12
+ wesabe.post(options)
13
+ end
14
+
15
+ # Requests via GET using the given options.
16
+ #
17
+ # @see Wesabe#get
18
+ def get(options)
19
+ wesabe.get(options)
20
+ end
21
+
22
+ private
23
+
24
+ def associate(what)
25
+ all_or_one(what) {|obj| obj.wesabe = wesabe}
26
+ end
27
+
28
+ def inspect_these(*attributes)
29
+ "#<#{self.class.name}#{attributes.map{|a| " #{a}=#{send(a).inspect}"}.join}>"
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ class Wesabe::Credential < Wesabe::BaseModel
2
+ # The id of the credential, used to identify the account in URLs.
3
+ attr_accessor :id
4
+ # The financial institution this credential is for.
5
+ attr_accessor :financial_institution
6
+ # The accounts linked to this credential.
7
+ attr_accessor :accounts
8
+
9
+ # Initializes a +Wesabe::Credential+ and yields itself.
10
+ #
11
+ # @yieldparam [Wesabe::Credential] credential
12
+ # The newly-created credential.
13
+ def initialize
14
+ yield self if block_given?
15
+ end
16
+
17
+ # Starts a new sync job for this +Wesabe::Credential+.
18
+ #
19
+ # @return [Wesabe::Job]
20
+ # The job that was just started.
21
+ def start_job
22
+ associate(Wesabe::Job.from_xml(Hpricot::XML(post(:url => "/credentials/#{id}/jobs.xml")) / :job))
23
+ end
24
+
25
+ # Returns a +Wesabe::Credential+ generated from Wesabe's API XML.
26
+ #
27
+ # @param [Hpricot::Element] xml
28
+ # The <credential> element from the API.
29
+ #
30
+ # @return [Wesabe::Credential]
31
+ # The newly-created credential populated by +xml+.
32
+ def self.from_xml(xml)
33
+ new do |cred|
34
+ cred.id = xml.at('id').inner_text.to_i
35
+ cred.financial_institution = Wesabe::FinancialInstitution.from_xml(
36
+ xml.children_of_type('financial-institution')[0])
37
+ cred.accounts = xml.search('accounts account').map do |account|
38
+ Wesabe::Account.from_xml(account)
39
+ end
40
+ end
41
+ end
42
+
43
+ def inspect
44
+ inspect_these :id, :financial_institution, :accounts
45
+ end
46
+
47
+ private
48
+
49
+ def associate(what)
50
+ all_or_one(super(what)) {|obj| obj.credential = self}
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ class Wesabe::Currency < Wesabe::BaseModel
2
+ attr_accessor :decimal_places, :symbol, :separator, :delimiter
3
+
4
+ # Initializes a +Wesabe::Currency+ and yields itself.
5
+ #
6
+ # @yieldparam [Wesabe::Currency] currency
7
+ # The newly-created currency.
8
+ def initialize
9
+ yield self if block_given?
10
+ end
11
+
12
+ alias_method :precision, :decimal_places
13
+ alias_method :precision=, :decimal_places=
14
+
15
+ # Returns a +Wesabe::Currency+ generated from Wesabe's API XML.
16
+ #
17
+ # @param [Hpricot::Element] xml
18
+ # The <currency> element from the API.
19
+ #
20
+ # @return [Wesabe::Currency]
21
+ # The newly-created currency populated by +xml+.
22
+ def self.from_xml(xml)
23
+ new do |currency|
24
+ currency.decimal_places = xml[:decimal_places].to_s.to_i
25
+ currency.symbol = xml[:symbol].to_s
26
+ currency.separator = xml[:separator].to_s
27
+ currency.delimiter = xml[:delimiter].to_s
28
+ end
29
+ end
30
+
31
+ def inspect
32
+ inspect_these :symbol, :decimal_places, :separator, :delimiter
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ class Wesabe::FinancialInstitution < Wesabe::BaseModel
2
+ # The id of this +FinancialInstitution+, as used in URLs.
3
+ attr_accessor :id
4
+ # The name of this +FinancialInstitution+ ("Bank of America").
5
+ attr_accessor :name
6
+ # The url users of this +FinancialInstitution+ log in to for online banking.
7
+ attr_accessor :login_url
8
+ # The home url of this +FinancialInstitution+.
9
+ attr_accessor :homepage_url
10
+
11
+ # Initializes a +Wesabe::FinancialInstitution+ and yields itself.
12
+ #
13
+ # @yieldparam [Wesabe::FinancialInstitution] financial_institution
14
+ # The newly-created financial institution.
15
+ def initialize
16
+ yield self if block_given?
17
+ end
18
+
19
+ # Returns a +Wesabe::FinancialInstitution+ generated from Wesabe's API XML.
20
+ #
21
+ # @param [Hpricot::Element] xml
22
+ # The <financial-institution> element from the API.
23
+ #
24
+ # @return [Wesabe::FinancialInstitution]
25
+ # The newly-created financial institution populated by +xml+.
26
+ def self.from_xml(xml)
27
+ new do |fi|
28
+ fi.id = (xml.children_of_type("id") + xml.children_of_type("wesabe-id")).first.inner_text
29
+ fi.name = xml.at("name").inner_text
30
+ fi.login_url = xml.at("login-url") && xml.at("login-url").inner_text
31
+ fi.homepage_url = xml.at("homepage-url") && xml.at("homepage-url").inner_text
32
+ end
33
+ end
34
+
35
+ def inspect
36
+ inspect_these :id, :name
37
+ end
38
+ end
@@ -0,0 +1,97 @@
1
+ class Wesabe::Job < Wesabe::BaseModel
2
+ # The globally unique identifier for this job.
3
+ attr_accessor :id
4
+ # The status of this job (pending|successful|failed).
5
+ attr_accessor :status
6
+ # The result of this job, which gives more specific
7
+ # information for "pending" and "failed" statuses.
8
+ attr_accessor :result
9
+ # When this job was created.
10
+ attr_accessor :created_at
11
+ # The credential that this job belongs to.
12
+ attr_accessor :credential
13
+
14
+ # Initializes a +Wesabe::Job+ and yields itself.
15
+ #
16
+ # @yieldparam [Wesabe::Job] job
17
+ # The newly-created job.
18
+ def initialize
19
+ yield self if block_given?
20
+ end
21
+
22
+ # Reloads this job from the server, useful when polling for updates.
23
+ #
24
+ # job = credential.start_job
25
+ # until job.complete?
26
+ # print '.'
27
+ # job.reload
28
+ # sleep 1
29
+ # end
30
+ # puts
31
+ # puts "Job finished with status=#{job.status}, result=#{job.result}"
32
+ #
33
+ # @return [Wesabe::Job] Returns self.
34
+ def reload
35
+ replace(
36
+ Wesabe::Job.from_xml(
37
+ Hpricot.XML(
38
+ get(:url => "/credentials/#{credential.id}/jobs/#{id}.xml"))))
39
+ return self
40
+ end
41
+
42
+ # Determines whether this job is still running.
43
+ #
44
+ # @return [Boolean] Whether the job is still running.
45
+ def pending?
46
+ status == 'pending'
47
+ end
48
+
49
+ # Determines whether this job is finished.
50
+ #
51
+ # @return [Boolean] Whether the job is finished running.
52
+ def complete?
53
+ !pending?
54
+ end
55
+
56
+ # Determines whether this job is successful.
57
+ #
58
+ # @return [Boolean]
59
+ # Whether this job has completed and, if so, whether it was successful.
60
+ def successful?
61
+ status == 'successful'
62
+ end
63
+
64
+ # Determines whether this job failed.
65
+ #
66
+ # @return [Boolean]
67
+ # Whether this job has completed and, if so, whether it failed.
68
+ def failed?
69
+ status == 'failed'
70
+ end
71
+
72
+ # Returns a +Wesabe::Job+ generated from Wesabe's API XML.
73
+ #
74
+ # @param [Hpricot::Element] xml
75
+ # The <job> element from the API.
76
+ #
77
+ # @return [Wesabe::Job]
78
+ # The newly-created job populated by +xml+.
79
+ def self.from_xml(xml)
80
+ new do |job|
81
+ job.id = xml.at('id').inner_text
82
+ job.status = xml.at('status').inner_text
83
+ job.result = xml.at('result').inner_text
84
+ job.created_at = Time.parse(xml.at('created-at').inner_text)
85
+ end
86
+ end
87
+
88
+ def inspect
89
+ inspect_these :id, :status, :result, :created_at
90
+ end
91
+
92
+ private
93
+
94
+ def replace(with)
95
+ with.instance_variables.each {|ivar| instance_variable_set(ivar, with.instance_variable_get(ivar))}
96
+ end
97
+ end
@@ -0,0 +1,186 @@
1
+ class Wesabe::Request
2
+ attr_reader :url, :username, :password, :method, :proxy, :payload
3
+
4
+ DEFAULT_HEADERS = {
5
+ 'User-Agent' => "Wesabe-RubyGem/#{Wesabe::VERSION} (Ruby #{RUBY_VERSION}; #{RUBY_PLATFORM})"
6
+ }
7
+
8
+ private
9
+
10
+ def initialize(options=Hash.new)
11
+ @url = options[:url] or raise ArgumentError, "Missing option 'url'"
12
+ @username = options[:username] or raise ArgumentError, "Missing option 'username'"
13
+ @password = options[:password] or raise ArgumentError, "Missing option 'password'"
14
+ @proxy = options[:proxy]
15
+ @method = options[:method] || :get
16
+ @payload = options[:payload]
17
+ end
18
+
19
+ # Returns a new Net::HTTP instance to connect to the Wesabe API.
20
+ #
21
+ # @return [Net::HTTP]
22
+ # A connection object all ready to be used to communicate securely.
23
+ def net
24
+ http = net_http_class.new(uri.host, uri.port)
25
+ if uri.scheme == 'https'
26
+ http.use_ssl = true
27
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
28
+ http.ca_file = self.class.ca_file
29
+ end
30
+ http
31
+ end
32
+
33
+ def net_http_class
34
+ if proxy
35
+ proxy_uri = URI.parse(proxy)
36
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
37
+ else
38
+ Net::HTTP
39
+ end
40
+ end
41
+
42
+ def uri
43
+ URI.join(self.class.base_url, url)
44
+ end
45
+
46
+ def process_response(res)
47
+ if %w[200 201 202].include?(res.code)
48
+ res.body
49
+ elsif %w[301 302 303].include?(res.code)
50
+ url = res.header['Location']
51
+
52
+ if url !~ /^http/
53
+ uri = URI.parse(@url)
54
+ uri.path = "/#{url}".squeeze('/')
55
+ url = uri.to_s
56
+ end
57
+
58
+ raise Redirect, url
59
+ elsif res.code == "401"
60
+ raise Unauthorized
61
+ elsif res.code == "404"
62
+ raise ResourceNotFound
63
+ else
64
+ raise RequestFailed, res
65
+ end
66
+ end
67
+
68
+ public
69
+
70
+ # Executes the request and returns the response.
71
+ #
72
+ # @return [String]
73
+ # The response object for the request just made.
74
+ #
75
+ # @raise [Wesabe::ServerConnectionBroken]
76
+ # If the connection with the server breaks.
77
+ #
78
+ # @raise [Timeout::Error]
79
+ # If the request takes too long.
80
+ def execute
81
+ # set up the uri
82
+ @username = uri.user if uri.user
83
+ @password = uri.password if uri.password
84
+
85
+ # set up the request
86
+ req = Net::HTTP.const_get(method.to_s.capitalize).new(uri.request_uri, DEFAULT_HEADERS)
87
+ req.basic_auth(username, password)
88
+
89
+ net.start do |http|
90
+ process_response http.request(req, payload || "")
91
+ end
92
+ end
93
+
94
+ # Executes a request and returns the response.
95
+ #
96
+ # @param [String] options[:url]
97
+ # The url relative to +Wesabe::Request.base_url+ to request (required).
98
+ #
99
+ # @param [String] options[:username]
100
+ # The Wesabe username (required).
101
+ #
102
+ # @param [String] options[:password]
103
+ # The Wesabe password (required).
104
+ #
105
+ # @param [String] options[:proxy]
106
+ # The proxy url to use (optional).
107
+ #
108
+ # @param [String, Symbol] options[:method]
109
+ # The HTTP method to use (defaults to +:get+).
110
+ #
111
+ # @param [String] options[:payload]
112
+ # The post-body to use (defaults to an empty string).
113
+ #
114
+ # @return [Net::HTTPResponse]
115
+ # The response object for the request just made.
116
+ #
117
+ # @raise [EOFError]
118
+ # If the connection with the server breaks.
119
+ #
120
+ # @raise [Timeout::Error]
121
+ # If the request takes too long.
122
+ def self.execute(options=Hash.new)
123
+ new(options).execute
124
+ end
125
+
126
+ def self.ca_file
127
+ [File.expand_path("~/.wesabe"), File.join(File.dirname(__FILE__), '..')].each do |dir|
128
+ file = File.join(dir, "cacert.pem")
129
+ return file if File.exist?(file)
130
+ end
131
+ raise "Unable to find a CA pem file to use for www.wesabe.com"
132
+ end
133
+
134
+ # Gets the base url for the Wesabe API.
135
+ def self.base_url
136
+ @base_url ||= "https://www.wesabe.com"
137
+ end
138
+
139
+ # Sets the base url for the Wesabe API.
140
+ def self.base_url=(base_url)
141
+ @base_url = base_url
142
+ end
143
+
144
+ class Exception < RuntimeError; end
145
+ class ServerBrokeConnection < Exception; end
146
+ class Redirect < Exception
147
+ attr_reader :location
148
+
149
+ def initialize(location)
150
+ @location = location
151
+ end
152
+
153
+ def message
154
+ "You've been redirected to #{location}"
155
+ end
156
+
157
+ def inspect
158
+ "#<#{self.class.name} Location=#{location.inspect}>"
159
+ end
160
+ end
161
+ class Unauthorized < Exception; end
162
+ class ResourceNotFound < Exception; end
163
+ class RequestFailed < Exception
164
+ attr_reader :response
165
+
166
+ def initialize(response=nil)
167
+ @response = response
168
+ end
169
+
170
+ def message
171
+ begin
172
+ (Hpricot.XML(response.body) / :error / :message).inner_text
173
+ rescue
174
+ response.body
175
+ end
176
+ end
177
+
178
+ def to_s
179
+ message
180
+ end
181
+
182
+ def inspect
183
+ "#<#{self.class.name} Status=#{response.code} Message=#{message.inspect}>"
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,35 @@
1
+ class Wesabe::Target < Wesabe::BaseModel
2
+ # The tag name
3
+ attr_accessor :tag
4
+ # This target's monthly limit ($)
5
+ attr_accessor :monthly_limit
6
+ # This target's amount remaining ($)
7
+ attr_accessor :amount_remaining
8
+
9
+ # Initializes a +Wesabe::Target+ and yields itself.
10
+ #
11
+ # @yieldparam [Wesabe::Target] Target
12
+ # The newly-created Target.
13
+ def initialize
14
+ yield self if block_given?
15
+ end
16
+
17
+ # Returns a +Wesabe::Target+ generated from Wesabe's API XML.
18
+ #
19
+ # @param [Hpricot::Element] xml
20
+ # The <Target> element from the API.
21
+ #
22
+ # @return [Wesabe::Target]
23
+ # The newly-created Target populated by +xml+.
24
+ def self.from_xml(xml)
25
+ new do |target|
26
+ target.tag = xml.at("tag").at("name").inner_text
27
+ target.monthly_limit = xml.at("monthly-limit").inner_text.to_f
28
+ target.amount_remaining = xml.at("amount-remaining").inner_text.to_f
29
+ end
30
+ end
31
+
32
+ def inspect
33
+ inspect_these :tag, :monthly_limit, :amount_remaining
34
+ end
35
+ end
@@ -0,0 +1,96 @@
1
+ # Encapsulates an upload and allows uploading files to wesabe.com.
2
+ class Wesabe::Upload < Wesabe::BaseModel
3
+ # The accounts this upload is associated with.
4
+ attr_accessor :accounts
5
+ # The financial institution this upload is associated with.
6
+ attr_accessor :financial_institution
7
+ # Whether this upload succeeded or failed, or +nil+ if it hasn't started.
8
+ attr_accessor :status
9
+ # The raw statement to post.
10
+ attr_accessor :statement
11
+
12
+ # Initializes a +Wesabe::Upload+ and yields itself.
13
+ #
14
+ # @yieldparam [Wesabe::Upload] upload
15
+ # The newly-created upload.
16
+ def initialize
17
+ yield self if block_given?
18
+ end
19
+
20
+ # Uploads the statement to Wesabe, raising on problems. It can raise
21
+ # anything that is raised by +Wesabe::Request#execute+ in addition to
22
+ # the list below.
23
+ #
24
+ # begin
25
+ # upload.upload!
26
+ # rescue Wesabe::Upload::StatementError => e
27
+ # $stderr.puts "The file you chose to upload couldn't be imported."
28
+ # $stderr.puts "This is what Wesabe said: #{e.message}"
29
+ # rescue Wesabe::Request::Exception
30
+ # $stderr.puts "There was a problem communicating with Wesabe."
31
+ # end
32
+ #
33
+ # @raise [Wesabe::Upload::StatementError]
34
+ # When the statement cannot be processed, this is returned (error code 5).
35
+ #
36
+ # @see Wesabe::Request#execute
37
+ def upload!
38
+ process_response do
39
+ post(:url => '/rest/upload/statement', :payload => pack_statement)
40
+ end
41
+ end
42
+
43
+ # Determines whether this upload succeeded or not.
44
+ #
45
+ # @return [Boolean]
46
+ # +true+ if +status+ is +"processed"+, +false+ otherwise.
47
+ def successful?
48
+ status == "processed"
49
+ end
50
+
51
+ # Determines whether this upload failed or not.
52
+ #
53
+ # @return [Boolean]
54
+ # +false+ if +status+ is +"processed"+, +true+ otherwise.
55
+ def failed?
56
+ !successful?
57
+ end
58
+
59
+ private
60
+
61
+ # Generates XML to upload to wesabe.com to create this +Upload+.
62
+ #
63
+ # @return [String]
64
+ # An XML document containing the relevant upload data.
65
+ def pack_statement
66
+ upload = self
67
+
68
+ Hpricot.build do
69
+ tag! :upload do
70
+ tag! :statement, :accttype => upload.accounts[0].type, :acctid => upload.accounts[0].number, :wesabe_id => upload.financial_institution.id do
71
+ text! upload.statement
72
+ end
73
+ end
74
+ end.inner_html
75
+ end
76
+
77
+ # Processes the response that is the result of +yield+ing.
78
+ #
79
+ # @see upload!
80
+ def process_response
81
+ self.status = nil
82
+ raw = yield
83
+ doc = Hpricot.XML(raw)
84
+ response = doc.at("response")
85
+ raise Exception, "There was an error processing the response: #{raw}" unless response
86
+ self.status = response["status"]
87
+
88
+ if !successful?
89
+ message = response.at("error>message")
90
+ raise StatementError, message && message.inner_text
91
+ end
92
+ end
93
+ end
94
+
95
+ class Wesabe::Upload::Exception < RuntimeError; end
96
+ class Wesabe::Upload::StatementError < Wesabe::Upload::Exception; end
@@ -0,0 +1,16 @@
1
+ module Wesabe::Util
2
+ extend self
3
+
4
+ # Yields +what+ or, if +what+ is an array, each element of +what+. It's
5
+ # sort of like an argument-agnostic +map+.
6
+ #
7
+ # @yieldparam [Object] element
8
+ # If +what+ is an +Array+, this is +what+. Otherwise it's elements of +what+.
9
+ #
10
+ # @return [Array<Object>,Object]
11
+ # If +what+ is an array, it acts like +what.map+. Otherwise +yield(what)+.
12
+ def all_or_one(what, &block)
13
+ result = Array(what).each(&block)
14
+ return what.is_a?(Array) ? result : result.first
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wesabe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Brian Donovan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-19 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.6"
24
+ version:
25
+ description: Wraps communication with the Wesabe API
26
+ email: brian@wesabe.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.markdown
33
+ - LICENSE
34
+ files:
35
+ - LICENSE
36
+ - README.markdown
37
+ - Rakefile
38
+ - bin/console
39
+ - lib/cacert.pem
40
+ - lib/wesabe/account.rb
41
+ - lib/wesabe/base_model.rb
42
+ - lib/wesabe/credential.rb
43
+ - lib/wesabe/currency.rb
44
+ - lib/wesabe/financial_institution.rb
45
+ - lib/wesabe/job.rb
46
+ - lib/wesabe/request.rb
47
+ - lib/wesabe/target.rb
48
+ - lib/wesabe/upload.rb
49
+ - lib/wesabe/util.rb
50
+ - lib/wesabe.rb
51
+ has_rdoc: true
52
+ homepage: https://www.wesabe.com/page/api
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project: wesabe
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Wraps communication with the Wesabe API
79
+ test_files: []
80
+