quovo 1.0.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.
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