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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +12 -0
- data/.travis.yml +6 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +176 -0
- data/Rakefile +12 -0
- data/bin/console +53 -0
- data/lib/quovo.rb +81 -0
- data/lib/quovo/api.rb +31 -0
- data/lib/quovo/api/accounts.rb +65 -0
- data/lib/quovo/api/base.rb +27 -0
- data/lib/quovo/api/brokerages.rb +21 -0
- data/lib/quovo/api/challenges.rb +28 -0
- data/lib/quovo/api/history.rb +36 -0
- data/lib/quovo/api/portfolios.rb +47 -0
- data/lib/quovo/api/positions.rb +30 -0
- data/lib/quovo/api/users.rb +44 -0
- data/lib/quovo/config.rb +61 -0
- data/lib/quovo/errors.rb +7 -0
- data/lib/quovo/fake.rb +29 -0
- data/lib/quovo/hook.rb +23 -0
- data/lib/quovo/models/account.rb +34 -0
- data/lib/quovo/models/base.rb +38 -0
- data/lib/quovo/models/brokerage.rb +15 -0
- data/lib/quovo/models/challenge.rb +34 -0
- data/lib/quovo/models/choice.rb +10 -0
- data/lib/quovo/models/image.rb +10 -0
- data/lib/quovo/models/portfolio.rb +50 -0
- data/lib/quovo/models/position.rb +36 -0
- data/lib/quovo/models/progress.rb +11 -0
- data/lib/quovo/models/sync.rb +20 -0
- data/lib/quovo/models/transaction.rb +37 -0
- data/lib/quovo/models/user.rb +14 -0
- data/lib/quovo/refinements/cast.rb +22 -0
- data/lib/quovo/refinements/compact.rb +11 -0
- data/lib/quovo/refinements/permit.rb +11 -0
- data/lib/quovo/refinements/require.rb +20 -0
- data/lib/quovo/refinements/sensitive.rb +38 -0
- data/lib/quovo/refinements/to_time.rb +18 -0
- data/lib/quovo/request.rb +114 -0
- data/lib/quovo/scope.rb +19 -0
- data/lib/quovo/token.rb +70 -0
- data/lib/quovo/version.rb +3 -0
- data/quovo.gemspec +19 -0
- 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,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,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,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,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
|