gmoney 0.1.0 → 0.2.0

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.
@@ -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