reso_transport 1.5.2 → 1.5.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.gitpod.yml +2 -0
- data/.rubocop.yml +33 -0
- data/Gemfile.lock +1 -1
- data/README.md +96 -7
- data/bin/console +10 -10
- data/bin/rake +29 -0
- data/lib/reso_transport.rb +22 -35
- data/lib/reso_transport/authentication/fetch_token_auth.rb +39 -16
- data/lib/reso_transport/authentication/middleware.rb +5 -4
- data/lib/reso_transport/base_metadata.rb +64 -0
- data/lib/reso_transport/client.rb +33 -16
- data/lib/reso_transport/datasystem.rb +25 -0
- data/lib/reso_transport/datasystem_parser.rb +26 -0
- data/lib/reso_transport/entity_set.rb +2 -4
- data/lib/reso_transport/entity_type.rb +11 -13
- data/lib/reso_transport/errors.rb +69 -0
- data/lib/reso_transport/metadata.rb +15 -32
- data/lib/reso_transport/metadata_cache.rb +20 -0
- data/lib/reso_transport/metadata_parser.rb +31 -22
- data/lib/reso_transport/query.rb +44 -52
- data/lib/reso_transport/resource.rb +28 -8
- data/lib/reso_transport/version.rb +1 -1
- data/reso_transport.gemspec +20 -20
- metadata +22 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91e46c54493b29b1afd7cf10e4193209a6f91692494f5d5f9b35c48e176f0e50
|
4
|
+
data.tar.gz: 5778db15cafacd613b1262c88434c4f484e508da600d62d7ef960ea3a4e6e1c5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 140b63e766d92ec632abf864803dd7a6beffde76069b5f9cec4b7f37f0c659b437a15b3b47bbde9692cf84070276e5d189520fbb45c35c7edc8af7396ed86ff6
|
7
|
+
data.tar.gz: 17525cbd4fb045a43b5c14082fd076f7798ff7d3abede002e36efecb85a526d5ab4b245d77c2cd08dc191942b7cf0b572979339c51bdda518061c209f889155a
|
data/.gitignore
CHANGED
data/.gitpod.yml
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.6
|
3
|
+
|
4
|
+
Layout/FirstHashElementIndentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Layout/MultilineMethodCallIndentation:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Metrics/AbcSize:
|
11
|
+
Max: 20
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Exclude:
|
15
|
+
- test/**/*
|
16
|
+
|
17
|
+
Metrics/ClassLength:
|
18
|
+
Max: 150
|
19
|
+
|
20
|
+
Metrics/MethodLength:
|
21
|
+
Max: 15
|
22
|
+
|
23
|
+
Metrics/ModuleLength:
|
24
|
+
Max: 150
|
25
|
+
|
26
|
+
Style/ColonMethodCall:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Style/Documentation:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Style/FrozenStringLiteralComment:
|
33
|
+
Enabled: false
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/wrstudios/reso_transport)
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/reso_transport.svg)](https://badge.fury.io/rb/reso_transport)
|
3
|
+
|
1
4
|
# ResoTransport
|
2
5
|
|
3
6
|
A Ruby gem for connecting to and interacting with RESO WebAPI services. Learn more about what that is by checking out the [RESO WebAPI](https://www.reso.org/reso-web-api/) Documentation.
|
@@ -51,7 +54,7 @@ Or you can set a logger for each specific instance of a client which can be usef
|
|
51
54
|
|
52
55
|
### Getting Connected
|
53
56
|
|
54
|
-
There are 2 strategies for authentication.
|
57
|
+
There are 2 strategies for authentication.
|
55
58
|
|
56
59
|
**Bearer Token**
|
57
60
|
|
@@ -60,7 +63,8 @@ It's simple to use a static access token if your token never expires:
|
|
60
63
|
```ruby
|
61
64
|
@client = ResoTransport::Client.new({
|
62
65
|
md_file: METADATA_CACHE,
|
63
|
-
endpoint: ENDPOINT_URL
|
66
|
+
endpoint: ENDPOINT_URL,
|
67
|
+
use_replication_endpoint: false # this is the default and can be ommitted
|
64
68
|
authentication: {
|
65
69
|
access_token: TOKEN,
|
66
70
|
token_type: "Bearer" # this is the default and can be ommitted
|
@@ -82,13 +86,72 @@ If the connection requires requesting a new token periodically, it's easy to pro
|
|
82
86
|
client_id: CLIENT_ID,
|
83
87
|
client_secret: CLIENT_SECRET,
|
84
88
|
grant_type: "client_credentials", # these are the default and can be ommitted
|
85
|
-
scope: "api"
|
89
|
+
scope: "api"
|
86
90
|
}
|
87
91
|
})
|
88
92
|
```
|
89
93
|
|
90
94
|
This will pre-fetch a token from the provided endpoint when the current token is either non-existent or has expired.
|
91
95
|
|
96
|
+
The `use_replication_endpoint` flag will append `/replication` to all resource queries if set to `true`. This is required
|
97
|
+
by some data sources to query resources beyond 10,000 records.
|
98
|
+
|
99
|
+
### Caching Metadata
|
100
|
+
|
101
|
+
The metadata file itself is large and parsing it is slow, so ResoTransport has built in support for caching the metadata to your file system. In the example above
|
102
|
+
you would replace `METADATA_CACHE` with a path to a file to store the metadata.
|
103
|
+
|
104
|
+
```
|
105
|
+
md_file: "reso_md_cache/#{@mls.name}",
|
106
|
+
```
|
107
|
+
|
108
|
+
This will store the metadata to a file with `@mls.name` in a folder named `reso_md_cache` in the relative root of your app.
|
109
|
+
|
110
|
+
**Customize your cache**
|
111
|
+
|
112
|
+
If you don't have access to the file system, like on Heroku, or you just don't want to store the metadata on the file system, you can provide your down metadata cache class.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class MyCacheStore < ResoTransport::MetadataCache
|
116
|
+
|
117
|
+
def read
|
118
|
+
# read `name` from somewhere
|
119
|
+
end
|
120
|
+
|
121
|
+
def write(data)
|
122
|
+
# write `name` with `data` somewhere
|
123
|
+
# return an IO instance
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
The metadata parser expects to recieve an IO instance so just make sure your `read` and `write` methods return one.
|
130
|
+
|
131
|
+
And you can instruct the client to use that cache store like so:
|
132
|
+
|
133
|
+
```
|
134
|
+
md_file: "reso_md_cache/#{@mls.name}",
|
135
|
+
md_cache: MyCacheStore
|
136
|
+
```
|
137
|
+
|
138
|
+
|
139
|
+
**Skip cache altogether**
|
140
|
+
|
141
|
+
Caching the metadata is not actually required, just be aware that it will be much slower. To skip caching just omit the related keys
|
142
|
+
when instantiating a new Client.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
@client = ResoTransport::Client.new({
|
146
|
+
endpoint: ENDPOINT_URL
|
147
|
+
authentication: {
|
148
|
+
endpoint: AUTH_ENDPOINT,
|
149
|
+
client_id: CLIENT_ID,
|
150
|
+
client_secret: CLIENT_SECRET,
|
151
|
+
}
|
152
|
+
})
|
153
|
+
```
|
154
|
+
|
92
155
|
|
93
156
|
### Resources
|
94
157
|
|
@@ -100,12 +163,25 @@ Once you have a successful connection you can explore what resources are availab
|
|
100
163
|
#=> {"Property"=>#<ResoTransport::Resource entity_set="Property", schema="ODataService">, "Office"=>#<ResoTransport::Resource entity_set="Office", schema="ODataService">, "Member"=>#<ResoTransport::Resource entity_set="Member", schema="ODataService">}
|
101
164
|
|
102
165
|
@client.resources["Property"]
|
103
|
-
#=> #<ResoTransport::Resource entity_set="Property", schema="ODataService">
|
166
|
+
#=> #<ResoTransport::Resource entity_set="Property", schema="ODataService">
|
104
167
|
|
105
168
|
@client.resources["Property"].query.limit(1).results
|
106
169
|
#=> Results Array
|
107
170
|
```
|
108
171
|
|
172
|
+
If the resource contains localizations you can access those as well.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
@client.resources["Property"].localizations
|
176
|
+
#=> {"CommercialSale"=>{"Name"=>"CommercialSale", "ResourcePath"=>"/Property?Class=CommercialSale", "Description"=>"Contains data for Commercial searches.", "DateTimeStamp"=>"2021-05-03T18:13:20.643-07:00"}, "Residential"=>{"Name"=>"Residential", "ResourcePath"=>"/Property?Class=Residential", "Description"=>"Contains data for Residential searches.", "DateTimeStamp"=>"2021-05-03T18:13:20.643-07:00"}}
|
177
|
+
```
|
178
|
+
|
179
|
+
If a resource contains localizations you must select one by name, before querying, like so:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
@client.resources["Property"].localization('Residential').query.limit(1).results
|
183
|
+
```
|
184
|
+
|
109
185
|
#### Querying
|
110
186
|
|
111
187
|
ResoTransport provides powerful querying capabilities:
|
@@ -138,7 +214,7 @@ To see what child records can be expanded look at `expandable`:
|
|
138
214
|
|
139
215
|
```ruby
|
140
216
|
@resource.expandable
|
141
|
-
#=> [#<struct ResoTransport::Property name="Media", data_type="Collection(RESO.Media)", attrs={"Name"=>"Media", "Type"=>"Collection(RESO.Media)"}, multi=true, enum=nil, complex_type=nil, entity_type=#<struct ResoTransport::EntityType name="Media", base_type=nil, primary_key="MediaKey", schema="CoreLogic.DataStandard.RESO.DD">> ...]
|
217
|
+
#=> [#<struct ResoTransport::Property name="Media", data_type="Collection(RESO.Media)", attrs={"Name"=>"Media", "Type"=>"Collection(RESO.Media)"}, multi=true, enum=nil, complex_type=nil, entity_type=#<struct ResoTransport::EntityType name="Media", base_type=nil, primary_key="MediaKey", schema="CoreLogic.DataStandard.RESO.DD">> ...]
|
142
218
|
```
|
143
219
|
|
144
220
|
Use `expand` to expand child records with the top level results.
|
@@ -152,7 +228,7 @@ You have several options to expand multiple child record sets. Each of these wil
|
|
152
228
|
|
153
229
|
```ruby
|
154
230
|
@resource.query.expand("Media", "Office").limit(10).results
|
155
|
-
|
231
|
+
|
156
232
|
@resource.query.expand(["Media", "Office"]).limit(10).results
|
157
233
|
|
158
234
|
@resource.query.expand("Media").expand("Office").limit(10).results
|
@@ -199,9 +275,22 @@ When querying for an enumeration value, you can provide either the system name,
|
|
199
275
|
|
200
276
|
```ruby
|
201
277
|
@resource.query.eq(StandardStatus: "Active Under Contract").limit(1).compile_params
|
202
|
-
#=> {"$top"=>1, "$filter"=>"StandardStatus eq 'ActiveUnderContract'"}
|
278
|
+
#=> {"$top"=>1, "$filter"=>"StandardStatus eq 'ActiveUnderContract'"}
|
203
279
|
```
|
204
280
|
|
281
|
+
### Troubleshooting
|
282
|
+
|
283
|
+
In the event there are connection issues, the following errors are raised:
|
284
|
+
|
285
|
+
* `ResoTransport::NoResponse` - The server did not respond to the request
|
286
|
+
* `ResoTransport::RequestError` - The server responded with a status code outside the 200 range
|
287
|
+
* `ResoTransport::ResponseError` - The server responded with errors in the body
|
288
|
+
* `ResoTransport::AccessDenied` - Check your authentication details
|
289
|
+
* `ResoTransport::LocalizationRequired` - Provide one of the required localizations through the `localization` method
|
290
|
+
* `ResoTransport::EncodeError` - No match was found for one or more of the properties
|
291
|
+
|
292
|
+
The Faraday Request hash is attached to the error for `NoResponse`, `RequestError`, `ResponseError`, and `AccessDenied`. A Faraday Response is attached on `RequestError`, `ResponseError`, and `AccessDenied`.
|
293
|
+
|
205
294
|
## Development
|
206
295
|
|
207
296
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/bin/console
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'reso_transport'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -12,19 +12,19 @@ require "reso_transport"
|
|
12
12
|
#
|
13
13
|
|
14
14
|
ResoTransport.configure do |c|
|
15
|
-
c.logger = Logger.new(
|
15
|
+
c.logger = Logger.new('log/console.log')
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
require "irb"
|
18
|
+
require 'irb'
|
20
19
|
require 'yaml'
|
21
20
|
require 'byebug'
|
22
21
|
|
23
|
-
SECRETS = YAML.load_file(
|
22
|
+
SECRETS = YAML.load_file('secrets.yml')
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
SECRETS.each do |name, data|
|
25
|
+
data[:logger] = Logger.new($stdout)
|
26
|
+
client = ResoTransport::Client.new(data)
|
27
|
+
instance_variable_set("@#{name}", client)
|
28
|
+
end
|
29
29
|
|
30
30
|
IRB.start(__FILE__)
|
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require "pathname"
|
12
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path("../bundle", __FILE__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
require "bundler/setup"
|
28
|
+
|
29
|
+
load Gem.bin_path("rake", "rake")
|
data/lib/reso_transport.rb
CHANGED
@@ -5,40 +5,28 @@ require 'faraday'
|
|
5
5
|
require 'json'
|
6
6
|
require 'time'
|
7
7
|
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require
|
20
|
-
require
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
# def escape(str)
|
28
|
-
# str.to_s.gsub(ESCAPE_RE) do |match|
|
29
|
-
# '%' + match.unpack('H2' * match.bytesize).join('%').upcase
|
30
|
-
# end.gsub(" ","%20")
|
31
|
-
|
32
|
-
# end
|
33
|
-
# end
|
34
|
-
# end
|
35
|
-
|
36
|
-
Faraday::Utils.default_space_encoding = "%20"
|
8
|
+
require 'reso_transport/version'
|
9
|
+
require 'reso_transport/configuration'
|
10
|
+
require 'reso_transport/authentication'
|
11
|
+
require 'reso_transport/client'
|
12
|
+
require 'reso_transport/resource'
|
13
|
+
require 'reso_transport/metadata'
|
14
|
+
require 'reso_transport/metadata_cache'
|
15
|
+
require 'reso_transport/metadata_parser'
|
16
|
+
require 'reso_transport/datasystem'
|
17
|
+
require 'reso_transport/datasystem_parser'
|
18
|
+
require 'reso_transport/schema'
|
19
|
+
require 'reso_transport/entity_set'
|
20
|
+
require 'reso_transport/entity_type'
|
21
|
+
require 'reso_transport/enum'
|
22
|
+
require 'reso_transport/property'
|
23
|
+
require 'reso_transport/query'
|
24
|
+
require 'reso_transport/errors'
|
25
|
+
|
26
|
+
Faraday::Utils.default_space_encoding = '%20'
|
37
27
|
|
38
28
|
module ResoTransport
|
39
|
-
|
40
|
-
class AccessDenied < StandardError; end
|
41
|
-
ODATA_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Z"
|
29
|
+
ODATA_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
|
42
30
|
|
43
31
|
class << self
|
44
32
|
attr_writer :configuration
|
@@ -52,8 +40,7 @@ module ResoTransport
|
|
52
40
|
yield(configuration)
|
53
41
|
end
|
54
42
|
|
55
|
-
def self.split_schema_and_class_name(
|
56
|
-
|
43
|
+
def self.split_schema_and_class_name(text)
|
44
|
+
text.to_s.partition(/(\w+)$/).first(2).map { |s| s.sub(/\.$/, '') }
|
57
45
|
end
|
58
|
-
|
59
46
|
end
|
@@ -1,48 +1,71 @@
|
|
1
1
|
module ResoTransport
|
2
2
|
module Authentication
|
3
3
|
class FetchTokenAuth < AuthStrategy
|
4
|
-
attr_reader :
|
5
|
-
|
4
|
+
attr_reader :endpoint,
|
5
|
+
:client_id,
|
6
|
+
:client_secret,
|
7
|
+
:grant_type,
|
8
|
+
:scope,
|
9
|
+
:username,
|
10
|
+
:password
|
11
|
+
|
6
12
|
def initialize(options)
|
7
|
-
|
8
|
-
|
13
|
+
super()
|
14
|
+
|
15
|
+
@grant_type = options.fetch(:grant_type, 'client_credentials')
|
16
|
+
@scope = options.fetch(:scope, 'api')
|
9
17
|
@client_id = options.fetch(:client_id)
|
10
18
|
@client_secret = options.fetch(:client_secret)
|
11
19
|
@endpoint = options.fetch(:endpoint)
|
20
|
+
@username = options.fetch(:username, nil)
|
21
|
+
@password = options.fetch(:password, nil)
|
22
|
+
@request = nil
|
23
|
+
end
|
12
24
|
|
13
|
-
|
25
|
+
def connection
|
26
|
+
@connection ||= Faraday.new(@endpoint) do |faraday|
|
14
27
|
faraday.request :url_encoded
|
15
28
|
faraday.response :logger, ResoTransport.configuration.logger if ResoTransport.configuration.logger
|
16
29
|
faraday.adapter Faraday.default_adapter
|
17
|
-
faraday.basic_auth
|
30
|
+
faraday.basic_auth client_id, client_secret
|
18
31
|
end
|
19
32
|
end
|
20
33
|
|
21
34
|
def authenticate
|
22
|
-
response = connection.post
|
35
|
+
response = connection.post(nil, auth_params { |req| @request = req })
|
23
36
|
json = JSON.parse response.body
|
24
37
|
|
25
|
-
unless response.success?
|
26
|
-
message = "#{response.reason_phrase}: #{json['error'] || response.body}"
|
27
|
-
raise ResoTransport::AccessDenied, response: response, message: message
|
28
|
-
end
|
38
|
+
raise AccessDenied.new(request, response, 'token') unless response.success?
|
29
39
|
|
30
40
|
Access.new({
|
31
41
|
access_token: json.fetch('access_token'),
|
32
42
|
expires_in: json.fetch('expires_in', 1 << (1.size * 8 - 2) - 1),
|
33
|
-
token_type: json.fetch('token_type',
|
43
|
+
token_type: json.fetch('token_type', 'Bearer')
|
34
44
|
})
|
35
45
|
end
|
36
46
|
|
47
|
+
def request
|
48
|
+
return @request.to_h if @request.respond_to? :to_h
|
49
|
+
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
|
37
53
|
private
|
38
54
|
|
39
55
|
def auth_params
|
40
|
-
{
|
41
|
-
client_id:
|
56
|
+
params = {
|
57
|
+
client_id: client_id,
|
42
58
|
client_secret: client_secret,
|
43
|
-
grant_type:
|
44
|
-
scope:
|
59
|
+
grant_type: grant_type,
|
60
|
+
scope: scope
|
45
61
|
}
|
62
|
+
|
63
|
+
if grant_type == 'password'
|
64
|
+
params[:username] = username
|
65
|
+
params[:password] = password
|
66
|
+
end
|
67
|
+
|
68
|
+
params
|
46
69
|
end
|
47
70
|
end
|
48
71
|
end
|