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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d0f1d48b90a3fac473d2fc548626f619a1389814fe476db23bb0e12113ee84d
4
- data.tar.gz: 0a61cdc9750832111812de93441beafe29bef47059841e7ea41105589c673373
3
+ metadata.gz: 49293478c0827ccb4bdca1bb6c9974900f8c0c16853b247487120a0ed3bb79df
4
+ data.tar.gz: c9d8c93979377ed18d22a179bae44175961b44e8641eb091930f99542d1e610f
5
5
  SHA512:
6
- metadata.gz: 96b56e7e821e978b4ff48f9023d82901c4449eb820f3b0ab82e9faa187b8c7e547dbef679a396c6385202f713aeaf8c446e2d1338b46e2c09f32632ab6e04d6a
7
- data.tar.gz: c054fa510a18b072a4e98505d48d73fb9bde7c06e3294aaafe9fcf7c28b94cd0ffb0e51019ea5b6b6630783584bcbde5e49a4abfa0d9fecebb45567bcab79308
6
+ metadata.gz: 4464fe0a9aa0006f25c5eab29dab8422401ac2ebf435b712e65d5727e43606bb0df4fdf895e5a1d6a1e3e44d4937d19c42389577d28568351832ccae64d55ebb
7
+ data.tar.gz: 729d643455ddd8e7a827ddc6aea2a8512f93bc65a3e469993da5d2cd5085ecdc5eed4befee4f376cd9986f51a81d363fdeb042a1f8f51e2e51d7230f265c2d7a
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,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 "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,30 @@ 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
+
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 = "%Y-%m-%dT%H:%M:%S%Z"
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(s)
56
- s.partition(/(\w+)$/).first(2).map {|s| s.sub(/\.$/, '') }
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 :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
+ end
12
23
 
13
- @connection = Faraday.new(@endpoint) do |faraday|
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 @client_id, @client_secret
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', "Bearer")
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: client_id,
52
+ params = {
53
+ client_id: client_id,
42
54
  client_secret: client_secret,
43
- grant_type: grant_type,
44
- scope: 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