gmoney 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,24 +6,30 @@ A gem for interacting with the Google Finance API
6
6
 
7
7
  gem install gmoney
8
8
 
9
- or
10
-
11
- gem install jspradlin-gmoney --source http://gems.github.com
12
-
13
9
  == Usage
14
10
 
15
11
  Login
16
12
  -----
17
13
 
18
- > GMoney::GFSession.login('google username', 'password')
14
+ GMoney::GFSession.login('google username', 'password')
19
15
 
20
16
  Portfolios
21
17
  --------
22
18
 
19
+ Reading
23
20
  > portfolios = GMoney::Portfolio.all #returns all of a users portfolios
24
21
  > portfolio = GMoney::Portfolio.find(9) #returns a specific portfolio
25
22
 
26
23
  > positions = portfolio.positions #returns an Array of the Positions held within a given portfolio
24
+
25
+ Deleting
26
+ > GMoney::Portfolio.delete 2
27
+
28
+ or
29
+
30
+ > portfolio = GMoney::Portfolio.find 2
31
+ > portfolio.destroy #call destroy on an instance of a portfolio
32
+
27
33
 
28
34
  Positions
29
35
  --------
@@ -32,6 +38,14 @@ Positions
32
38
  > position = GMoney::Position.find("9/NASDAQ:GOOG") #returns a specific position within a given portfolio
33
39
 
34
40
  > transactions = position.transactions #returns an Array of the Transactions within a given position
41
+
42
+ Deleting
43
+ > GMoney::Position.delete '2/NASDAQ:GOOG'
44
+
45
+ or
46
+
47
+ > position = GMoney::Position.find '2/NASDAQ:GOOG'
48
+ > position.destroy #call destroy on an instance of a position
35
49
 
36
50
  Transactions
37
51
  --------
@@ -39,6 +53,14 @@ Transactions
39
53
  > transactions = GMoney::Transaction.find("9/NASDAQ:GOOG") #returns all of a users transactions within a given position
40
54
  > transaction = GMoney::Transaction.find("9/NASDAQ:GOOG/2") #returns a specific transaction within a given position
41
55
 
56
+ Deleting
57
+ > GMoney::Transaction.delete '2/NASDAQ:GOOG/1'
58
+
59
+ or
60
+
61
+ > transaction = GMoney::Transaction.find '2/NASDAQ:GOOG/1'
62
+ > transaction.destroy #call destroy on an instance of a transaction
63
+
42
64
  ----------
43
65
 
44
66
  Options parameter
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{gmoney}
5
- s.version = "0.1.0"
5
+ s.version = "0.2.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Justin Spradlin"]
9
- s.date = %q{2009-10-01}
9
+ s.date = %q{2009-11-13}
10
10
  s.description = %q{A gem for interacting with the Google Finance API}
11
11
  s.email = %q{jspradlin@gmail.com}
12
12
  s.extra_rdoc_files = ["README.rdoc", "lib/extensions/fixnum.rb", "lib/extensions/string.rb", "lib/gmoney.rb", "lib/gmoney/authentication_request.rb", "lib/gmoney/feed_parser.rb", "lib/gmoney/gf_request.rb", "lib/gmoney/gf_response.rb", "lib/gmoney/gf_service.rb", "lib/gmoney/gf_session.rb", "lib/gmoney/portfolio.rb", "lib/gmoney/portfolio_feed_parser.rb", "lib/gmoney/position.rb", "lib/gmoney/position_feed_parser.rb", "lib/gmoney/transaction.rb", "lib/gmoney/transaction_feed_parser.rb"]
@@ -1,7 +1,8 @@
1
1
  class String
2
- class PortfolioParseError < StandardError; end
3
- class PositionParseError < StandardError; end
4
- class TransactionParseError < StandardError; end
2
+ class ParseError < StandardError; end
3
+ class PortfolioParseError < ParseError; end
4
+ class PositionParseError < ParseError; end
5
+ class TransactionParseError < ParseError; end
5
6
 
6
7
  @@portfolio_re = /\d+/
7
8
  @@portfolio_re_in = /^\d+$/
