rets-hack 0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/Manifest.txt +58 -0
  4. data/README.md +129 -0
  5. data/Rakefile +28 -0
  6. data/bin/rets +202 -0
  7. data/example/connect.rb +19 -0
  8. data/example/get-photos.rb +20 -0
  9. data/example/get-property.rb +16 -0
  10. data/lib/rets/client.rb +373 -0
  11. data/lib/rets/client_progress_reporter.rb +48 -0
  12. data/lib/rets/http_client.rb +133 -0
  13. data/lib/rets/locking_http_client.rb +34 -0
  14. data/lib/rets/measuring_http_client.rb +27 -0
  15. data/lib/rets/metadata/caching.rb +59 -0
  16. data/lib/rets/metadata/containers.rb +89 -0
  17. data/lib/rets/metadata/file_cache.rb +29 -0
  18. data/lib/rets/metadata/json_serializer.rb +27 -0
  19. data/lib/rets/metadata/lookup_table.rb +65 -0
  20. data/lib/rets/metadata/lookup_type.rb +19 -0
  21. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  22. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  23. data/lib/rets/metadata/null_cache.rb +24 -0
  24. data/lib/rets/metadata/resource.rb +103 -0
  25. data/lib/rets/metadata/rets_class.rb +57 -0
  26. data/lib/rets/metadata/rets_object.rb +41 -0
  27. data/lib/rets/metadata/root.rb +155 -0
  28. data/lib/rets/metadata/table.rb +33 -0
  29. data/lib/rets/metadata/table_factory.rb +19 -0
  30. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  31. data/lib/rets/metadata.rb +18 -0
  32. data/lib/rets/parser/compact.rb +117 -0
  33. data/lib/rets/parser/error_checker.rb +56 -0
  34. data/lib/rets/parser/multipart.rb +39 -0
  35. data/lib/rets.rb +269 -0
  36. data/test/fixtures.rb +324 -0
  37. data/test/helper.rb +14 -0
  38. data/test/test_caching.rb +89 -0
  39. data/test/test_client.rb +307 -0
  40. data/test/test_error_checker.rb +87 -0
  41. data/test/test_file_cache.rb +42 -0
  42. data/test/test_http_client.rb +132 -0
  43. data/test/test_json_serializer.rb +26 -0
  44. data/test/test_locking_http_client.rb +29 -0
  45. data/test/test_marshal_serializer.rb +26 -0
  46. data/test/test_metadata.rb +71 -0
  47. data/test/test_metadata_class.rb +50 -0
  48. data/test/test_metadata_lookup_table.rb +21 -0
  49. data/test/test_metadata_lookup_type.rb +21 -0
  50. data/test/test_metadata_multi_lookup_table.rb +60 -0
  51. data/test/test_metadata_object.rb +33 -0
  52. data/test/test_metadata_resource.rb +148 -0
  53. data/test/test_metadata_root.rb +151 -0
  54. data/test/test_metadata_table.rb +21 -0
  55. data/test/test_metadata_table_factory.rb +24 -0
  56. data/test/test_parser_compact.rb +115 -0
  57. data/test/test_parser_multipart.rb +39 -0
  58. data/test/test_yaml_serializer.rb +26 -0
  59. data/test/vcr_cassettes/unauthorized_response.yml +262 -0
  60. metadata +227 -0
