pricehubble 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +30 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +62 -0
- data/.simplecov +3 -0
- data/.travis.yml +27 -0
- data/.yardopts +6 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +9 -0
- data/Dockerfile +29 -0
- data/Envfile +6 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/Makefile +149 -0
- data/README.md +385 -0
- data/Rakefile +80 -0
- data/bin/console +16 -0
- data/bin/run +12 -0
- data/bin/setup +8 -0
- data/config/docker/.bash_profile +3 -0
- data/config/docker/.bashrc +48 -0
- data/config/docker/.inputrc +17 -0
- data/doc/assets/project.svg +68 -0
- data/doc/examples/authentication.rb +19 -0
- data/doc/examples/complex_property_valuations.rb +91 -0
- data/doc/examples/config.rb +30 -0
- data/doc/examples/property_valuations_errors.rb +30 -0
- data/doc/examples/simple_property_valuations.rb +69 -0
- data/docker-compose.yml +9 -0
- data/gemfiles/rails_4.2.gemfile +11 -0
- data/gemfiles/rails_5.0.gemfile +11 -0
- data/gemfiles/rails_5.1.gemfile +11 -0
- data/gemfiles/rails_5.2.gemfile +11 -0
- data/lib/price_hubble.rb +3 -0
- data/lib/pricehubble/client/authentication.rb +29 -0
- data/lib/pricehubble/client/base.rb +59 -0
- data/lib/pricehubble/client/request/data_sanitization.rb +28 -0
- data/lib/pricehubble/client/request/default_headers.rb +33 -0
- data/lib/pricehubble/client/response/data_sanitization.rb +29 -0
- data/lib/pricehubble/client/response/recursive_open_struct.rb +31 -0
- data/lib/pricehubble/client/utils/request.rb +41 -0
- data/lib/pricehubble/client/utils/response.rb +60 -0
- data/lib/pricehubble/client/valuation.rb +68 -0
- data/lib/pricehubble/client.rb +25 -0
- data/lib/pricehubble/configuration.rb +26 -0
- data/lib/pricehubble/configuration_handling.rb +50 -0
- data/lib/pricehubble/core_ext/hash.rb +52 -0
- data/lib/pricehubble/entity/address.rb +11 -0
- data/lib/pricehubble/entity/authentication.rb +34 -0
- data/lib/pricehubble/entity/base_entity.rb +63 -0
- data/lib/pricehubble/entity/concern/associations.rb +197 -0
- data/lib/pricehubble/entity/concern/attributes/date_array.rb +29 -0
- data/lib/pricehubble/entity/concern/attributes/enum.rb +57 -0
- data/lib/pricehubble/entity/concern/attributes/range.rb +32 -0
- data/lib/pricehubble/entity/concern/attributes/string_inquirer.rb +27 -0
- data/lib/pricehubble/entity/concern/attributes.rb +171 -0
- data/lib/pricehubble/entity/concern/callbacks.rb +19 -0
- data/lib/pricehubble/entity/concern/client.rb +31 -0
- data/lib/pricehubble/entity/coordinates.rb +11 -0
- data/lib/pricehubble/entity/location.rb +14 -0
- data/lib/pricehubble/entity/property.rb +32 -0
- data/lib/pricehubble/entity/property_conditions.rb +20 -0
- data/lib/pricehubble/entity/property_qualities.rb +20 -0
- data/lib/pricehubble/entity/property_type.rb +21 -0
- data/lib/pricehubble/entity/valuation.rb +48 -0
- data/lib/pricehubble/entity/valuation_request.rb +60 -0
- data/lib/pricehubble/entity/valuation_scores.rb +11 -0
- data/lib/pricehubble/errors.rb +60 -0
- data/lib/pricehubble/faraday.rb +12 -0
- data/lib/pricehubble/identity.rb +46 -0
- data/lib/pricehubble/railtie.rb +16 -0
- data/lib/pricehubble/utils/bangers.rb +44 -0
- data/lib/pricehubble/utils/decision.rb +97 -0
- data/lib/pricehubble/version.rb +6 -0
- data/lib/pricehubble.rb +103 -0
- data/pricehubble.gemspec +47 -0
- metadata +432 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative './config'
|
5
|
+
|
6
|
+
# Build a property with all relevant information for the valuation. This
|
7
|
+
# example reflects not the full list of supported fields, [see the API
|
8
|
+
# specification](https://docs.pricehubble.com/#types-property) for the full
|
9
|
+
# details.
|
10
|
+
apartment = PriceHubble::Property.new(
|
11
|
+
location: {
|
12
|
+
address: {
|
13
|
+
post_code: '22769',
|
14
|
+
city: 'Hamburg',
|
15
|
+
street: 'Stresemannstr.',
|
16
|
+
house_number: '29'
|
17
|
+
}
|
18
|
+
},
|
19
|
+
property_type: { code: :apartment },
|
20
|
+
building_year: 1990,
|
21
|
+
living_area: 200,
|
22
|
+
balcony_Area: 30,
|
23
|
+
floor_number: 5,
|
24
|
+
has_lift: true,
|
25
|
+
is_furnished: false,
|
26
|
+
is_new: false,
|
27
|
+
renovation_year: 2014,
|
28
|
+
condition: {
|
29
|
+
bathrooms: :well_maintained,
|
30
|
+
kitchen: :well_maintained,
|
31
|
+
flooring: :well_maintained,
|
32
|
+
windows: :well_maintained,
|
33
|
+
masonry: :well_maintained
|
34
|
+
},
|
35
|
+
quality: {
|
36
|
+
bathrooms: :normal,
|
37
|
+
kitchen: :normal,
|
38
|
+
flooring: :normal,
|
39
|
+
windows: :normal,
|
40
|
+
masonry: :normal
|
41
|
+
}
|
42
|
+
)
|
43
|
+
|
44
|
+
house = PriceHubble::Property.new(
|
45
|
+
location: {
|
46
|
+
address: {
|
47
|
+
post_code: '22769',
|
48
|
+
city: 'Hamburg',
|
49
|
+
street: 'Stresemannstr.',
|
50
|
+
house_number: '29'
|
51
|
+
}
|
52
|
+
},
|
53
|
+
property_type: { code: :house },
|
54
|
+
building_year: 1990,
|
55
|
+
land_area: 100,
|
56
|
+
living_area: 500,
|
57
|
+
number_of_floors_in_building: 5
|
58
|
+
)
|
59
|
+
|
60
|
+
# Fetch the property valuations for multiple properties, on multiple dates
|
61
|
+
request = PriceHubble::ValuationRequest.new(
|
62
|
+
deal_type: :sale,
|
63
|
+
properties: [apartment, house],
|
64
|
+
# The dates order is reflected on the valuations list
|
65
|
+
valuation_dates: [
|
66
|
+
1.year.ago,
|
67
|
+
Date.current,
|
68
|
+
1.year.from_now
|
69
|
+
]
|
70
|
+
)
|
71
|
+
valuations = request.perform!
|
72
|
+
|
73
|
+
# Print the valuations in a simple ASCII table. This is just a
|
74
|
+
# demonstration of how to use the valuations for a simple usecase.
|
75
|
+
require 'terminal-table'
|
76
|
+
table = Terminal::Table.new do |tab|
|
77
|
+
tab << ['Deal Type', 'Property Type', *request.valuation_dates.map(&:year)]
|
78
|
+
tab << :separator
|
79
|
+
# Group the valuations by the property they represent
|
80
|
+
valuations.group_by(&:property).each do |property, valuations|
|
81
|
+
tab << [request.deal_type, property.property_type.code,
|
82
|
+
*valuations.map { |val| "#{val.value} #{val.currency}" }]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
puts table
|
86
|
+
# => +-----------+---------------+-------------+-------------+-------------+
|
87
|
+
# => | Deal Type | Property Type | 2018 | 2019 | 2020 |
|
88
|
+
# => +-----------+---------------+-------------+-------------+-------------+
|
89
|
+
# => | sale | apartment | 1282100 EUR | 1373100 EUR | 1420100 EUR |
|
90
|
+
# => | sale | house | 1824800 EUR | 1950900 EUR | 2016000 EUR |
|
91
|
+
# => +-----------+---------------+-------------+-------------+-------------+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'pricehubble'
|
5
|
+
|
6
|
+
PriceHubble.reset_configuration!
|
7
|
+
PriceHubble.configure do |conf|
|
8
|
+
# conf.request_logging = true
|
9
|
+
end
|
10
|
+
|
11
|
+
errors = false
|
12
|
+
|
13
|
+
if ENV['PRICEHUBBLE_USERNAME'].blank?
|
14
|
+
errors = true
|
15
|
+
puts '> [ERR] Environment variable `PRICEHUBBLE_USERNAME` is missing.'
|
16
|
+
end
|
17
|
+
|
18
|
+
if ENV['PRICEHUBBLE_PASSWORD'].blank?
|
19
|
+
errors = true
|
20
|
+
puts '> [ERR] Environment variable `PRICEHUBBLE_PASSWORD` is missing.'
|
21
|
+
end
|
22
|
+
|
23
|
+
if errors
|
24
|
+
puts
|
25
|
+
puts '> Usage:'
|
26
|
+
puts "> $ export PRICEHUBBLE_USERNAME='your-username'"
|
27
|
+
puts "> $ export PRICEHUBBLE_PASSWORD='your-password'"
|
28
|
+
puts "> $ #{$PROGRAM_NAME} #{ARGV.join(' ')}"
|
29
|
+
exit
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative './config'
|
5
|
+
|
6
|
+
begin
|
7
|
+
# Fetch the valuations for a single property (sale) for today (defaults).
|
8
|
+
# This is the bare minimum variant, as a starting point.
|
9
|
+
PriceHubble::ValuationRequest.new(
|
10
|
+
property: {
|
11
|
+
location: {
|
12
|
+
address: {
|
13
|
+
post_code: '22769',
|
14
|
+
city: 'Hamburg',
|
15
|
+
street: 'Stresemannstr.',
|
16
|
+
house_number: '29'
|
17
|
+
}
|
18
|
+
},
|
19
|
+
property_type: { code: :apartment },
|
20
|
+
building_year: 2999,
|
21
|
+
living_area: 200
|
22
|
+
}
|
23
|
+
).perform!
|
24
|
+
rescue PriceHubble::EntityInvalid => e
|
25
|
+
# => #<PriceHubble::EntityInvalid: buildingYear: ...>
|
26
|
+
|
27
|
+
# The error message includes the detailed problem.
|
28
|
+
pp e.message
|
29
|
+
# => "buildingYear: Must be between 1850 and 2022."
|
30
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative './config'
|
5
|
+
|
6
|
+
# Fetch the valuations for a single property (sale) for today (defaults).
|
7
|
+
# This is the bare minimum variant, as a starting point.
|
8
|
+
valuations = PriceHubble::ValuationRequest.new(
|
9
|
+
property: {
|
10
|
+
location: {
|
11
|
+
address: {
|
12
|
+
post_code: '22769',
|
13
|
+
city: 'Hamburg',
|
14
|
+
street: 'Stresemannstr.',
|
15
|
+
house_number: '29'
|
16
|
+
}
|
17
|
+
},
|
18
|
+
property_type: { code: :apartment },
|
19
|
+
building_year: 1990,
|
20
|
+
living_area: 200
|
21
|
+
}
|
22
|
+
).perform!
|
23
|
+
# => [#<PriceHubble::Valuation ...>]
|
24
|
+
|
25
|
+
# Print all relevant valuation attributes
|
26
|
+
pp valuations.first.attributes.deep_compact
|
27
|
+
# => {"currency"=>"EUR",
|
28
|
+
# => "sale_price"=>1283400,
|
29
|
+
# => "sale_price_range"=>1180800..1386100,
|
30
|
+
# => "confidence"=>"good",
|
31
|
+
# => "deal_type"=>"sale",
|
32
|
+
# => "valuation_date"=>Thu, 17 Oct 2019,
|
33
|
+
# => "country_code"=>"DE",
|
34
|
+
# => "property"=>
|
35
|
+
# => {"location"=>
|
36
|
+
# => {"address"=>
|
37
|
+
# => {"post_code"=>"22769",
|
38
|
+
# => "city"=>"Hamburg",
|
39
|
+
# => "street"=>"Stresemannstr.",
|
40
|
+
# => "house_number"=>"29"}},
|
41
|
+
# => "property_type"=>{"code"=>:apartment},
|
42
|
+
# => "building_year"=>1990,
|
43
|
+
# => "living_area"=>200}}
|
44
|
+
|
45
|
+
# We just want to work on the first valuation
|
46
|
+
valuation = valuations.first
|
47
|
+
|
48
|
+
# Get the deal type dependent value of the property in a generic way.
|
49
|
+
# (sale price if deal type is sale, and rent gross if deal type is rent)
|
50
|
+
pp valuation.value
|
51
|
+
# => 1283400
|
52
|
+
|
53
|
+
# Get the upper and lower value range of the property in a generic
|
54
|
+
# way. The deal type logic is equal to the
|
55
|
+
# +PriceHubble::Valuation#value+ method.
|
56
|
+
pp valuation.value_range
|
57
|
+
# => 1180800..1386100
|
58
|
+
|
59
|
+
# Query the valuation confidence in an elegant way
|
60
|
+
pp valuation.confidence.good?
|
61
|
+
# => true
|
62
|
+
|
63
|
+
# The +PriceHubble::Valuation+ entity is a self contained
|
64
|
+
# representation of the request and response. This means it includes
|
65
|
+
# the property, deal type, valuation date, country code, etc it was
|
66
|
+
# requested for. This makes it easy to process the data because
|
67
|
+
# everything related is in one place.
|
68
|
+
pp valuation.property.property_type.apartment?
|
69
|
+
# => true
|
data/docker-compose.yml
ADDED
data/lib/price_hubble.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
# A high level client library for the PriceHubble Authentication API.
|
6
|
+
class Authentication < Base
|
7
|
+
# Perform a login request while sending the passed credentials.
|
8
|
+
#
|
9
|
+
# @param args [Hash{Symbol => Mixed}] the authentication credentials
|
10
|
+
# @return [PriceHubble::Authentication, nil] the authentication entity,
|
11
|
+
# or +nil+ on error
|
12
|
+
def login(**args)
|
13
|
+
res = connection.post do |req|
|
14
|
+
req.path = '/auth/login/credentials'
|
15
|
+
req.body = args.except(:bang)
|
16
|
+
use_default_context(req, :login)
|
17
|
+
end
|
18
|
+
decision(bang: args.fetch(:bang, false)) do |result|
|
19
|
+
result.bang { PriceHubble::AuthenticationError.new(nil, res) }
|
20
|
+
result.good { PriceHubble::Authentication.new(res.body) }
|
21
|
+
successful?(res)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generate bang method variants
|
26
|
+
bangers :login
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
# The base API client class definition. It bundles all the separated
|
6
|
+
# application logic on a low level.
|
7
|
+
class Base
|
8
|
+
include PriceHubble::Client::Utils::Request
|
9
|
+
include PriceHubble::Client::Utils::Response
|
10
|
+
include PriceHubble::Utils::Decision
|
11
|
+
include PriceHubble::Utils::Bangers
|
12
|
+
|
13
|
+
# Configure the connection instance in a generic manner. Each client can
|
14
|
+
# modify the connection in a specific way, when the application requires
|
15
|
+
# special handling. Just overwrite the +configure+ method, and call
|
16
|
+
# +super(con)+. Here is a full example:
|
17
|
+
#
|
18
|
+
# def configure(con)
|
19
|
+
# super(con)
|
20
|
+
# con.request :url_encoded
|
21
|
+
# con.response :logger
|
22
|
+
# con.adapter Faraday.default_adapter
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @param con [Faraday::Connection] the connection object
|
26
|
+
#
|
27
|
+
# rubocop:disable Metrics/MethodLength because of the middleware list
|
28
|
+
def configure(con)
|
29
|
+
con.use :instrumentation
|
30
|
+
|
31
|
+
# The definition order is execution order
|
32
|
+
con.request :ph_data_sanitization
|
33
|
+
con.request :ph_default_headers
|
34
|
+
con.request :json
|
35
|
+
con.request :multipart
|
36
|
+
con.request :url_encoded
|
37
|
+
|
38
|
+
# The definition order is reverse to the execution order
|
39
|
+
con.response :ph_recursive_open_struct
|
40
|
+
con.response :ph_data_sanitization
|
41
|
+
con.response :dates
|
42
|
+
con.response :json, content_type: /\bjson$/
|
43
|
+
con.response :follow_redirects
|
44
|
+
|
45
|
+
con.adapter Faraday.default_adapter
|
46
|
+
end
|
47
|
+
# rubocop:enable Metrics/MethodLength
|
48
|
+
|
49
|
+
# Create a new Faraday connection on the first shot, and pass the cached
|
50
|
+
# connection object on subsequent calls.
|
51
|
+
#
|
52
|
+
# @return [Faraday::Connection] the connection object
|
53
|
+
def connection
|
54
|
+
@connection ||= Faraday.new(url: PriceHubble.configuration.base_url,
|
55
|
+
&method(:configure))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Request
|
6
|
+
# We like snake cased attributes here, but the API requires camel cased
|
7
|
+
# hash keys, so we take care of it in a generic way here. Furthermore, we
|
8
|
+
# perform a deep compaction of the given body hash, to strip +nil+ values
|
9
|
+
# and empty hashes from the request. There is no need to send null-data
|
10
|
+
# on a round trip.
|
11
|
+
class DataSanitization < Faraday::Middleware
|
12
|
+
# Serve the Faraday middleware API.
|
13
|
+
#
|
14
|
+
# @param env [Hash{Symbol => Mixed}] the request
|
15
|
+
def call(env)
|
16
|
+
body = env[:body]
|
17
|
+
|
18
|
+
# Perform the data compaction and the hash key transformation,
|
19
|
+
# when the body is available and a hash
|
20
|
+
env[:body] = body.deep_compact.deep_camelize_keys \
|
21
|
+
if body&.is_a?(Hash)
|
22
|
+
|
23
|
+
@app.call(env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Request
|
6
|
+
# Add some default headers to every request.
|
7
|
+
class DefaultHeaders < Faraday::Middleware
|
8
|
+
# Serve the Faraday middleware API.
|
9
|
+
#
|
10
|
+
# @param env [Hash{Symbol => Mixed}] the request
|
11
|
+
def call(env)
|
12
|
+
env[:request_headers].merge! \
|
13
|
+
'User-Agent' => user_agent,
|
14
|
+
'Accept' => 'application/json'
|
15
|
+
|
16
|
+
# Set request content type to JSON as fallback
|
17
|
+
env[:request_headers]['Content-Type'] = 'application/json' \
|
18
|
+
if env[:request_headers]['Content-Type'].blank?
|
19
|
+
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Build an useful user agent string to pass. We identify ourself as
|
24
|
+
# +PriceHubbleGem+ with the current gem version.
|
25
|
+
#
|
26
|
+
# @return [String] the user agent string
|
27
|
+
def user_agent
|
28
|
+
"PriceHubbleGem/#{PriceHubble::VERSION}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Response
|
6
|
+
# We like snake cased attributes here, but the API sends camel cased
|
7
|
+
# hash keys on responses, so we take care of it in a generic way here.
|
8
|
+
class DataSanitization < Faraday::Middleware
|
9
|
+
# Serve the Faraday middleware API.
|
10
|
+
#
|
11
|
+
# @param env [Hash{Symbol => Mixed}] the request
|
12
|
+
def call(env)
|
13
|
+
@app.call(env).on_complete do |res|
|
14
|
+
body = res[:body]
|
15
|
+
|
16
|
+
# Skip string bodies, they are unparsed or contain binary data
|
17
|
+
next if body.is_a? String
|
18
|
+
|
19
|
+
# Skip non-hash bodies, we cannot handle them here
|
20
|
+
next unless body.is_a? Hash
|
21
|
+
|
22
|
+
# Perform the key transformation
|
23
|
+
res[:body] = body.deep_underscore_keys
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Response
|
6
|
+
# Convert every response body to an +RecursiveOpenStruct+ for an easy
|
7
|
+
# access.
|
8
|
+
class RecursiveOpenStruct < Faraday::Middleware
|
9
|
+
# Serve the Faraday middleware API.
|
10
|
+
#
|
11
|
+
# @param env [Hash{Symbol => Mixed}] the request
|
12
|
+
def call(env)
|
13
|
+
@app.call(env).on_complete do |res|
|
14
|
+
body = res[:body]
|
15
|
+
|
16
|
+
# Skip string bodies, they are unparsed or contain binary data
|
17
|
+
next if body.is_a? String
|
18
|
+
|
19
|
+
# By definition empty responses (HTTP status 204)
|
20
|
+
# or actual empty bodies should be an empty hash
|
21
|
+
body = {} if res[:status] == 204 || res[:body].empty?
|
22
|
+
|
23
|
+
# Looks like we have some actual data we can wrap
|
24
|
+
res[:body] = \
|
25
|
+
::RecursiveOpenStruct.new(body, recurse_over_arrays: true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Utils
|
6
|
+
# Some helpers to prepare requests in a general way.
|
7
|
+
#
|
8
|
+
module Request
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
# A common HTTP content type to symbol
|
13
|
+
# mapping for correct header settings.
|
14
|
+
CONTENT_TYPE = {
|
15
|
+
json: 'application/json',
|
16
|
+
multipart: 'multipart/form-data',
|
17
|
+
url_encoded: 'application/x-www-form-urlencoded'
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# Use the configured identity to authenticate the given request.
|
21
|
+
#
|
22
|
+
# @param req [Faraday::Request] the request to manipulate
|
23
|
+
def use_authentication(req)
|
24
|
+
req.params.merge!(access_token: PriceHubble.identity.access_token)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Use the default request context to identificate the request.
|
28
|
+
#
|
29
|
+
# @param action [String, Symbol] the used client action
|
30
|
+
# @param req [Faraday::Request] the request to manipulate
|
31
|
+
def use_default_context(req, action)
|
32
|
+
req.options.context ||= {}
|
33
|
+
req.options.context.merge!(client: self.class,
|
34
|
+
action: action,
|
35
|
+
request_id: SecureRandom.hex(3))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
module Utils
|
6
|
+
# Some helpers to work with responses in a general way.
|
7
|
+
module Response
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Simple helper to query the response status.
|
12
|
+
#
|
13
|
+
# @param res [Faraday::response] the response object
|
14
|
+
# @param code [Range, Array, Integer] the range of good
|
15
|
+
# response codes
|
16
|
+
# @return [Boolean] whenever the request got an allowed status
|
17
|
+
def status?(res, code: 0..399)
|
18
|
+
code = [code] unless code.is_a? Range
|
19
|
+
code = code.flatten if code.is_a? Array
|
20
|
+
code.include? res.status
|
21
|
+
end
|
22
|
+
alias_method :successful?, :status?
|
23
|
+
|
24
|
+
# A simple syntactic sugar helper to query the response status.
|
25
|
+
#
|
26
|
+
# @param res [Faraday::response] the response object
|
27
|
+
# @param code [Range] the range of failed response codes
|
28
|
+
# @return [Boolean] whenever the request failed
|
29
|
+
def failed?(res, code: 400..600)
|
30
|
+
status?(res, code: code)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Perform a common error handling for entity responses. This allows a
|
34
|
+
# clean usage of the decision flow control. Here comes an example:
|
35
|
+
#
|
36
|
+
# decision do |result|
|
37
|
+
# result.bang(&bang_entity(entity, res, id: entity.id))
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @param entity [PriceHubble::BaseEntity, Class] the result entity
|
41
|
+
# @param res [Faraday::Response] the response object
|
42
|
+
# @param data [Hash{Mixed => Mixed}] the request data
|
43
|
+
# @return [Proc] the proc which performs the error handling
|
44
|
+
def bang_entity(entity, res, data)
|
45
|
+
class_name = entity
|
46
|
+
class_name = entity.class unless entity.is_a? Class
|
47
|
+
lambda do
|
48
|
+
next PriceHubble::EntityNotFound.new(nil, class_name, data) \
|
49
|
+
if res.status == 404
|
50
|
+
next PriceHubble::EntityInvalid.new(res.body.message, entity) \
|
51
|
+
if res.status == 400
|
52
|
+
|
53
|
+
PriceHubble::RequestError.new(nil, res)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PriceHubble
|
4
|
+
module Client
|
5
|
+
# A high level client library for the PriceHubble Valuation API.
|
6
|
+
class Valuation < Base
|
7
|
+
# Perform a full-fledged valuation request.
|
8
|
+
#
|
9
|
+
# @param request [PriceHubble::ValuationRequest] the valuation request
|
10
|
+
# @param args [Hash{Symbol => Mixed}] the authentication credentials
|
11
|
+
# @return [Array<PriceHubble::Valuation>, nil] the valuation results,
|
12
|
+
# or +nil+ on error
|
13
|
+
#
|
14
|
+
# rubocop:disable Metrics/MethodLength because of the request handling
|
15
|
+
def property_value(request, **args)
|
16
|
+
data = request.attributes(true)
|
17
|
+
res = connection.post do |req|
|
18
|
+
req.path = '/api/v1/valuation/property_value'
|
19
|
+
req.body = data
|
20
|
+
use_default_context(req, :property_value)
|
21
|
+
use_authentication(req)
|
22
|
+
end
|
23
|
+
decision(bang: args.fetch(:bang, false)) do |result|
|
24
|
+
result.bang(&bang_entity(request, res, data))
|
25
|
+
result.good(&assign_valuations(res.body.valuations, request))
|
26
|
+
successful?(res)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
# rubocop:enable Metrics/MethodLength
|
30
|
+
|
31
|
+
# Map and assign the valuation response to our local
|
32
|
+
# +PriceHubble::Valuation+ representation. While taking care of the
|
33
|
+
# multi-dimensional array structure which is reflected from the request
|
34
|
+
# data. You get back a lambda which you need to call to get the results
|
35
|
+
# (for use in a decision). The return values is a
|
36
|
+
# +Array<PriceHubble::Valuation>+.
|
37
|
+
#
|
38
|
+
# @param data [Array<Array<RecursiveOpenStruct>>] the raw response
|
39
|
+
# valuations data
|
40
|
+
# @param request [PriceHubble::ValuationRequest] the original request
|
41
|
+
# @return [Proc] the valuation mapping code
|
42
|
+
#
|
43
|
+
# rubocop:disable Metrics/MethodLength because of the request
|
44
|
+
# to response mapping
|
45
|
+
def assign_valuations(data, request)
|
46
|
+
lambda do
|
47
|
+
# valuations[i][j] contains the valuation for property i on date j
|
48
|
+
data.each_with_index.map do |valuations, property_idx|
|
49
|
+
valuations.each_with_index.map do |valuation, date_idx|
|
50
|
+
# Fetch the request data for this valuation and
|
51
|
+
# extend the raw data to build a local representation
|
52
|
+
valuation.property = request.properties[property_idx]
|
53
|
+
valuation.valuation_date = request.valuation_dates[date_idx]
|
54
|
+
valuation.deal_type = request.deal_type
|
55
|
+
valuation.country_code = request.country_code
|
56
|
+
# Build the local representation from the raw data
|
57
|
+
PriceHubble::Valuation.new(valuation)
|
58
|
+
end
|
59
|
+
end.flatten
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# rubocop:enable Metrics/MethodLength
|
63
|
+
|
64
|
+
# Generate bang method variants
|
65
|
+
bangers :property_value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|