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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1ae4fd99eee1b55b80dd22982d070081a49be756e7495e49536bd887fc231c2
4
- data.tar.gz: c711e2c0c848fd96b3837d4dbdf1629e6d05a5bd49192f815a1c917c4b72dda4
3
+ metadata.gz: 91e46c54493b29b1afd7cf10e4193209a6f91692494f5d5f9b35c48e176f0e50
4
+ data.tar.gz: 5778db15cafacd613b1262c88434c4f484e508da600d62d7ef960ea3a4e6e1c5
5
5
  SHA512:
6
- metadata.gz: c745d382e1cfa925562d318736b8c154e81db511ec4456360e57358230edd118e46a421b249f7404087b4557fd7935371870e78aeb49b3731661fa6be57793a4
7
- data.tar.gz: c9affa40ce40bd80f90f3f2aed2128687ce4eb0b3b3092d6ad69aa569293f6afd8a64d7aaaebc70257de83b7d4452ad3ac7e52f35ce1fc1d819a1a270d30cbbe
6
+ metadata.gz: 140b63e766d92ec632abf864803dd7a6beffde76069b5f9cec4b7f37f0c659b437a15b3b47bbde9692cf84070276e5d189520fbb45c35c7edc8af7396ed86ff6
7
+ data.tar.gz: 17525cbd4fb045a43b5c14082fd076f7798ff7d3abede002e36efecb85a526d5ab4b245d77c2cd08dc191942b7cf0b572979339c51bdda518061c209f889155a
data/.gitignore CHANGED
@@ -13,5 +13,7 @@
13
13
  /log/**
14
14
  /test/vcr_cassettes/**
15
15
  md_cache/**
16
+ ds_cache/**
16
17
  .byebug_history
17
18
  secrets.yml
19
+ /.history/
data/.gitpod.yml ADDED
@@ -0,0 +1,2 @@
1
+ tasks:
2
+ - init: bundle install
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reso_transport (1.5.1)
4
+ reso_transport (1.5.5)
5
5
  faraday (~> 1.0.1)
6
6
 
7
7
  GEM
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 "bundler/setup"
4
- require "reso_transport"
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("log/console.log")
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("secrets.yml")
22
+ SECRETS = YAML.load_file('secrets.yml')
24
23
 
25
- @trestle = ResoTransport::Client.new(SECRETS[:trestle].merge(logger: Logger.new($stdout)))
26
- @bridge = ResoTransport::Client.new(SECRETS[:bridge].merge(logger: Logger.new($stdout)))
27
- # @spark = ResoTransport::Client.new(SECRETS[:spark])
28
- @crmls = ResoTransport::Client.new(SECRETS[:crmls])
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")
@@ -5,40 +5,28 @@ require 'faraday'
5
5
  require 'json'
6
6
  require 'time'
7
7
 
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_parser"
15
- require "reso_transport/schema"
16
- require "reso_transport/entity_set"
17
- require "reso_transport/entity_type"
18
- require "reso_transport/enum"
19
- require "reso_transport/property"
20
- require "reso_transport/query"
21
-
22
-
23
-
24
- # module Faraday
25
- # module Utils
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
- class Error < StandardError; end
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(s)
56
- s.partition(/(\w+)$/).first(2).map {|s| s.sub(/\.$/, '') }
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 :connection, :endpoint, :client_id, :client_secret, :grant_type, :scope
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
- @grant_type = options.fetch(:grant_type, "client_credentials")
8
- @scope = options.fetch(:scope, "api")
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
- @connection = Faraday.new(@endpoint) do |faraday|
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 @client_id, @client_secret
30
+ faraday.basic_auth client_id, client_secret
18
31
  end
19
32
  end
20
33
 
21
34
  def authenticate
22
- response = connection.post nil, auth_params
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', "Bearer")
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: client_id,
56
+ params = {
57
+ client_id: client_id,
42
58
  client_secret: client_secret,
43
- grant_type: grant_type,
44
- scope: 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