quovo 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.rubocop.yml +12 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE +21 -0
  8. data/README.md +176 -0
  9. data/Rakefile +12 -0
  10. data/bin/console +53 -0
  11. data/lib/quovo.rb +81 -0
  12. data/lib/quovo/api.rb +31 -0
  13. data/lib/quovo/api/accounts.rb +65 -0
  14. data/lib/quovo/api/base.rb +27 -0
  15. data/lib/quovo/api/brokerages.rb +21 -0
  16. data/lib/quovo/api/challenges.rb +28 -0
  17. data/lib/quovo/api/history.rb +36 -0
  18. data/lib/quovo/api/portfolios.rb +47 -0
  19. data/lib/quovo/api/positions.rb +30 -0
  20. data/lib/quovo/api/users.rb +44 -0
  21. data/lib/quovo/config.rb +61 -0
  22. data/lib/quovo/errors.rb +7 -0
  23. data/lib/quovo/fake.rb +29 -0
  24. data/lib/quovo/hook.rb +23 -0
  25. data/lib/quovo/models/account.rb +34 -0
  26. data/lib/quovo/models/base.rb +38 -0
  27. data/lib/quovo/models/brokerage.rb +15 -0
  28. data/lib/quovo/models/challenge.rb +34 -0
  29. data/lib/quovo/models/choice.rb +10 -0
  30. data/lib/quovo/models/image.rb +10 -0
  31. data/lib/quovo/models/portfolio.rb +50 -0
  32. data/lib/quovo/models/position.rb +36 -0
  33. data/lib/quovo/models/progress.rb +11 -0
  34. data/lib/quovo/models/sync.rb +20 -0
  35. data/lib/quovo/models/transaction.rb +37 -0
  36. data/lib/quovo/models/user.rb +14 -0
  37. data/lib/quovo/refinements/cast.rb +22 -0
  38. data/lib/quovo/refinements/compact.rb +11 -0
  39. data/lib/quovo/refinements/permit.rb +11 -0
  40. data/lib/quovo/refinements/require.rb +20 -0
  41. data/lib/quovo/refinements/sensitive.rb +38 -0
  42. data/lib/quovo/refinements/to_time.rb +18 -0
  43. data/lib/quovo/request.rb +114 -0
  44. data/lib/quovo/scope.rb +19 -0
  45. data/lib/quovo/token.rb +70 -0
  46. data/lib/quovo/version.rb +3 -0
  47. data/quovo.gemspec +19 -0
  48. metadata +93 -0
