wku-ruby-rets 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7334380eaedc3dd2482334b030f6c3f9c561351f
4
+ data.tar.gz: 2d959fe4c4c2077cd73f4593ac89131d81fa45a4
5
+ SHA512:
6
+ metadata.gz: 802e0d0fe5d4b17305bea448083ea16fde66ed90ab4d92a3381ae30b1a3dbb22afe9484bc5fdf6bd01c0bce9834505b70292fd56722a4dc769c787890f5b47ed
7
+ data.tar.gz: ea43b713311a72b218819f1ea93ffa89af2ebb5eabd5c224f46143e232e84be310878b1e10272269b94af2f93893bbfe9ce57b893e55db954bb24ed8e8ec63d1
data/CHANGELOG.md ADDED
@@ -0,0 +1,104 @@
1
+ # Overview
2
+
3
+ ## 2.0.8 [In Progress]
4
+
5
+ ### Features
6
+ * Added support for using a HTTP Proxy
7
+ * Streaming can be disabled for `get_metadata` calls
8
+
9
+ ### Fixes
10
+ * Disabled gzip while streaming is enabled to fix parsing issues, it's still enabled when streaming is disabled (`:disable_stream => true`)
11
+ * No longer errors if Set-Cookie is returned with an invalid value (RAPRETS)
12
+
13
+ ## 2.0.7
14
+
15
+ Confirmed to work with Ruby 2.0.0-p0
16
+
17
+ ### Fixes
18
+ * `RETS-Version` is no longer set to `RETS/1.7` unless an error `20037` is returned and no other one is specified
19
+ * `RETS-Version` can be set based on a HTTP 200 response for systems which only pass it after authentication (Innovia)
20
+
21
+ ## 2.0.6
22
+
23
+ ### Features
24
+ * System name added to `rets_data[:system_id]` when pulling metadata out
25
+ * `client.request_size` and `client.request_hash` can be used when streaming is disabled in `client.search`
26
+ * Added `client.request_time` which gives the time taken in seconds for a `client.search` or `client.get_object` to run
27
+
28
+ ### Fixes
29
+ * Fixed an authentication issue when using Retsiq where it would return 20041 in place of HTTP 401
30
+ * Fixed errors when streaming with an unknown Content-Length and chunked encoding
31
+ * Fixed a block being required when making a `client.search` call with `:count_mode => :only`
32
+ * Fixed character encoding that was causing issues on some RETS servers (Paul Trippett)
33
+ * Fixed an exception when Rapattoni RETS servers returned a "RETS-STATUS" XML tag on a multipart `client.get_object` call
34
+ * Fixed a `RETS::APIError` code 0 error under some RETS systems when making a call to `client.get_object` with `:location`
35
+
36
+ ## 2.0.5
37
+
38
+ ### Features
39
+ * Make `client.rets_data` available immediately when calling `client.search` rather than having to wait until it finishes (Paul Trippett)
40
+ * `:rets_version` can be passed to `client.login` without the User-Agent fields being set, for RETS servers that require the version to be passed initially
41
+
42
+ ### Fixes
43
+ * Default to HTTP Digest authentication when Digest and Basic are provided (Paul Trippett)
44
+ * Fixed authentication header parser if a server returns Basic/Digest and the Digest data goes stale (Paul Trippett)
45
+ * Fixed query strings from the initial login request not being saved when discovery service URLs (Paul Trippett)
46
+
47
+ ## 2.0.4
48
+
49
+ ### Features
50
+ * Added support for RETS servers that use digest authentication without the quality of protection flag (MRIS)
51
+ * Added SSL support (Paul Trippett)
52
+
53
+ ### Fixes
54
+ * Fixed metadata parsing breaking if a field wasn't filled out (Paul Trippett)
55
+ * Fixed multipart parsing for `client.get_object` if a part is blank
56
+
57
+ ## 2.0.3
58
+
59
+ ### Fixes
60
+ * Fixed a stack overflow due to how Interealty handles User-Agent authentication errors
61
+
62
+ ## 2.0.2
63
+
64
+ ### Features
65
+ * Dropped support for TimeoutSeconds, instead if an HTTP 401 is received after a successful request then a reauthentication is forced. Provides better compatibility with how some RETS implementations handle sessions
66
+
67
+ ### Fixes
68
+ * Client methods no longer return the HTTP request
69
+ * Requests will correctly be called after a HTTP digest becomes stale
70
+
71
+ ## 2.0.1
72
+
73
+ ### API Changes
74
+ * `client.login` will now raise `ResponseError` errors if the RETS tag cannot be found in the response
75
+ * `client.login` added the ability to pass `:rets_version` to force the RETS Version used in HTTP requests. Provides a small speedup as it can skip one HTTP request depending on the RETS implementation
76
+ * `client.get_object` can return both Content-Description or Description rather than just Description. Also will return Preferred
77
+
78
+ ### Features
79
+ * Added support for TimeoutSeconds, after the timeout passes the gem seamlessly reauthenticates
80
+ * Improved the edge case handling for authentication requests to greatly increase compatability with logging into any RETS based system
81
+
82
+ ### Fixes
83
+ * Object multipart parsing no longer fails if the boundary is wrapped in quotes
84
+ * Response parsing won't fail if the RETS server uses odd casing for the "ReplyText" and "ReplyCode" args in RETS
85
+
86
+ ## 2.0.0
87
+
88
+ ### API Changes
89
+ * `client.logout` will now raise `CapabilityNotFound` errors if it's unsupported
90
+ * `client.get_object` now requires a block which is yielded to rather than returning an array of the content
91
+ * `client.get_object` headers are now returned in lowercase form ("content-id" not "Content-ID" and so on)
92
+ * `RETS::Client.login` now uses `:useragent => {:name => "Foo", :password => "Bar"}` to pass User Agent data
93
+ * `RETS::Client.login` no longer implies the User-Agent username or password by the primary username and password
94
+
95
+ ### Features
96
+ * Added support for Count, Offset, Select and RestrictedIndicators in `client.search`
97
+ * Added support for Location in `client.get_object`
98
+ * RETS reply code, text and other data such as count or delimiter can be gotten through `client.rets_data` after the call is finished
99
+
100
+ ### Fixes
101
+ * Redid how authentication is handled, no longer implies HTTP Basic auth when using RETS-UA-Authorization
102
+ * RETS-Version is now used for RETS-UA-Authorization when available with "RETS/1.7" as a fallback
103
+ * Exceptions are now raised consistently and have been simplifed to `APIError`, `HTTPError`, `Unauthorized` and `CapabilityNotFound`
104
+ * `HTTPError` and `APIError` now include the reply text and code in `reply_code` and `reply_text`
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Placester, Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ Overview
2
+ ===
3
+ Simplifies the process of dealing with RETS servers. Without having to worry about the various authentication methods or edge cases associated with dealing with RETS. Should work against all 1.x implementations.
4
+
5
+ Compability
6
+ -
7
+ Tested against Ruby 1.9.3, 2.0.0, RBX and JRuby, build history is available [here](http://travis-ci.org/Placester/ruby-rets).
8
+
9
+ <img src="https://secure.travis-ci.org/Placester/ruby-rets.png?branch=master&.png"/>
10
+
11
+ Documentation
12
+ -
13
+ See http://rubydoc.info/github/Placester/ruby-rets/master/frames for full documentation.
14
+
15
+ Examples
16
+ -
17
+
18
+ client = RETS::Client.login(:url => "http://foobar.com/rets/Login", :username => "foo", :password => "bar")
19
+ client.search(:search_type => :Property, :class => :RES, :query => "(ListPrice=50000-)") do |data|
20
+ # RETS data in key/value format, as COMPACT-DECODED
21
+ end
22
+
23
+ client.get_object(:resource => :Property, :type => :Photo, :location => false, :id => "1:0:*") do |headers, content|
24
+ puts "Object-ID #{headers"object-id"]}, Content-ID #{headers["content-id"]}, Description #{["description"]}"
25
+ puts "Data"
26
+ puts content
27
+ end
28
+
29
+ VCR / WebMock
30
+ -
31
+ Due to the streaming parser, the search features won't work with a library like VCR or Ephemeral Response. For WebMock, you can use the below patch to enable support for saving the HTTP requests to speed up your own tests.
32
+
33
+ module Net
34
+ module WebMockHTTPResponse
35
+ def self.extended(response)
36
+ response.instance_variable_set(:@socket, StringIO.new(response.body))
37
+ end
38
+ end
39
+ end
40
+
41
+ License
42
+ -
43
+ Licensed under MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "rake"
5
+ require "rspec"
6
+ require "rspec/core/rake_task"
7
+
8
+ RSpec::Core::RakeTask.new("spec") do |spec|
9
+ spec.pattern = "spec/**/*_spec.rb"
10
+ end
11
+
12
+ task :default => :spec
@@ -0,0 +1,303 @@
1
+ # For more information on what the possible values of fields that are passed to the RETS server can be, see {http://www.rets.org/documentation}.
2
+ module RETS
3
+ module Base
4
+ class Core
5
+ GET_OBJECT_DATA = ["object-id", "description", "content-id", "content-description", "location", "content-type", "preferred"]
6
+
7
+ # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
8
+ # @return [String] How big the request was
9
+ attr_reader :request_size
10
+
11
+ # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
12
+ # @return [String] SHA1 hash of the request
13
+ attr_reader :request_hash
14
+
15
+ # Can be called after any {#get_object} or {#search} call that hits the RETS Server.
16
+ # @return [Float] How long the request took
17
+ attr_reader :request_time
18
+
19
+ # Can be called after any {RETS::Base::Core} call that hits the RETS Server.
20
+ # @return [Hash]
21
+ # Gives access to the miscellaneous RETS data, such as reply text, code, delimiter, count and so on depending on the API call made.
22
+ # * *text* (String) - Reply text from the server
23
+ # * *code* (String) - Reply code from the server
24
+ attr_reader :rets_data
25
+
26
+ def initialize(http, urls)
27
+ @http = http
28
+ @urls = urls
29
+ end
30
+
31
+ ##
32
+ # Attempts to logout of the RETS server.
33
+ #
34
+ # @raise [RETS::CapabilityNotFound]
35
+ # @raise [RETS::APIError]
36
+ # @raise [RETS::HTTPError]
37
+ def logout
38
+ unless @urls[:logout]
39
+ raise RETS::CapabilityNotFound.new("No Logout capability found for given user.")
40
+ end
41
+
42
+ @http.request(:url => @urls[:logout])
43
+
44
+ nil
45
+ end
46
+
47
+ ##
48
+ # Whether the RETS server has the requested capability.
49
+ #
50
+ # @param [Symbol] type Lowercase of the capability, "getmetadata", "getobject" and so on
51
+ # @return [Boolean]
52
+ def has_capability?(type)
53
+ @urls.has_key?(type)
54
+ end
55
+
56
+ ##
57
+ # Requests metadata from the RETS server.
58
+ #
59
+ # @param [Hash] args
60
+ # @option args [String] :type Metadata to request, the same value if you were manually making the request, "METADATA-SYSTEM", "METADATA-CLASS" and so on
61
+ # @option args [String] :id Filter the data returned by ID, "*" would return all available data
62
+ # @option args [Integer, Optional] :read_timeout How many seconds to wait before giving up
63
+ # @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
64
+ #
65
+ # @yield For every group of metadata downloaded
66
+ # @yieldparam [String] :type Type of data that was parsed with "METADATA-" stripped out, for "METADATA-SYSTEM" this will be "SYSTEM"
67
+ # @yieldparam [Hash] :attrs Attributes of the data, generally *Version*, *Date* and *Resource* but can vary depending on what metadata you requested
68
+ # @yieldparam [Array] :metadata Array of hashes with metadata info
69
+ #
70
+ # @raise [RETS::CapabilityNotFound]
71
+ # @raise [RETS::APIError]
72
+ # @raise [RETS::HTTPError]
73
+ # @see #rets_data
74
+ # @see #request_size
75
+ # @see #request_hash
76
+ def get_metadata(args, &block)
77
+ raise ArgumentError, "No block passed" unless block_given?
78
+
79
+ unless @urls[:getmetadata]
80
+ raise RETS::CapabilityNotFound.new("No GetMetadata capability found for given user.")
81
+ end
82
+
83
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
84
+
85
+ start = Time.now.utc.to_f
86
+ @http.request(:url => @urls[:getmetadata], :read_timeout => args[:read_timeout], :disable_compression => !args[:disable_stream], :params => {:Format => :COMPACT, :Type => args[:type], :ID => args[:id]}) do |response|
87
+ if args[:disable_stream]
88
+ stream = StringIO.new(response.body)
89
+ @request_time = Time.now.utc.to_f - start
90
+ else
91
+ stream = RETS::StreamHTTP.new(response)
92
+ end
93
+
94
+ sax = RETS::Base::SAXMetadata.new(block)
95
+ Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
96
+
97
+ if args[:disable_stream]
98
+ @request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
99
+ else
100
+ @request_size, @request_hash = stream.size, stream.hash
101
+ end
102
+
103
+ @rets_data = sax.rets_data
104
+ end
105
+
106
+ nil
107
+ end
108
+
109
+ ##
110
+ # Requests an object from the RETS server.
111
+ #
112
+ # @param [Hash] args
113
+ # @option args [String] :resource Resource to load, typically *Property*
114
+ # @option args [String] :type Type of object you want, usually *Photo*
115
+ # @option args [String] :id What objects to return
116
+ # @option args [Array, Optional] :accept Array of MIME types to accept, by default this is *image/png*, *image/gif* and *image/jpeg*
117
+ # @option args [Boolean, Optional] :location Return the location of the object rather than the contents of it
118
+ # @option args [Integer, Optional] :read_timeout How many seconds to wait before timing out
119
+ #
120
+ # @yield For every object downloaded
121
+ # @yieldparam [Hash] :headers Object headers
122
+ # * *object-id* (String) - Objects ID
123
+ # * *content-id* (String) - Content ID
124
+ # * *content-type* (String) - MIME type of the content
125
+ # * *description* (String, Optional) - A description of the object
126
+ # * *location* (String, Optional) - Where the file is located, only returned is *location* is true
127
+ # @yieldparam [String, Optional] :content Content for the object, not called when *location* is set
128
+ #
129
+ # @raise [RETS::CapabilityNotFound]
130
+ # @raise [RETS::APIError]
131
+ # @raise [RETS::HTTPError]
132
+ # @see #rets_data
133
+ # @see #request_size
134
+ # @see #request_hash
135
+ def get_object(args, &block)
136
+ raise ArgumentError, "No block passed" unless block_given?
137
+
138
+ unless @urls[:getobject]
139
+ raise RETS::CapabilityNotFound.new("No GetObject capability found for given user.")
140
+ end
141
+
142
+ req = {:url => @urls[:getobject], :read_timeout => args[:read_timeout], :headers => {}}
143
+ req[:params] = {:Resource => args[:resource], :Type => args[:type], :Location => (args[:location] ? 1 : 0), :ID => args[:id]}
144
+ if args[:accept].is_a?(Array)
145
+ req[:headers]["Accept"] = args[:accept].join(",")
146
+ else
147
+ req[:headers]["Accept"] = "image/png,image/gif,image/jpeg"
148
+ end
149
+
150
+ # Will get swapped to a streaming call rather than a download-and-parse later, easy to do as it's called with a block now
151
+ start = Time.now.utc.to_f
152
+
153
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
154
+ @http.request(req) do |response|
155
+ body = response.read_body
156
+
157
+ @request_time = Time.now.utc.to_f - start
158
+ @request_size, @request_hash = body.length, Digest::SHA1.hexdigest(body)
159
+
160
+ # Make sure we aren't erroring
161
+ if body =~ /(<RETS (.+)\>)/
162
+ # RETSIQ (and probably others) return a <RETS> tag on a location request without any error inside
163
+ # since parsing errors out of full image data calls is a tricky pain. We're going to keep the
164
+ # loose error checking, but will confirm that it has an actual error code
165
+ code, text = @http.get_rets_response(Nokogiri::XML($1).xpath("//RETS").first)
166
+ unless code == "0"
167
+ @rets_data = {:code => code, :text => text}
168
+
169
+ if code == "20403"
170
+ return
171
+ else
172
+ raise RETS::APIError.new("#{code}: #{text}", code, text)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Using a wildcard somewhere
178
+ if response.content_type == "multipart/parallel"
179
+ boundary = response.type_params["boundary"]
180
+ boundary.gsub!(/^"|"$/, "")
181
+
182
+ parts = body.split("--#{boundary}\r\n")
183
+ parts.last.gsub!("\r\n--#{boundary}--", "")
184
+ parts.each do |part|
185
+ part.strip!
186
+ next if part == ""
187
+
188
+ headers, content = part.split("\r\n\r\n", 2)
189
+
190
+ parsed_headers = {}
191
+ headers.split("\r\n").each do |line|
192
+ name, value = line.split(":", 2)
193
+ next unless value and value != ""
194
+
195
+ parsed_headers[name.downcase] = value.strip
196
+ end
197
+
198
+ # Check off the first children because some Rap Rets seems to use RETS-Status
199
+ # and it will include it with an object while returning actual data.
200
+ # It only does this for multipart requests, single image pulls will use <RETS> like it should.
201
+ if parsed_headers["content-type"] == "text/xml"
202
+ code, text = @http.get_rets_response(Nokogiri::XML(content).children.first)
203
+ next if code == "20403"
204
+ end
205
+
206
+ if block.arity == 1
207
+ yield parsed_headers
208
+ else
209
+ yield parsed_headers, content
210
+ end
211
+
212
+ end
213
+
214
+ # Either text (error) or an image of some sorts, which is irrelevant for this
215
+ else
216
+ headers = {}
217
+ GET_OBJECT_DATA.each do |field|
218
+ next unless response.header[field] and response.header[field] != ""
219
+ headers[field] = response.header[field].strip
220
+ end
221
+
222
+ if block.arity == 1
223
+ yield headers
224
+ else
225
+ yield headers, body
226
+ end
227
+ end
228
+ end
229
+
230
+ nil
231
+ end
232
+
233
+ ##
234
+ # Searches the RETS server for data.
235
+ #
236
+ # @param [Hash] args
237
+ # @option args [String] :search_type What to search on, typically *Property*, *Office* or *Agent*
238
+ # @option args [String] :class What class of data to return, varies between RETS implementations and can be anything from *1* to *ResidentialProperty*
239
+ # @option args [String] :query How to filter data, should be unescaped as CGI::escape will be called on the string
240
+ # @option args [Symbol, Optional] :count_mode Either *:only* to return just the total records found or *:both* to get count and records returned
241
+ # @option args [Integer, Optional] :limit Limit total records returned
242
+ # @option args [Integer, Optional] :offset Offset to start returning records from
243
+ # @option args [Array, Optional] :select Restrict the fields the RETS server returns
244
+ # @option args [Boolean, Optional] :standard_names Whether to use standard names for all fields
245
+ # @option args [String, Optional] :restricted String to show in place of a field value for any restricted fields the user cannot see
246
+ # @option args [Integer, Optional] :read_timeout How long to wait for data from the socket before giving up
247
+ # @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
248
+ #
249
+ # @yield Called for every <DATA></DATA> group from the RETS server
250
+ # @yieldparam [Hash] :data One record of data from the RETS server
251
+ #
252
+ # @raise [RETS::CapabilityNotFound]
253
+ # @raise [RETS::APIError]
254
+ # @raise [RETS::HTTPError]
255
+ # @see #rets_data
256
+ # @see #request_size
257
+ # @see #request_hash
258
+ def search(args, &block)
259
+ if !block_given? and args[:count_mode] != :only
260
+ raise ArgumentError, "No block found"
261
+ end
262
+
263
+ unless @urls[:search]
264
+ raise RETS::CapabilityNotFound.new("Cannot find URL for Search call")
265
+ end
266
+
267
+ req = {:url => @urls[:search], :read_timeout => args[:read_timeout], :disable_compression => !args[:disable_stream]}
268
+ req[:params] = {:Format => "COMPACT-DECODED", :SearchType => args[:search_type], :QueryType => "DMQL2", :Query => args[:query], :Class => args[:class], :Limit => args[:limit], :Offset => args[:offset], :RestrictedIndicator => args[:restricted]}
269
+ req[:params][:Select] = args[:select].join(",") if args[:select].is_a?(Array)
270
+ req[:params][:StandardNames] = 1 if args[:standard_names]
271
+
272
+ if args[:count_mode] == :only
273
+ req[:params][:Count] = 2
274
+ elsif args[:count_mode] == :both
275
+ req[:params][:Count] = 1
276
+ end
277
+
278
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, {}
279
+
280
+ start = Time.now.utc.to_f
281
+ @http.request(req) do |response|
282
+ if args[:disable_stream]
283
+ stream = StringIO.new(response.body)
284
+ @request_time = Time.now.utc.to_f - start
285
+ else
286
+ stream = RETS::StreamHTTP.new(response)
287
+ end
288
+
289
+ sax = RETS::Base::SAXSearch.new(@rets_data, block)
290
+ Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
291
+
292
+ if args[:disable_stream]
293
+ @request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
294
+ else
295
+ @request_size, @request_hash = stream.size, stream.hash
296
+ end
297
+ end
298
+
299
+ nil
300
+ end
301
+ end
302
+ end
303
+ end