reso_transport 1.5.1 → 1.5.7
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 +83 -7
- data/bin/console +10 -10
- data/bin/rake +29 -0
- data/lib/reso_transport.rb +22 -33
- data/lib/reso_transport/authentication/fetch_token_auth.rb +30 -11
- data/lib/reso_transport/base_metadata.rb +61 -0
- data/lib/reso_transport/client.rb +34 -19
- data/lib/reso_transport/datasystem.rb +22 -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/metadata.rb +12 -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 +43 -41
- data/lib/reso_transport/resource.rb +22 -9
- data/lib/reso_transport/version.rb +1 -1
- data/reso_transport.gemspec +20 -20
- metadata +21 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49293478c0827ccb4bdca1bb6c9974900f8c0c16853b247487120a0ed3bb79df
|
4
|
+
data.tar.gz: c9d8c93979377ed18d22a179bae44175961b44e8641eb091930f99542d1e610f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4464fe0a9aa0006f25c5eab29dab8422401ac2ebf435b712e65d5727e43606bb0df4fdf895e5a1d6a1e3e44d4937d19c42389577d28568351832ccae64d55ebb
|
7
|
+
data.tar.gz: 729d643455ddd8e7a827ddc6aea2a8512f93bc65a3e469993da5d2cd5085ecdc5eed4befee4f376cd9986f51a81d363fdeb042a1f8f51e2e51d7230f265c2d7a
|
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,7 +275,7 @@ 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
|
|
205
281
|
## Development
|
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,30 @@ 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
|
+
|
25
|
+
Faraday::Utils.default_space_encoding = '%20'
|
37
26
|
|
38
27
|
module ResoTransport
|
39
28
|
class Error < StandardError; end
|
29
|
+
|
40
30
|
class AccessDenied < StandardError; end
|
41
|
-
ODATA_TIME_FORMAT =
|
31
|
+
ODATA_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'.freeze
|
42
32
|
|
43
33
|
class << self
|
44
34
|
attr_writer :configuration
|
@@ -52,8 +42,7 @@ module ResoTransport
|
|
52
42
|
yield(configuration)
|
53
43
|
end
|
54
44
|
|
55
|
-
def self.split_schema_and_class_name(
|
56
|
-
|
45
|
+
def self.split_schema_and_class_name(text)
|
46
|
+
text.to_s.partition(/(\w+)$/).first(2).map { |s| s.sub(/\.$/, '') }
|
57
47
|
end
|
58
|
-
|
59
48
|
end
|
@@ -1,20 +1,32 @@
|
|
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
|
+
end
|
12
23
|
|
13
|
-
|
24
|
+
def connection
|
25
|
+
@connection ||= Faraday.new(@endpoint) do |faraday|
|
14
26
|
faraday.request :url_encoded
|
15
27
|
faraday.response :logger, ResoTransport.configuration.logger if ResoTransport.configuration.logger
|
16
28
|
faraday.adapter Faraday.default_adapter
|
17
|
-
faraday.basic_auth
|
29
|
+
faraday.basic_auth client_id, client_secret
|
18
30
|
end
|
19
31
|
end
|
20
32
|
|
@@ -30,19 +42,26 @@ module ResoTransport
|
|
30
42
|
Access.new({
|
31
43
|
access_token: json.fetch('access_token'),
|
32
44
|
expires_in: json.fetch('expires_in', 1 << (1.size * 8 - 2) - 1),
|
33
|
-
token_type: json.fetch('token_type',
|
45
|
+
token_type: json.fetch('token_type', 'Bearer')
|
34
46
|
})
|
35
47
|
end
|
36
48
|
|
37
49
|
private
|
38
50
|
|
39
51
|
def auth_params
|
40
|
-
{
|
41
|
-
client_id:
|
52
|
+
params = {
|
53
|
+
client_id: client_id,
|
42
54
|
client_secret: client_secret,
|
43
|
-
grant_type:
|
44
|
-
scope:
|
55
|
+
grant_type: grant_type,
|
56
|
+
scope: scope
|
45
57
|
}
|
58
|
+
|
59
|
+
if grant_type == 'password'
|
60
|
+
params[:username] = username
|
61
|
+
params[:password] = password
|
62
|
+
end
|
63
|
+
|
64
|
+
params
|
46
65
|
end
|
47
66
|
end
|
48
67
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ResoTransport
|
2
|
+
class BaseMetadata
|
3
|
+
MIME_TYPES = {
|
4
|
+
xml: 'application/xml',
|
5
|
+
json: 'application/json'
|
6
|
+
}.freeze
|
7
|
+
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def initialize(client)
|
11
|
+
@client = client
|
12
|
+
@prefix = nil
|
13
|
+
@classname = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def prefix
|
17
|
+
raise 'prefix not set' unless @prefix
|
18
|
+
|
19
|
+
@prefix
|
20
|
+
end
|
21
|
+
|
22
|
+
def classname
|
23
|
+
raise 'classname not set' unless @classname
|
24
|
+
|
25
|
+
@classname
|
26
|
+
end
|
27
|
+
|
28
|
+
def parser
|
29
|
+
@parser ||= Object::const_get("#{classname}Parser").new.parse(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def data
|
33
|
+
if cache_file
|
34
|
+
cache.read || cache.write(raw)
|
35
|
+
else
|
36
|
+
raw
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache
|
41
|
+
@cache ||= client.send("#{prefix}_cache").new(cache_file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache_file
|
45
|
+
@cache_file ||= client.send "#{prefix}_file"
|
46
|
+
end
|
47
|
+
|
48
|
+
def raw
|
49
|
+
if response.success?
|
50
|
+
response.body
|
51
|
+
else
|
52
|
+
puts response.body
|
53
|
+
raise "Error getting #{classname}!"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def response
|
58
|
+
raise 'Must implement response method'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|