@@ -0,0 +1,34 @@
1
+ module Quovo
2
+ module Models
3
+ class Challenge < Base
4
+ using Quovo::Refinements::Cast
5
+
6
+ fields %i(
7
+ account
8
+ type
9
+ is_answered
10
+ last_asked
11
+ should_answer
12
+ question
13
+ image
14
+ choices
15
+ image_choices
16
+ )
17
+
18
+ undef :image
19
+ def image
20
+ @image.cast(Image)
21
+ end
22
+
23
+ undef :choices
24
+ def choices
25
+ @choices.cast(Choice)
26
+ end
27
+
28
+ undef :image_choices
29
+ def image_choices
30
+ @image_choices.cast(Image)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Quovo
2
+ module Models
3
+ class Choice < Base
4
+ fields %i(
5
+ category
6
+ value
7
+ )
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Quovo
2
+ module Models
3
+ class Image < Base
4
+ fields %i(
5
+ html
6
+ source
7
+ )
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ module Quovo
2
+ module Models
3
+ class Portfolio < Base
4
+ using Quovo::Refinements::ToTime
5
+ CATEGORIES = {
6
+ 'Investment' => %w(
7
+ 401a 401k 403b 457b 529
8
+ Brokerage\ Account
9
+ Education\ Savings\ Account
10
+ Health\ Savings\ Account
11
+ IRA
12
+ Non-Taxable\ Brokerage\ Account
13
+ Pension
14
+ Roth\ IRA Roth\ 401k Simple\ IRA SEP\ IRA
15
+ Thrift\ Savings\ Plan
16
+ Profit\ Sharing\ Plan
17
+ ),
18
+ 'Banking' => %w(Checking Credit\ Card Savings),
19
+ 'Insurance' => %w(Annuity Fixed\ Annuity Insurance Variable\ Annuity),
20
+ 'Loan' => %w(Loan Mortgage Student\ Loan),
21
+ 'Unknown' => %w(Unknown)
22
+ }.freeze
23
+
24
+ fields %i(
25
+ id
26
+ account
27
+ brokerage
28
+ brokerage_name
29
+ description
30
+ is_inactive
31
+ is_taxable
32
+ last_change
33
+ nickname
34
+ owner_type
35
+ portfolio_name
36
+ portfolio_type
37
+ portfolio_category
38
+ update_count
39
+ user
40
+ username
41
+ value
42
+ )
43
+
44
+ undef :last_change
45
+ def last_change
46
+ @last_change.to_time
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ module Quovo
2
+ module Models
3
+ class Position < Base
4
+ using Quovo::Refinements::ToTime
5
+
6
+ fields %i(
7
+ account
8
+ asset_class
9
+ cost_basis
10
+ cost_basis_type
11
+ currency
12
+ cusip
13
+ fxrate
14
+ id
15
+ last_purchase_date
16
+ market_code
17
+ portfolio
18
+ portfolio_name
19
+ price
20
+ quantity
21
+ sector
22
+ security_type
23
+ ticker
24
+ ticker_name
25
+ user
26
+ username
27
+ value
28
+ )
29
+
30
+ undef :last_purchase_date
31
+ def last_purchase_date
32
+ @last_purchase_date.to_time
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Quovo
2
+ module Models
3
+ class Progress < Base
4
+ fields %i(
5
+ message
6
+ percent
7
+ state
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Quovo
2
+ module Models
3
+ class Sync < Base
4
+ using Quovo::Refinements::Cast
5
+
6
+ fields %i(
7
+ account
8
+ has_realtime
9
+ config_instructions
10
+ progress
11
+ status
12
+ )
13
+
14
+ undef :progress
15
+ def progress
16
+ @progress.cast(Progress)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ module Quovo
2
+ module Models
3
+ class Transaction < Base
4
+ using Quovo::Refinements::ToTime
5
+
6
+ fields %i(
7
+ account
8
+ currency
9
+ cusip
10
+ date
11
+ expense_category
12
+ fees
13
+ fxrate
14
+ id
15
+ memo
16
+ portfolio
17
+ price
18
+ quantity
19
+ ticker
20
+ ticker_name
21
+ tran_category
22
+ tran_type
23
+ user
24
+ value
25
+ )
26
+
27
+ undef :date
28
+ def date
29
+ @date.to_time
30
+ end
31
+
32
+ def sort_key
33
+ [date, id]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module Quovo
2
+ module Models
3
+ class User < Base
4
+ fields %i(
5
+ id
6
+ username
7
+ email
8
+ name
9
+ phone
10
+ value
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ module Quovo
2
+ module Refinements
3
+ module Cast
4
+ refine Object do
5
+ def cast(model)
6
+ case self
7
+ when Hash
8
+ model.new(self)
9
+ when Array
10
+ map do |entry|
11
+ entry.cast(model)
12
+ end
13
+ when NilClass
14
+ nil
15
+ else
16
+ raise 'unknown source type for casting'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ module Quovo
2
+ module Refinements
3
+ module Compact
4
+ refine Hash do
5
+ def compact!
6
+ delete_if { |_, value| value.nil? }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Quovo
2
+ module Refinements
3
+ module Permit
4
+ refine Hash do
5
+ def permit!(*keys)
6
+ delete_if { |k, _| !keys.include?(k) }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ module Quovo
2
+ module Refinements
3
+ module Require
4
+ refine Object do
5
+ def require!(*keys, as: 'parameter')
6
+ case self
7
+ when Hash
8
+ missing = keys - self.keys
9
+ if missing.any?
10
+ raise Quovo::ParamsError, "Bad params list. Expected: #{keys}, actual #{self.keys}"
11
+ end
12
+ keys.each { |k| self[k].require!(as: k) }
13
+ when NilClass
14
+ raise Quovo::ParamsError, "#{as} cannot be nil!"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ module Quovo
2
+ module Refinements
3
+ module Sensitive
4
+ FILTER_KEYS = %w(username password question answer access_token choices questions).freeze
5
+ refine Object do
6
+ def strip_sensitive
7
+ case self
8
+ when Hash
9
+ {}.tap do |result|
10
+ each do |key, value|
11
+ result[key] = FILTER_KEYS.include?(key.to_s) ? '[FILTERED]' : value.strip_sensitive
12
+ end
13
+ end
14
+ when Array
15
+ block = -> (value) { value.strip_sensitive }
16
+ map(&block)
17
+ else
18
+ self
19
+ end
20
+ end
21
+
22
+ def sensitive?
23
+ case self
24
+ when Hash
25
+ each do |key, value|
26
+ return true if FILTER_KEYS.include?(key.to_s) || value.sensitive?
27
+ end
28
+ when Array
29
+ each do |value|
30
+ return true if value.sensitive?
31
+ end
32
+ end
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module Quovo
2
+ module Refinements
3
+ module ToTime
4
+ refine Object do
5
+ def to_time
6
+ case self
7
+ when String
8
+ Time.parse(self + ' UTC')
9
+ when NilClass
10
+ nil
11
+ else
12
+ raise "cannot convert #{inspect} to time"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,114 @@
1
+ module Quovo
2
+ module Request
3
+ using Quovo::Refinements::Sensitive
4
+
5
+ def request(method, path, params = {}, format = :plain, config = Quovo.config)
6
+ return fake_request(method, path, params, &Proc.new) if Quovo.fake?
7
+
8
+ request = build_http_request(config.endpoint, method, path, params)
9
+
10
+ yield(request) if block_given?
11
+
12
+ do_http_request(request, config.request_timeout, format) do |code, payload, elapsed|
13
+ Quovo.run_hooks!(
14
+ path,
15
+ method.to_s.upcase,
16
+ strip_sensitive(params, config),
17
+ code,
18
+ strip_sensitive(payload, config),
19
+ elapsed
20
+ )
21
+ payload
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def build_http_request(endpoint, method, path, params)
28
+ request = case method
29
+ when :get
30
+ Net::HTTP::Get
31
+ when :post
32
+ Net::HTTP::Post
33
+ when :put
34
+ Net::HTTP::Put
35
+ when :delete
36
+ Net::HTTP::Delete
37
+ else
38
+ raise Quovo::HttpError, 'unsupported method'
39
+ end.new(URI(endpoint + path))
40
+
41
+ inject_http_params(request, method, params) if params.any?
42
+ request
43
+ end
44
+
45
+ def http_transport(uri)
46
+ Net::HTTP.new(uri.host, uri.port)
47
+ end
48
+
49
+ private
50
+
51
+ def do_http_request(request, timeout, format)
52
+ http = http_transport(request.uri)
53
+ http.read_timeout = timeout
54
+ http.use_ssl = true
55
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
56
+
57
+ http.start do |transport|
58
+ (code, payload), elapsed = with_timing { parse_response(transport.request(request), format) }
59
+ yield(code, payload, elapsed)
60
+ end
61
+ end
62
+
63
+ def inject_http_params(request, method, params)
64
+ if method == :get
65
+ request.uri.query = URI.encode_www_form(params)
66
+ else
67
+ request.body = params.to_json
68
+ request['Content-Type'] = 'application/json'
69
+ end
70
+ end
71
+
72
+ def parse_response(response, format)
73
+ code = response.code
74
+ body = response.body
75
+ payload = format == :json ? JSON.parse(body) : body
76
+ raise Quovo::NotFoundError, body if code =~ /404/
77
+ raise Quovo::ForbiddenError, body if code =~ /403/
78
+ raise Quovo::HttpError, body if code =~ /^[45]/
79
+ [code, payload]
80
+ end
81
+
82
+ def with_timing
83
+ start_at = Time.now
84
+ result = yield
85
+ elapsed = (Time.now - start_at).round(3)
86
+ [result, elapsed]
87
+ end
88
+
89
+ def strip_sensitive(data, config)
90
+ config.strip_sensitive_params ? data.strip_sensitive : data
91
+ end
92
+
93
+ class FakeRequest
94
+ attr_reader :username, :password
95
+ def basic_auth(username, password)
96
+ @username = username
97
+ @password = password
98
+ end
99
+
100
+ def []=(_, __)
101
+ {}
102
+ end
103
+ end
104
+
105
+ def fake_request(method, path, params)
106
+ fake = Quovo.fake_calls.find do |fake_method, fake_path, fake_params, _|
107
+ fake_method == method && fake_path == path && (fake_params == params || fake_params == '*')
108
+ end
109
+ raise StubNotFoundError, [method, path, params] unless fake
110
+ yield(FakeRequest.new) if block_given?
111
+ fake.last
112
+ end
113
+ end
114
+ end