@@ -0,0 +1,373 @@
1
+ require 'logger'
2
+
3
+ module Rets
4
+ class Client
5
+ COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
6
+ CASE_INSENSITIVE_PROC = Proc.new { |h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
7
+
8
+ attr_accessor :cached_metadata, :client_progress, :logger, :login_url, :options
9
+
10
+ def initialize(options)
11
+ @options = options
12
+ clean_setup
13
+ end
14
+
15
+ def clean_setup
16
+ if options.fetch(:login_after_error, true)
17
+ @capabilities = nil
18
+ end
19
+ @metadata = nil
20
+ @tries = nil
21
+ @login_url = options[:login_url]
22
+ @cached_metadata = options[:metadata]
23
+ @cached_capabilities = options[:capabilities]
24
+ @logger = options[:logger] || FakeLogger.new
25
+ @client_progress = ClientProgressReporter.new(logger, options[:stats_collector], options[:stats_prefix])
26
+ @http_client = Rets::HttpClient.from_options(options, logger)
27
+ @caching = Metadata::Caching.make(options)
28
+ end
29
+
30
+ # Attempts to login by making an empty request to the URL provided in
31
+ # initialize. Returns the capabilities that the RETS server provides, per
32
+ # page 34 of http://www.realtor.org/retsorg.nsf/retsproto1.7d6.pdf#page=34
33
+ def login
34
+ res = http_get(login_url)
35
+ Parser::ErrorChecker.check(res)
36
+
37
+ new_capabilities = extract_capabilities(Nokogiri.parse(res.body))
38
+ unless new_capabilities
39
+ raise UnknownResponse, "Cannot read rets server capabilities."
40
+ end
41
+ @capabilities = new_capabilities
42
+ end
43
+
44
+ def logout
45
+ unless capabilities["Logout"]
46
+ raise NoLogout.new('No logout method found for rets client')
47
+ end
48
+ http_get(capability_url("Logout"))
49
+ rescue UnknownResponse => e
50
+ unless e.message.match(/expected a 200, but got 401/)
51
+ raise e
52
+ end
53
+ end
54
+
55
+ # Finds records.
56
+ #
57
+ # [quantity] Return the first record, or an array of records.
58
+ # Uses a symbol <tt>:first</tt> or <tt>:all</tt>, respectively.
59
+ #
60
+ # [opts] A hash of arguments used to construct the search query,
61
+ # using the following keys:
62
+ #
63
+ # <tt>:search_type</tt>:: Required. The resource to search for.
64
+ # <tt>:class</tt>:: Required. The class of the resource to search for.
65
+ # <tt>:query</tt>:: Required. The DMQL2 query string to execute.
66
+ # <tt>:limit</tt>:: The number of records to request from the server.
67
+ # <tt>:resolve</tt>:: Provide resolved values that use metadata instead
68
+ # of raw system values.
69
+ #
70
+ # Any other keys are converted to the RETS query format, and passed
71
+ # to the server as part of the query. For instance, the key <tt>:offset</tt>
72
+ # will be sent as +Offset+.
73
+ #
74
+ def find(quantity, opts = {})
75
+ case quantity
76
+ when :first then find_with_retries(opts.merge(:limit => 1)).first
77
+ when :all then find_with_retries(opts)
78
+ else raise ArgumentError, "First argument must be :first or :all"
79
+ end
80
+ end
81
+
82
+ alias search find
83
+
84
+ def find_with_retries(opts = {})
85
+ retries = 0
86
+ find_with_given_retry(retries, opts)
87
+ end
88
+
89
+ def find_with_given_retry(retries, opts)
90
+ begin
91
+ find_every(opts)
92
+ rescue NoRecordsFound => e
93
+ if opts.fetch(:no_records_not_an_error, false)
94
+ client_progress.no_records_found
95
+ opts[:count] == COUNT.only ? 0 : []
96
+ else
97
+ handle_find_failure(retries, opts, e)
98
+ end
99
+ rescue InvalidRequest, HttpError => e
100
+ handle_find_failure(retries, opts, e)
101
+ rescue AuthorizationFailure => e
102
+ login
103
+ handle_find_failure(retries, opts, e)
104
+ end
105
+ end
106
+
107
+ def handle_find_failure(retries, opts, e)
108
+ max_retries = fetch_max_retries(opts)
109
+ if retries < max_retries
110
+ retries += 1
111
+ wait_before_next_request
112
+ client_progress.find_with_retries_failed_a_retry(e, retries, max_retries)
113
+ clean_setup
114
+ find_with_given_retry(retries, opts)
115
+ else
116
+ client_progress.find_with_retries_exceeded_retry_count(e)
117
+ raise e
118
+ end
119
+ end
120
+
121
+ def fetch_max_retries(hash)
122
+ hash[:max_retries] || options.fetch(:max_retries, 3)
123
+ end
124
+
125
+ def wait_before_next_request
126
+ sleep_time = Float(options.fetch(:recoverable_error_wait_secs, 0))
127
+ if sleep_time > 0
128
+ logger.info "Waiting #{sleep_time} seconds before next attempt"
129
+ sleep sleep_time
130
+ end
131
+ end
132
+
133
+ def find_every(opts)
134
+ raise ArgumentError.new("missing option :search_type (provide the name of a RETS resource)") unless opts[:search_type]
135
+ raise ArgumentError.new("missing option :class (provide the name of a RETS class)") unless opts[:class]
136
+
137
+ params = {
138
+ "SearchType" => opts.fetch(:search_type),
139
+ "Class" => opts.fetch(:class),
140
+ "Count" => opts[:count],
141
+ "Format" => opts.fetch(:format, "COMPACT"),
142
+ "Limit" => opts[:limit],
143
+ "Offset" => opts[:offset],
144
+ "Select" => opts[:select],
145
+ "RestrictedIndicator" => opts[:RestrictedIndicator],
146
+ "StandardNames" => opts[:standard_name],
147
+ "Payload" => opts[:payload],
148
+ "Query" => opts[:query],
149
+ "QueryType" => opts.fetch(:query_type, "DMQL2"),
150
+ }.reject { |k,v| v.nil? }
151
+ res = clean_response(http_post(capability_url("Search"), params))
152
+
153
+ if opts[:count] == COUNT.only
154
+ Parser::Compact.get_count(res.body)
155
+ else
156
+ results = Parser::Compact.parse_document(
157
+ res.body
158
+ )
159
+ if opts[:resolve]
160
+ rets_class = find_rets_class(opts[:search_type], opts[:class])
161
+ decorate_results(results, rets_class)
162
+ else
163
+ results
164
+ end
165
+ end
166
+ end
167
+
168
+ def find_rets_class(resource_name, rets_class_name)
169
+ metadata.tree[resource_name].find_rets_class(rets_class_name)
170
+ end
171
+
172
+ def decorate_results(results, rets_class)
173
+ results.map do |result|
174
+ decorate_result(result, rets_class)
175
+ end
176
+ end
177
+
178
+ def decorate_result(result, rets_class)
179
+ result.each do |key, value|
180
+ table = rets_class.find_table(key)
181
+ if table
182
+ result[key] = table.resolve(value.to_s)
183
+ else
184
+ #can't resolve just leave the value be
185
+ client_progress.could_not_resolve_find_metadata(key)
186
+ end
187
+ end
188
+ end
189
+
190
+ # Returns an array of all objects associated with the given resource.
191
+ def all_objects(opts = {})
192
+ objects("*", opts)
193
+ end
194
+
195
+ # Returns an array of specified objects.
196
+ def objects(object_ids, opts = {})
197
+ response = case object_ids
198
+ when String then fetch_object(object_ids, opts)
199
+ when Array then fetch_object(object_ids.join(":"), opts)
200
+ else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}."
201
+ end
202
+
203
+ create_parts_from_response(response)
204
+ end
205
+
206
+ def create_parts_from_response(response)
207
+ content_type = response.header["content-type"][0]
208
+
209
+ if content_type.nil?
210
+ raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}"
211
+ end
212
+
213
+ if content_type.include?("multipart")
214
+ boundary = content_type.scan(/boundary="?([^;"]*)?/).join
215
+
216
+ parts = Parser::Multipart.parse(response.body, boundary)
217
+
218
+ logger.debug "Rets::Client: Found #{parts.size} parts"
219
+
220
+ return parts
221
+ else
222
+ logger.debug "Rets::Client: Found 1 part (the whole body)"
223
+
224
+ # fake a multipart for interface compatibility
225
+ headers = {}
226
+ response.headers.each { |k,v| headers[k.downcase] = v }
227
+ part = Parser::Multipart::Part.new(headers, response.body)
228
+
229
+ return [part]
230
+ end
231
+ end
232
+
233
+ # Returns a single object.
234
+ #
235
+ # resource RETS resource as defined in the resource metadata.
236
+ # object_type an object type defined in the object metadata.
237
+ # resource_id the KeyField value of the given resource instance.
238
+ # object_id can be "*" or a colon delimited string of integers or an array of integers.
239
+ def object(object_id, opts = {})
240
+ response = fetch_object(Array(object_id).join(':'), opts)
241
+ response.body
242
+ end
243
+
244
+ def fetch_object(object_id, opts = {})
245
+ params = {
246
+ "Resource" => opts.fetch("resource"),
247
+ "Type" => opts.fetch("object_type"),
248
+ "ID" => "#{opts.fetch("resource_id")}:#{object_id}",
249
+ "Location" => opts.fetch("location", 0)
250
+ }
251
+
252
+ extra_headers = {
253
+ "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
254
+ }
255
+
256
+ http_post(capability_url("GetObject"), params, extra_headers)
257
+ end
258
+
259
+ def metadata(types=nil)
260
+ return @metadata if @metadata
261
+ @cached_metadata ||= @caching.load(@logger)
262
+ if cached_metadata && (options[:skip_metadata_uptodate_check] ||
263
+ cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
264
+ client_progress.use_cached_metadata
265
+ @metadata = cached_metadata
266
+ else
267
+ client_progress.bad_cached_metadata(cached_metadata)
268
+ @metadata = Metadata::Root.new(logger, retrieve_metadata(types))
269
+ @caching.save(metadata)
270
+ end
271
+ @metadata
272
+ end
273
+
274
+ def retrieve_metadata(types=nil)
275
+ raw_metadata = {}
276
+ (types || Metadata::METADATA_TYPES).each {|type|
277
+ raw_metadata[type] = retrieve_metadata_type(type)
278
+ }
279
+ raw_metadata
280
+ end
281
+
282
+ def retrieve_metadata_type(type)
283
+ res = http_post(capability_url("GetMetadata"),
284
+ { "Format" => "COMPACT",
285
+ "Type" => "METADATA-#{type}",
286
+ "ID" => "0"
287
+ })
288
+ clean_response(res).body
289
+ end
290
+
291
+ # The capabilies as provided by the RETS server during login.
292
+ #
293
+ # Currently, only the path in the endpoint URLs is used[1]. Host,
294
+ # port, other details remaining constant with those provided to
295
+ # the constructor.
296
+ #
297
+ # [1] In fact, sometimes only a path is returned from the server.
298
+ def capabilities
299
+ if @capabilities
300
+ @capabilities
301
+ elsif @cached_capabilities
302
+ @capabilities = add_case_insensitive_default_proc(@cached_capabilities)
303
+ else
304
+ login
305
+ end
306
+ end
307
+
308
+ def capability_url(name)
309
+ val = capabilities[name] || capabilities[name.downcase]
310
+
311
+ raise UnknownCapability.new(name, capabilities.keys) unless val
312
+
313
+ begin
314
+ if val.downcase.match(/^https?:\/\//)
315
+ uri = URI.parse(val)
316
+ else
317
+ uri = URI.parse(login_url)
318
+ uri.path = val
319
+ end
320
+ rescue URI::InvalidURIError
321
+ raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}"
322
+ end
323
+ uri.to_s
324
+ end
325
+
326
+ def extract_capabilities(document)
327
+ raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip
328
+
329
+ # ... :(
330
+ # Feel free to make this better. It has a test.
331
+ hash = raw_key_values.split(/\n/).
332
+ map { |r| r.split(/\=/, 2) }.
333
+ each_with_object({}) { |(k,v), h| h[k.strip.downcase] = v.strip }
334
+
335
+ add_case_insensitive_default_proc(hash)
336
+ end
337
+
338
+ def add_case_insensitive_default_proc(hash)
339
+ new_hash = hash.dup
340
+ new_hash.default_proc = CASE_INSENSITIVE_PROC
341
+ new_hash
342
+ end
343
+
344
+ def save_cookie_store
345
+ @http_client.save_cookie_store
346
+ end
347
+
348
+ def http_get(url, params=nil, extra_headers={})
349
+ clean_response(@http_client.http_get(url, params, extra_headers))
350
+ end
351
+
352
+ def clean_response(res)
353
+ res.body.encode!("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace)
354
+ res
355
+ end
356
+
357
+ def http_post(url, params, extra_headers = {})
358
+ @http_client.http_post(url, params, extra_headers)
359
+ end
360
+
361
+ def tries
362
+ @tries ||= 1
363
+
364
+ (@tries += 1) - 1
365
+ end
366
+
367
+ class FakeLogger < Logger
368
+ def initialize
369
+ super(IO::NULL)
370
+ end
371
+ end
372
+ end
373
+ end
@@ -0,0 +1,48 @@
1
+ module Rets
2
+ class NullStatsReporter
3
+ def time(metric_name, &block)
4
+ block.call
5
+ end
6
+
7
+ def gauge(metric_name, measurement)
8
+ end
9
+
10
+ def count(metric_name, count=1)
11
+ end
12
+ end
13
+
14
+ class ClientProgressReporter
15
+ def initialize(logger, stats, stats_prefix)
16
+ @logger = logger
17
+ @stats = stats || NullStatsReporter.new
18
+ @stats_prefix = stats_prefix
19
+ end
20
+
21
+ def find_with_retries_failed_a_retry(exception, retries, max_retries)
22
+ @stats.count("#{@stats_prefix}find_with_retries_failed_retry")
23
+ @logger.warn("Rets::Client: Failed with message: #{exception.message}")
24
+ @logger.info("Rets::Client: Retry #{retries}/#{max_retries}")
25
+ end
26
+
27
+ def find_with_retries_exceeded_retry_count(exception)
28
+ @stats.count("#{@stats_prefix}find_with_retries_exceeded_retry_count")
29
+ end
30
+
31
+ def no_records_found
32
+ @logger.info("Rets::Client: No Records Found")
33
+ end
34
+
35
+ def could_not_resolve_find_metadata(key)
36
+ @stats.count("#{@stats_prefix}could_not_resolve_find_metadata")
37
+ @logger.warn "Rets::Client: Can't resolve find metadata for #{key.inspect}"
38
+ end
39
+
40
+ def use_cached_metadata
41
+ @logger.info "Rets::Client: Use cached metadata"
42
+ end
43
+
44
+ def bad_cached_metadata(cached_metadata)
45
+ @logger.info cached_metadata ? "Rets::Client: Cached metadata out of date" : "Rets::Client: Cached metadata unavailable"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,133 @@
1
+ require 'http-cookie'
2
+ require 'httpclient'
3
+
4
+ module Rets
5
+ class HttpClient
6
+ attr_reader :http, :options, :logger, :login_url
7
+
8
+ def initialize(http, options, logger, login_url)
9
+ @http = http
10
+ @options = options
11
+ @logger = logger
12
+ @login_url = login_url
13
+ @options.fetch(:ca_certs, []).each {|c| @http.ssl_config.add_trust_ca(c) }
14
+ end
15
+
16
+ def self.from_options(options, logger)
17
+ if options[:http_proxy]
18
+ http = HTTPClient.new(options.fetch(:http_proxy))
19
+
20
+ if options[:proxy_username]
21
+ http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
22
+ end
23
+ else
24
+ http = HTTPClient.new
25
+ end
26
+
27
+ if options[:receive_timeout]
28
+ http.receive_timeout = options[:receive_timeout]
29
+ end
30
+
31
+ if options[:cookie_store]
32
+ ensure_cookie_store_exists! options[:cookie_store]
33
+ http.set_cookie_store(options[:cookie_store])
34
+ end
35
+
36
+ http_client = new(http, options, logger, options[:login_url])
37
+
38
+ if options[:http_timing_stats_collector]
39
+ http_client = Rets::MeasuringHttpClient.new(http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
40
+ end
41
+
42
+ if options[:lock_around_http_requests]
43
+ http_client = Rets::LockingHttpClient.new(http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
44
+ end
45
+
46
+ http_client
47
+ end
48
+
49
+ def self.ensure_cookie_store_exists!(cookie_store)
50
+ unless File.exist? cookie_store
51
+ FileUtils.touch(cookie_store)
52
+ end
53
+ end
54
+
55
+ def http_get(url, params=nil, extra_headers={})
56
+ http.set_auth(url, options[:username], options[:password])
57
+ headers = extra_headers.merge(rets_extra_headers)
58
+ res = nil
59
+ log_http_traffic("GET", url, params, headers) do
60
+ res = http.get(url, params, headers)
61
+ end
62
+ Parser::ErrorChecker.check(res)
63
+ res
64
+ end
65
+
66
+ def http_post(url, params, extra_headers = {})
67
+ http.set_auth(url, options[:username], options[:password])
68
+ headers = extra_headers.merge(rets_extra_headers)
69
+ res = nil
70
+ log_http_traffic("POST", url, params, headers) do
71
+ res = http.post(url, params, headers)
72
+ end
73
+ Parser::ErrorChecker.check(res)
74
+ res
75
+ end
76
+
77
+ def log_http_traffic(method, url, params, headers, &block)
78
+ # optimization, we don't want to compute log params
79
+ # if logging is off
80
+ if logger.debug?
81
+ logger.debug "Rets::Client >> #{method} #{url}"
82
+ logger.debug "Rets::Client >> params = #{params.inspect}"
83
+ logger.debug "Rets::Client >> headers = #{headers.inspect}"
84
+ end
85
+
86
+ res = block.call
87
+
88
+ # optimization, we don't want to compute log params
89
+ # if logging is off, especially when there is a loop just
90
+ # for logging
91
+ if logger.debug?
92
+ logger.debug "Rets::Client << Status #{res.status_code}"
93
+ res.headers.each { |k, v| logger.debug "Rets::Client << #{k}: #{v}" }
94
+ end
95
+ end
96
+
97
+ def save_cookie_store
98
+ if options[:cookie_store]
99
+ #save session cookies
100
+ @http.cookie_manager.save_cookies(true)
101
+ end
102
+ end
103
+
104
+ def rets_extra_headers
105
+ user_agent = options[:agent] || "Client/1.0"
106
+ rets_version = options[:version] || "RETS/1.7.2"
107
+
108
+ headers = {
109
+ "User-Agent" => user_agent,
110
+ "RETS-Version" => rets_version
111
+ }
112
+
113
+ if options[:ua_password]
114
+ up = Digest::MD5.hexdigest "#{user_agent}:#{options[:ua_password]}"
115
+ session_id = http_cookie('RETS-Session-ID') || ''
116
+ digest = Digest::MD5.hexdigest "#{up}::#{session_id}:#{rets_version}"
117
+ headers.merge!("RETS-UA-Authorization" => "Digest #{digest}")
118
+ end
119
+
120
+ headers
121
+ end
122
+
123
+ def http_cookie(name)
124
+ @http.cookie_manager.cookies(login_url).each do |c|
125
+ if c.name.downcase == name.downcase
126
+ return c.value
127
+ end
128
+ end
129
+
130
+ nil
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,34 @@
1
+ module Rets
2
+ class LockingHttpClient
3
+ def initialize(http_client, locker, lock_name, options={})
4
+ @http_client = http_client
5
+ @locker = locker
6
+ @lock_name = lock_name
7
+ @options = options
8
+ end
9
+
10
+ def http_get(url, params=nil, extra_headers={})
11
+ lock_around do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ lock_around do
18
+ @http_client.http_post(url, params, extra_headers)
19
+ end
20
+ end
21
+
22
+ def save_cookie_store
23
+ @http_client.save_cookie_store
24
+ end
25
+
26
+ def lock_around(&block)
27
+ result = nil
28
+ @locker.lock(@lock_name, @options) do
29
+ result = block.call
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ module Rets
2
+ class MeasuringHttpClient
3
+ def initialize(http_client, stats, prefix)
4
+ @http_client = http_client
5
+ @stats = stats
6
+ @prefix = prefix
7
+ end
8
+
9
+ def http_get(url, params=nil, extra_headers={})
10
+ @stats.count("#{@prefix}.http_get_rate")
11
+ @stats.time("#{@prefix}.http_get") do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ @stats.count("#{@prefix}.http_post_rate")
18
+ @stats.time("#{@prefix}.http_post") do
19
+ @http_client.http_post(url, params, extra_headers)
20
+ end
21
+ end
22
+
23
+ def save_cookie_store
24
+ @http_client.save_cookie_store
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,59 @@
1
+ module Rets
2
+ module Metadata
3
+
4
+ # Metadata caching.
5
+ # @api internal
6
+ class Caching
7
+
8
+ # Given the options passed to Client#initialize, make an
9
+ # instance. Options:
10
+ #
11
+ # * :metadata_cache - Persistence mechanism. Defaults to
12
+ # NullCache.
13
+ #
14
+ # * "metadata_serializer - Serialization mechanism. Defaults to
15
+ # MarshalSerializer.
16
+ def self.make(options)
17
+ cache = options.fetch(:metadata_cache) { Metadata::NullCache.new }
18
+ serializer = options.fetch(:metadata_serializer) do
19
+ Metadata::MarshalSerializer.new
20
+ end
21
+ new(cache, serializer)
22
+ end
23
+
24
+ attr_reader :cache
25
+ attr_reader :serializer
26
+
27
+ # The cache is responsible for reading and writing the
28
+ # serialized metadata. The cache should quack like a
29
+ # Rets::Metadata::FileCache.
30
+ #
31
+ # The serializer is responsible for serializing/deserializing
32
+ # the metadata. The serializer should quack like a
33
+ # Rets::Metadata::MarshalSerializer.
34
+ def initialize(cache, serializer)
35
+ @cache = cache
36
+ @serializer = serializer
37
+ end
38
+
39
+ # Load metadata. Returns a Metadata::Root if successful, or nil
40
+ # if it could be loaded for any reason.
41
+ def load(logger)
42
+ sources = @cache.load do |file|
43
+ @serializer.load(file)
44
+ end
45
+ return nil unless sources.is_a?(Hash)
46
+ Metadata::Root.new(logger, sources)
47
+ end
48
+
49
+ # Save metadata.
50
+ def save(metadata)
51
+ @cache.save do |file|
52
+ @serializer.save(file, metadata.sources)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end