@@ -30,6 +31,13 @@ class String
30
31
  "#{portfolio}/#{position}"
31
32
  end
32
33
 
34
+ def transaction_feed_id
35
+ portfolio = self[self.rindex('portfolios/')+11..index('/positions')-1]
36
+ position = self[self.rindex('positions/')+10..index('/transactions')-1]
37
+ transaction = self[rindex('/')+1..-1]
38
+ "#{portfolio}/#{position}/#{transaction}"
39
+ end
40
+
33
41
  def portfolio_id
34
42
  if self[@@transaction_re_in] || self[@@position_re_in] || self[@@portfolio_re_in]
35
43
  self[@@portfolio_re]
@@ -27,7 +27,7 @@ require 'gmoney/transaction'
27
27
  require 'gmoney/transaction_feed_parser'
28
28
 
29
29
  module GMoney
30
- VERSION = '0.1.0'
30
+ VERSION = '0.2.0'
31
31
  GF_URL = "https://finance.google.com/finance"
32
32
  GF_FEED_URL = "#{GF_URL}/feeds/default"
33
33
  GF_PORTFOLIO_FEED_URL = "#{GF_FEED_URL}/portfolios"
@@ -1,6 +1,7 @@
1
1
  module GMoney
2
2
  class Portfolio
3
3
  class PortfolioRequestError < StandardError;end
4
+ class PortfolioDeleteError < StandardError;end
4
5
 
5
6
  attr_accessor :title, :currency_code
6
7
 
@@ -25,7 +26,16 @@ module GMoney
25
26
 
26
27
  @positions.is_a?(Array) ? @positions : [@positions]
27
28
  end
28
-
29
+
30
+ def self.delete(id)
31
+ delete_portfolio(id)
32
+ end
33
+
34
+ def destroy
35
+ Portfolio.delete(@id.portfolio_feed_id)
36
+ freeze
37
+ end
38
+
29
39
  def self.retreive_portfolios(id, options = {})
30
40
  url = GF_PORTFOLIO_FEED_URL
31
41
  url += "/#{id}" if id != :all
@@ -45,8 +55,17 @@ module GMoney
45
55
  return portfolios[0] if portfolios.size == 1
46
56
 
47
57
  portfolios
