quovo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|