wesabe 0.0.3

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/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
+