48
- end
58
+ end
59
+
60
+ #If you are working behind some firewalls HTTP DELETE request won't work.
61
+ #To overcome this problem the google doc say to use a post request with
62
+ #the X-HTTP-Method-Override set to "DELETE"
63
+ def self.delete_portfolio(id)
64
+ url = "#{GF_PORTFOLIO_FEED_URL}/#{id}"
65
+ response = GFService.send_request(GFRequest.new(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=#{GFSession.auth_token}", "X-HTTP-Method-Override" => "DELETE"}))
66
+ raise PortfolioDeleteError, response.body if response.status_code != HTTPOK
67
+ end
49
68
 
50
- private_class_method :retreive_portfolios
69
+ private_class_method :retreive_portfolios, :delete_portfolio
51
70
  end
52
71
  end
@@ -1,6 +1,8 @@
1
1
  module GMoney
2
2
  class Position
3
3
  class PositionRequestError < StandardError; end
4
+ class PositionDeleteError < StandardError; end
5
+
4
6
  attr_reader :id, :updated, :title, :feed_link, :exchange, :symbol, :shares,
5
7
  :full_name, :gain_percentage, :return1w, :return4w, :return3m,
6
8
  :return_ytd, :return1y, :return3y, :return5y, :return_overall,
@@ -19,6 +21,15 @@ module GMoney
19
21
 
20
22
  @transactions.is_a?(Array) ? @transactions : [@transactions]
21
23
  end
24
+
25
+ def self.delete(id)
26
+ delete_position(id)
27
+ end
28
+
29
+ def destroy
30
+ Position.delete(@id.position_feed_id)
31
+ freeze
32
+ end
22
33
 
23
34
  def self.find_by_url(url, options = {})
24
35
  positions = []
@@ -39,6 +50,24 @@ module GMoney
39
50
  positions
40
51
  end
41
52
 
42
- private_class_method :find_by_url
53
+ #In order to delete a position you must delete all the transactions that fall under
54
+ #that position.
55
+ def self.delete_position(id)
56
+ begin
57
+ trans = Transaction.find("#{id.portfolio_id}/#{id.position_id}")
58
+ if trans.class == Transaction
59
+ trans.destroy
60
+ else
61
+ trans.each {|t| t.destroy }
62
+ end
63
+ rescue Transaction::TransactionRequestError => e
64
+ raise PositionDeleteError, e.message
65
+ rescue String::ParseError
66
+ raise PositionDeleteError, 'Invalid Position ID'
67
+ end
68
+ nil
69
+ end
70
+
71
+ private_class_method :find_by_url, :delete_position
43
72
  end
44
73
  end
@@ -1,6 +1,7 @@
1
1
  module GMoney
2
2
  class Transaction
3
3
  class TransactionRequestError < StandardError; end
4
+ class TransactionDeleteError < StandardError;end
4
5
 
5
6
  attr_reader :id, :updated, :title
6
7
 
@@ -10,6 +11,15 @@ module GMoney
10
11
  find_by_url("#{GF_PORTFOLIO_FEED_URL}/#{id.portfolio_id}/positions/#{id.position_id}/transactions/#{id.transaction_id}", options)
11
12
  end
12
13
 
14
+ def self.delete(id)
15
+ delete_transaction(id)
16
+ end
17
+
18
+ def destroy
19
+ Transaction.delete(@id.transaction_feed_id)
20
+ freeze
21
+ end
22
+
13
23
  def self.find_by_url(url, options={})
14
24
  transactions = []
15
25
 
@@ -26,6 +36,15 @@ module GMoney
26
36
  transactions
27
37
  end
28
38
 
29
- private_class_method :find_by_url
39
+ #If you are working behind some firewalls DELETE HTTP request won't work.
40
+ #To overcome this problem the google doc say to use a post request with
41
+ #the X-HTTP-Method-Override set to "DELETE"
42
+ def self.delete_transaction(id)
43
+ url = "#{GF_PORTFOLIO_FEED_URL}/#{id.portfolio_id}/positions/#{id.position_id}/transactions/#{id.transaction_id}"
44
+ response = GFService.send_request(GFRequest.new(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=#{GFSession.auth_token}", "X-HTTP-Method-Override" => "DELETE"}))
45
+ raise TransactionDeleteError, response.body if response.status_code != HTTPOK
46
+ end
47
+
48
+ private_class_method :find_by_url, :delete_transaction
30
49
  end
31
50
  end
@@ -31,7 +31,8 @@ describe GMoney::PortfolioFeedParser do
31
31
  it "should create Portfolio objects with valid numeric data types for the returns" do
32
32
  @portfolios_with_returns.each do |portfolio|
33
33
  portfolio.public_methods(false).each do |pm|
34
- if !(['id', 'feed_link', 'updated', 'title', 'currency_code', 'positions'].include? pm) && !(pm.include?('='))
34
+ if (['gain_percentage', 'return1w', 'return4w', 'return3m', 'return_ytd', 'return1y', 'return3y', 'return5y',
35
+ 'return_overall', 'cost_basis', 'days_gain', 'gain', 'market_value'].include? pm) && !(pm.include?('='))
35
36
  return_val = portfolio.send(pm)
36
37
  return_val.should be_instance_of(Float) if return_val
37
38
  end
@@ -93,6 +93,47 @@ describe GMoney::Portfolio do
93
93
  portfolio.positions.size.should be_eql(2)
94
94
  end
95
95
 
96
+ it "should delete portfolios using a class method and id" do
97
+ @gf_request = GMoney::GFRequest.new(@url)
98
+ @gf_request.method = :delete
99
+
100
+ @gf_response = GMoney::GFResponse.new
101
+ @gf_response.status_code = 200
102
+
103
+ portfolio_delete_helper("#{@url}/19")
104
+
105
+ GMoney::Portfolio.delete(19).should be_nil
106
+ end
107
+
108
+ it "should delete portfolios by calling destroy on an instance of a portfolio" do
109
+ @gf_request = GMoney::GFRequest.new(@url)
110
+ @gf_request.method = :delete
111
+
112
+ @gf_response = GMoney::GFResponse.new
113
+ @gf_response.status_code = 200
114
+
115
+ portfolio = GMoney::Portfolio.new
116
+ portfolio.instance_variable_set("@id", "#{@url}/24")
117
+
118
+ portfolio_delete_helper("#{@url}/24")
119
+
120
+ portfolio_return = portfolio.destroy
121
+ portfolio_return.should be_eql(portfolio)
122
+ portfolio_return.frozen?.should be_true
123
+ end
124
+
125
+ it "should raise a PortfolioDeleteError when there is an attempt to delete an portfolio that doesn't exist')" do
126
+ @gf_request = GMoney::GFRequest.new(@url)
127
+ @gf_request.method = :delete
128
+
129
+ @gf_response = GMoney::GFResponse.new
130
+ @gf_response.status_code = 400
131
+ @gf_request.body = "Invalid portfolio ID."
132
+
133
+ portfolio_delete_helper("#{@url}/asdf")
134
+
135
+ lambda { GMoney::Portfolio.delete("asdf") }.should raise_error(GMoney::Portfolio::PortfolioDeleteError, @gf_response.body)
136
+ end
96
137
 
97
138
  def portfolio_helper(url, id = nil, options = {})
98
139
  GMoney::GFSession.should_receive(:auth_token).and_return('toke')
@@ -110,4 +151,12 @@ describe GMoney::Portfolio do
110
151
 
111
152
  portfolios = id ? GMoney::Portfolio.find(id, options) : GMoney::Portfolio.all(options)
112
153
  end
154
+
155
+ def portfolio_delete_helper(url)
156
+ GMoney::GFSession.should_receive(:auth_token).and_return('toke')
157
+
158
+ GMoney::GFRequest.should_receive(:new).with(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=toke", "X-HTTP-Method-Override" => "DELETE"}).and_return(@gf_request)
159
+
160
+ GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
161
+ end
113
162
  end
@@ -28,8 +28,8 @@ describe GMoney::PositionFeedParser do
28
28
 
29
29
  it "should create Position objects with valid numeric data types for the returns" do
30
30
  @positions_with_returns.each do |position|
31
- position.public_methods(false).each do |pm|
32
- if !(['id', 'feed_link', 'updated', 'title', 'exchange', 'symbol', 'full_name', 'transactions'].include? pm) && !(pm.include?('='))
31
+ position.public_methods(false).each do |pm|
32
+ if (['shares', 'gain_percentage', 'return1w', 'return4w', 'return3m', 'return_ytd', 'return1y', 'return3y', 'return5y', 'return_overall', 'cost_basis', 'days_gain', 'gain', 'market_value'].include? pm) && !(pm.include?('='))
33
33
  return_val = position.send(pm)
34
34
  return_val.should be_instance_of(Float) if return_val
35
35
  end
@@ -10,6 +10,7 @@ describe GMoney::Position do
10
10
 
11
11
  before(:each) do
12
12
  @portfolio_id = '9'
13
+ @url = 'https://finance.google.com/finance/feeds/default/portfolios/9/positions'
13
14
 
14
15
  @gf_request = GMoney::GFRequest.new(@portfolio_id)
15
16
  @gf_request.method = :get
@@ -87,6 +88,51 @@ describe GMoney::Position do
87
88
  position.transactions(:eager => true).size.should be_eql(3)
88
89
  end
89
90
 
91
+
92
+ it "should delete positions (with a single transaction) using a class method and id" do
93
+ @trans = GMoney::Transaction.new
94
+ @trans.instance_variable_set("@id", "#{@url}/NASDAQ:GOOG/transactions/21")
95
+
96
+ position_delete_helper('9/NASDAQ:GOOG')
97
+
98
+ GMoney::Position.delete('9/NASDAQ:GOOG').should be_nil
99
+ end
100
+
101
+ it "should delete positions (with multiple transactions) using a class method and id" do
102
+ tran1 = GMoney::Transaction.new
103
+ tran1.instance_variable_set("@id", "#{@url}/NASDAQ:GOOG/transactions/21")
104
+ tran2 = GMoney::Transaction.new
105
+ tran2.instance_variable_set("@id", "#{@url}/NASDAQ:GOOG/transactions/22")
106
+ @trans = [tran1, tran2]
107
+
108
+ position_delete_helper('9/NASDAQ:GOOG')
109
+
110
+ GMoney::Position.delete('9/NASDAQ:GOOG').should be_nil
111
+ end
112
+
113
+ it "should delete positions when calling destroy on an instance of a position" do
114
+ position = GMoney::Position.new
115
+ position.instance_variable_set("@id", "#{@url}/NASDAQ:GOOG")
116
+
117
+ @trans = GMoney::Transaction.new
118
+ @trans.instance_variable_set("@id", "#{@url}/NASDAQ:GOOG/transactions/21")
119
+
120
+ position_delete_helper('9/NASDAQ:GOOG')
121
+
122
+ position_return = position.destroy
123
+ position_return.should be_eql(position)
124
+ position_return.frozen?.should be_true
125
+ end
126
+
127
+ it "should raise a PositionDeleteError when there is an attempt to delete a position with a bad position id')" do
128
+ lambda { GMoney::Position.delete("9/NASDAQ:GOOG/asdf") }.should raise_error(GMoney::Position::PositionDeleteError, 'Invalid Position ID')
129
+ end
130
+
131
+ it "should raise a PositionDeleteError when there is an attempt to delete a position with a bad position id')" do
132
+ GMoney::Transaction.should_receive(:find).with('9/NYSE:C').and_raise(GMoney::Transaction::TransactionRequestError.new('No position exists with ticker NYSE:C'))
133
+ lambda { GMoney::Position.delete("9/NYSE:C") }.should raise_error(GMoney::Position::PositionDeleteError, 'No position exists with ticker NYSE:C')
134
+ end
135
+
90
136
  def position_helper(id, options = {})
91
137
  GMoney::GFSession.should_receive(:auth_token).and_return('toke')
92
138
 
@@ -106,4 +152,14 @@ describe GMoney::Position do
106
152
 
107
153
  GMoney::Position.find(id, options)
108
154
  end
155
+
156
+ def position_delete_helper(id)
157
+ GMoney::Transaction.should_receive(:find).with(id).and_return(@trans)
158
+
159
+ if @trans.class == GMoney::Transaction
160
+ @trans.should_receive(:destroy)
161
+ else
162
+ @trans.each {|t| t.should_receive(:destroy)}
163
+ end
164
+ end
109
165
  end
@@ -42,10 +42,15 @@ describe String do
42
42
  "http://finance.google.com/finance/feeds/user@example.com/portfolios/9".portfolio_feed_id.should be_eql("9")
43
43
  end
44
44
 
45
- it "should be able to parse a position id from an position feed feed url" do
45
+ it "should be able to parse a position id from an position feed url" do
46
46
  "http://finance.google.com/finance/feeds/user@example.com/portfolios/9/positions/NASDAG:GOOG".position_feed_id.should be_eql("9/NASDAG:GOOG")
47
47
  end
48
48
 
49
+ it "should be able to parse a transaction id from an transaction feed url" do
50
+ "http://finance.google.com/finance/feeds/user@example.com/portfolios/9/positions/NASDAG:GOOG/transactions/12".transaction_feed_id.should be_eql("9/NASDAG:GOOG/12")
51
+ "http://finance.google.com/finance/feeds/user@example.com/portfolios/12/positions/NYSE:GLD/transactions/3".transaction_feed_id.should be_eql("12/NYSE:GLD/3")
52
+ end
53
+
49
54
  it "should be able to parse a portfolio id out of a string" do
50
55
  1.portfolio_id.should be_eql("1")
51
56
  "1".portfolio_id.should be_eql("1")
@@ -5,7 +5,6 @@ describe GMoney::TransactionFeedParser do
5
5
  feed = File.read('spec/fixtures/transactions_feed_for_GOOG.xml')
6
6
  @transactions = GMoney::TransactionFeedParser.parse_transaction_feed(feed)
7
7
  end
8
-
9
8
  it "should create Transaction objects out of transaction feeds" do
10
9
  @transactions.each do |transaction|
11
10
  transaction.should be_instance_of(GMoney::Transaction)
@@ -19,7 +18,7 @@ describe GMoney::TransactionFeedParser do
19
18
  it "should create Transaction objects with valid numeric data types" do
20
19
  @transactions.each do |transaction|
21
20
  transaction.public_methods(false).each do |pm|
22
- if !(['id', 'updated', 'title', 'date', 'type', 'notes'].include? pm) && !(pm.include?('='))
21
+ if (['shares', 'commission', 'price'].include? pm) && !(pm.include?('='))
23
22
  return_val = transaction.send(pm)
24
23
  return_val.should be_instance_of(Float) if return_val
25
24
  end
@@ -41,7 +41,49 @@ describe GMoney::Transaction do
41
41
 
42
42
  transaction.commission.should be_eql(50.0)
43
43
  transaction.price.should be_eql(400.0)
44
- end
44
+ end
45
+
46
+ it "should delete transactions using a class method and id" do
47
+ @gf_request = GMoney::GFRequest.new("#{@url}/24")
48
+ @gf_request.method = :post
49
+
50
+ @gf_response = GMoney::GFResponse.new
51
+ @gf_response.status_code = 200
52
+
53
+ transaction_delete_helper("#{@url}/24")
54
+
55
+ GMoney::Transaction.delete('9/NASDAQ:GOOG/24').should be_nil
56
+ end
57
+
58
+ it "should delete transactions when calling destroy on an instance of a transaction" do
59
+ @gf_request = GMoney::GFRequest.new("#{@url}/21")
60
+ @gf_request.method = :post
61
+
62
+ @gf_response = GMoney::GFResponse.new
63
+ @gf_response.status_code = 200
64
+
65
+ transaction = GMoney::Transaction.new
66
+ transaction.instance_variable_set("@id", "#{@url}/21")
67
+
68
+ transaction_delete_helper("#{@url}/21")
69
+
70
+ transaction_return = transaction.destroy
71
+ transaction_return.should be_eql(transaction)
72
+ transaction_return.frozen?.should be_true
73
+ end
74
+
75
+ it "should raise a TransactionDeleteError when there is an attempt to delete a transaction from a portfolio that doesn't exist')" do
76
+ @gf_request = GMoney::GFRequest.new("#{@url}/24")
77
+ @gf_request.method = :post
78
+
79
+ @gf_response = GMoney::GFResponse.new
80
+ @gf_response.status_code = 400
81
+ @gf_request.body = "Invalid Portfolio"
82
+
83
+ transaction_delete_helper("#{@url}/24")
84
+
85
+ lambda { GMoney::Transaction.delete("9/NASDAQ:GOOG/24") }.should raise_error(GMoney::Transaction::TransactionDeleteError, @gf_response.body)
86
+ end
45
87
 
46
88
  def transaction_helper(id, options={})
47
89
  GMoney::GFSession.should_receive(:auth_token).and_return('toke')
@@ -54,4 +96,12 @@ describe GMoney::Transaction do
54
96
 
55
97
  GMoney::Transaction.find(id, options)
56
98
  end
99
+
100
+ def transaction_delete_helper(url)
101
+ GMoney::GFSession.should_receive(:auth_token).and_return('toke')
102
+
103
+ GMoney::GFRequest.should_receive(:new).with(url, :method => :post, :headers => {"Authorization" => "GoogleLogin auth=toke", "X-HTTP-Method-Override" => "DELETE"}).and_return(@gf_request)
104
+
105
+ GMoney::GFService.should_receive(:send_request).with(@gf_request).and_return(@gf_response)
106
+ end
57
107
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gmoney
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Spradlin
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-10-01 00:00:00 -04:00
12
+ date: 2009-11-13 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies: []
15
15