class Rets::Client
Constants
- CASE_INSENSITIVE_PROC
- COUNT
Attributes
Public Class Methods
# File lib/rets/client.rb, line 10 def initialize(options) @options = options clean_setup end
Public Instance Methods
# File lib/rets/client.rb, line 338 def add_case_insensitive_default_proc(hash) new_hash = hash.dup new_hash.default_proc = CASE_INSENSITIVE_PROC new_hash end
Returns an array of all objects associated with the given resource.
# File lib/rets/client.rb, line 191 def all_objects(opts = {}) objects("*", opts) end
The capabilies as provided by the RETS server during login.
Currently, only the path in the endpoint URLs is used. Host, port, other details remaining constant with those provided to the constructor.
- 1
-
In fact, sometimes only a path is returned from the server.
# File lib/rets/client.rb, line 298 def capabilities if @capabilities @capabilities elsif @cached_capabilities @capabilities = add_case_insensitive_default_proc(@cached_capabilities) else login end end
# File lib/rets/client.rb, line 308 def capability_url(name) val = capabilities[name] || capabilities[name.downcase] raise UnknownCapability.new(name, capabilities.keys) unless val begin if val.downcase.match(/^https?:\/\//) uri = URI.parse(val) else uri = URI.parse(login_url) uri.path = val end rescue URI::InvalidURIError raise MalformedResponse, "Unable to parse capability URL: #{name} => #{val.inspect}" end uri.to_s end
# File lib/rets/client.rb, line 352 def clean_response(res) res.body.encode!("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace) res end
# File lib/rets/client.rb, line 15 def clean_setup if options.fetch(:login_after_error, true) @capabilities = nil end @metadata = nil @tries = nil @login_url = options[:login_url] @cached_metadata = options[:metadata] @cached_capabilities = options[:capabilities] @logger = options[:logger] || FakeLogger.new @client_progress = ClientProgressReporter.new(logger, options[:stats_collector], options[:stats_prefix]) @http_client = Rets::HttpClient.from_options(options, logger) @caching = Metadata::Caching.make(options) end
# File lib/rets/client.rb, line 206 def create_parts_from_response(response) content_type = response.header["content-type"][0] if content_type.nil? raise MalformedResponse, "Unable to read content-type from response: #{response.inspect}" end if content_type.include?("multipart") boundary = content_type.scan(/boundary="?([^;"]*)?/).join parts = Parser::Multipart.parse(response.body, boundary) logger.debug "Rets::Client: Found #{parts.size} parts" return parts else logger.debug "Rets::Client: Found 1 part (the whole body)" # fake a multipart for interface compatibility headers = {} response.headers.each { |k,v| headers[k.downcase] = v } part = Parser::Multipart::Part.new(headers, response.body) return [part] end end
# File lib/rets/client.rb, line 178 def decorate_result(result, rets_class) result.each do |key, value| table = rets_class.find_table(key) if table result[key] = table.resolve(value.to_s) else #can't resolve just leave the value be client_progress.could_not_resolve_find_metadata(key) end end end
# File lib/rets/client.rb, line 172 def decorate_results(results, rets_class) results.map do |result| decorate_result(result, rets_class) end end
# File lib/rets/client.rb, line 326 def extract_capabilities(document) raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip # ... :( # Feel free to make this better. It has a test. hash = raw_key_values.split(/\n/). map { |r| r.split(/\=/, 2) }. each_with_object({}) { |(k,v), h| h[k.strip.downcase] = v.strip } add_case_insensitive_default_proc(hash) end
# File lib/rets/client.rb, line 121 def fetch_max_retries(hash) hash[:max_retries] || options.fetch(:max_retries, 3) end
# File lib/rets/client.rb, line 244 def fetch_object(object_id, opts = {}) params = { "Resource" => opts.fetch("resource"), "Type" => opts.fetch(:object_type), "ID" => "#{opts.fetch(:resource_id)}:#{object_id}", "Location" => opts.fetch(:location, 0) } extra_headers = { "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1", } http_post(capability_url("GetObject"), params, extra_headers) end
Finds records.
- quantity
-
Return the first record, or an array of records. Uses a symbol
:first
or:all
, respectively. - opts
-
A hash of arguments used to construct the search query, using the following keys:
:search_type
-
Required. The resource to search for.
:class
-
Required. The class of the resource to search for.
:query
-
Required. The DMQL2 query string to execute.
:limit
-
The number of records to request from the server.
:resolve
-
Provide resolved values that use metadata instead of raw system values.
Any other keys are converted to the RETS query format, and passed to the server as part of the query. For instance, the key :offset
will be sent as Offset
.
# File lib/rets/client.rb, line 74 def find(quantity, opts = {}) case quantity when :first then find_with_retries(opts.merge(:limit => 1)).first when :all then find_with_retries(opts) else raise ArgumentError, "First argument must be :first or :all" end end
# File lib/rets/client.rb, line 133 def find_every(opts) raise ArgumentError.new("missing option :search_type (provide the name of a RETS resource)") unless opts[:search_type] raise ArgumentError.new("missing option :class (provide the name of a RETS class)") unless opts[:class] params = { "SearchType" => opts.fetch(:search_type), "Class" => opts.fetch(:class), "Count" => opts[:count], "Format" => opts.fetch(:format, "COMPACT"), "Limit" => opts[:limit], "Offset" => opts[:offset], "Select" => opts[:select], "RestrictedIndicator" => opts[:RestrictedIndicator], "StandardNames" => opts[:standard_name], "Payload" => opts[:payload], "Query" => opts[:query], "QueryType" => opts.fetch(:query_type, "DMQL2"), }.reject { |k,v| v.nil? } res = clean_response(http_post(capability_url("Search"), params)) if opts[:count] == COUNT.only Parser::Compact.get_count(res.body) else results = Parser::Compact.parse_document( res.body ) if opts[:resolve] rets_class = find_rets_class(opts[:search_type], opts[:class]) decorate_results(results, rets_class) else results end end end
# File lib/rets/client.rb, line 168 def find_rets_class(resource_name, rets_class_name) metadata.tree[resource_name].find_rets_class(rets_class_name) end
# File lib/rets/client.rb, line 89 def find_with_given_retry(retries, opts) begin find_every(opts) rescue NoRecordsFound => e if opts.fetch(:no_records_not_an_error, false) client_progress.no_records_found opts[:count] == COUNT.only ? 0 : [] else handle_find_failure(retries, opts, e) end rescue InvalidRequest, HttpError => e handle_find_failure(retries, opts, e) rescue AuthorizationFailure => e login handle_find_failure(retries, opts, e) end end
# File lib/rets/client.rb, line 84 def find_with_retries(opts = {}) retries = 0 find_with_given_retry(retries, opts) end
# File lib/rets/client.rb, line 107 def handle_find_failure(retries, opts, e) max_retries = fetch_max_retries(opts) if retries < max_retries retries += 1 wait_before_next_request client_progress.find_with_retries_failed_a_retry(e, retries, max_retries) clean_setup find_with_given_retry(retries, opts) else client_progress.find_with_retries_exceeded_retry_count(e) raise e end end
# File lib/rets/client.rb, line 348 def http_get(url, params=nil, extra_headers={}) clean_response(@http_client.http_get(url, params, extra_headers)) end
# File lib/rets/client.rb, line 357 def http_post(url, params, extra_headers = {}) @http_client.http_post(url, params, extra_headers) end
Attempts to login by making an empty request to the URL provided in initialize. Returns the capabilities that the RETS server provides, per page 34 of www.realtor.org/retsorg.nsf/retsproto1.7d6.pdf#page=34
# File lib/rets/client.rb, line 33 def login res = http_get(login_url) Parser::ErrorChecker.check(res) new_capabilities = extract_capabilities(Nokogiri.parse(res.body)) unless new_capabilities raise UnknownResponse, "Cannot read rets server capabilities." end @capabilities = new_capabilities end
# File lib/rets/client.rb, line 44 def logout unless capabilities["Logout"] raise NoLogout.new('No logout method found for rets client') end http_get(capability_url("Logout")) rescue UnknownResponse => e unless e.message.match(/expected a 200, but got 401/) raise e end end
# File lib/rets/client.rb, line 259 def metadata(types=nil) return @metadata if @metadata @cached_metadata ||= @caching.load(@logger) if cached_metadata && (options[:skip_metadata_uptodate_check] || cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"])) client_progress.use_cached_metadata @metadata = cached_metadata else client_progress.bad_cached_metadata(cached_metadata) @metadata = Metadata::Root.new(logger, retrieve_metadata(types)) @caching.save(metadata) end @metadata end
Returns a single object.
resource RETS resource as defined in the resource metadata. object_type an object type defined in the object metadata. resource_id the KeyField value of the given resource instance. object_id can be “*” or a colon delimited string of integers or an array of integers.
# File lib/rets/client.rb, line 239 def object(object_id, opts = {}) response = fetch_object(Array(object_id).join(':'), opts) response.body end
Returns an array of specified objects.
# File lib/rets/client.rb, line 196 def objects(object_ids, opts = {}) response = case object_ids when String then fetch_object(object_ids, opts) when Array then fetch_object(object_ids.join(":"), opts) else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}." end create_parts_from_response(response) end
# File lib/rets/client.rb, line 274 def retrieve_metadata(types=nil) raw_metadata = {} (types || Metadata::METADATA_TYPES).each {|type| raw_metadata[type] = retrieve_metadata_type(type) } raw_metadata end
# File lib/rets/client.rb, line 282 def retrieve_metadata_type(type) res = http_post(capability_url("GetMetadata"), { "Format" => "COMPACT", "Type" => "METADATA-#{type}", "ID" => "0" }) clean_response(res).body end
# File lib/rets/client.rb, line 361 def tries @tries ||= 1 (@tries += 1) - 1 end
# File lib/rets/client.rb, line 125 def wait_before_next_request sleep_time = Float(options.fetch(:recoverable_error_wait_secs, 0)) if sleep_time > 0 logger.info "Waiting #{sleep_time} seconds before next attempt" sleep sleep_time end end