caboose-rets 0.0.57 → 0.0.58

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.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ODAxYTczY2U0NzIxNWY3NTgwNzc1MDQ5NTQ5NGMxMWNmZmMwNjIxNQ==
4
+ YzA2ZjFiZWNjODQ0YTQ1NWI3ZmMwZTY4MGUzOWEwZGE5YTU0NzQ0Nw==
5
5
  data.tar.gz: !binary |-
6
- ZjQzOGVhYWJmMGVjOGU3MzQwNWFjYTQyNTUxYzdmNGUwMWIyYzhmYQ==
7
- !binary "U0hBNTEy":
6
+ YzNjZmJhMWM2NjI4YjI5MjZjYTI3MGFkNGZiNzliYTQwYjk3Yjg4NQ==
7
+ SHA512:
8
8
  metadata.gz: !binary |-
9
- OGM0ZjVhNThjOTc0ZjI2YThmZjE0MDk5YzI0NmY2NjI0ZGRjOGMyNzVkM2M5
10
- MDgyNWM5OTJiNWY1ZWQxYWE1ZDVlZTE0OTcxMmYzZmE1NTYxODdhOWE2NTNi
11
- ZDRiNTZiNjJjMTcwNDBjOTY1YzE3MDcwNzA2Nzk5NjhmMDFlZDg=
9
+ ZDM2YTE0OGM1ZjdkNjMzMTk1N2Q2NzAwYWQwZWQ2Nzg4NmI0MGRjNjJmZDZh
10
+ MjNlNjBmOTY5NGI5Yjg2NWM3YjI5ZDBhMTBmMTRmNWZhYTgwODJjZGFlN2U4
11
+ NjJkNmFhYjJmZGNmMDRlOWMyYjYyOWU5YjRjZjYzYjQ3MjVhYWQ=
12
12
  data.tar.gz: !binary |-
13
- OWE1YTU0OWNmMzExM2Q2N2U2YWNkNzA5ZjFlNTdhYzk5MmQ3NTQ0MmEwZDQ1
14
- MjgzZTA5N2JkODhmOTE3NmVkMTNhM2JlMzQ2MzY3MWYzNTNjZjFmMGVhZTky
15
- Y2ZjMmRhYjkzZGJlYTU3MzBhODVkZTVkZTI0NGE2NmZkNjg3ZmQ=
13
+ YWNiZWNkZTExYTA3ZTI2Y2FkZDBiNTUyOWM0ZjUzMDhjYmI0MjY4Y2ZjOGQy
14
+ MmVhMjIxNmI5OGJhMzg0OThlMTg0YjVlZGFlNGZhNWZmOWVmOGEwNTBkZWMx
15
+ YmYyZmFjYmY4NzM2MzI2NDM4YzQ5MGY4YTIzMjA2YzI1MWY3OTA=
@@ -1,4 +1,13 @@
1
- require 'ruby-rets'
1
+ #require 'ruby-rets'
2
+ require "rets/version"
3
+ require "rets/exceptions"
4
+ require "rets/client"
5
+ require "rets/http"
6
+ require "rets/stream_http"
7
+ require "rets/base/core"
8
+ require "rets/base/sax_search"
9
+ require "rets/base/sax_metadata"
10
+
2
11
  require 'httparty'
3
12
  require 'json'
4
13
 
@@ -118,22 +127,22 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
118
127
  # Main updater
119
128
  #=============================================================================
120
129
 
121
- def self.update_after(date_modified)
122
- self.update_residential_properties_modified_after(date_modified)
123
- self.update_commercial_properties_modified_after(date_modified)
124
- self.update_land_properties_modified_after(date_modified)
125
- self.update_multi_family_properties_modified_after(date_modified)
126
- self.update_offices_modified_after(date_modified)
127
- self.update_agents_modified_after(date_modified)
128
- self.update_open_houses_modified_after(date_modified)
130
+ def self.update_after(date_modified, save_images = true)
131
+ self.update_residential_properties_modified_after(date_modified, save_images)
132
+ self.update_commercial_properties_modified_after(date_modified, save_images)
133
+ self.update_land_properties_modified_after(date_modified, save_images)
134
+ self.update_multi_family_properties_modified_after(date_modified, save_images)
135
+ self.update_offices_modified_after(date_modified, save_images)
136
+ self.update_agents_modified_after(date_modified, save_images)
137
+ self.update_open_houses_modified_after(date_modified, save_images)
129
138
  end
130
- def self.update_residential_properties_modified_after(date_modified) self.client.search({ :search_type => 'Property' , :class => 'RES', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_residential_property data['MLS_ACCT' ] } end
131
- def self.update_commercial_properties_modified_after(date_modified) self.client.search({ :search_type => 'Property' , :class => 'COM', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_commercial_property data['MLS_ACCT' ] } end
132
- def self.update_land_properties_modified_after(date_modified) self.client.search({ :search_type => 'Property' , :class => 'LND', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_land_property data['MLS_ACCT' ] } end
133
- def self.update_multi_family_properties_modified_after(date_modified) self.client.search({ :search_type => 'Property' , :class => 'MUL', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_multi_family_property data['MLS_ACCT' ] } end
134
- def self.update_offices_modified_after(date_modified) self.client.search({ :search_type => 'Office' , :class => 'OFF', :select => ['LO_LO_CODE'] , :query => "(LO_DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)", :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_office data['LO_LO_CODE'] } end
135
- def self.update_agents_modified_after(date_modified) self.client.search({ :search_type => 'Agent' , :class => 'AGT', :select => ['LA_LA_CODE'] , :query => "(LA_DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)", :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_agent data['LA_LA_CODE'] } end
136
- def self.update_open_houses_modified_after(date_modified) self.client.search({ :search_type => 'OpenHouse', :class => 'OPH', :select => ['ID'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_open_house data['ID' ] } end
139
+ def self.update_residential_properties_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Property' , :class => 'RES', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_residential_property( data['MLS_ACCT' ], save_images) } end
140
+ def self.update_commercial_properties_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Property' , :class => 'COM', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_commercial_property( data['MLS_ACCT' ], save_images) } end
141
+ def self.update_land_properties_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Property' , :class => 'LND', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_land_property( data['MLS_ACCT' ], save_images) } end
142
+ def self.update_multi_family_properties_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Property' , :class => 'MUL', :select => ['MLS_ACCT'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_multi_family_property( data['MLS_ACCT' ], save_images) } end
143
+ def self.update_offices_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Office' , :class => 'OFF', :select => ['LO_LO_CODE'] , :query => "(LO_DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)", :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_office( data['LO_LO_CODE'], save_images) } end
144
+ def self.update_agents_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'Agent' , :class => 'AGT', :select => ['LA_LA_CODE'] , :query => "(LA_DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)", :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_agent( data['LA_LA_CODE'], save_images) } end
145
+ def self.update_open_houses_modified_after(date_modified, save_images = true) self.client.search({ :search_type => 'OpenHouse', :class => 'OPH', :select => ['ID'] , :query => "(DATE_MODIFIED=#{date_modified.strftime("%FT%T")}+)" , :standard_names_only => true, :timeout => -1 }) { |data| self.delay(:priority => 10).import_open_house( data['ID' ], save_images) } end
137
146
 
138
147
  #=============================================================================
139
148
  # Single model import methods (called from a worker dyno)
@@ -158,47 +167,47 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
158
167
  self.download_property_images(p)
159
168
  end
160
169
 
161
- def self.import_residential_property(mls_acct)
170
+ def self.import_residential_property(mls_acct, save_images = true)
162
171
  self.import("(MLS_ACCT=*#{mls_acct}*)", 'Property', 'RES')
163
172
  p = CabooseRets::ResidentialProperty.where(:id => mls_acct.to_i).first
164
- self.download_property_images(p)
173
+ self.download_property_images(p, save_images)
165
174
  self.update_coords(p)
166
175
  end
167
176
 
168
- def self.import_commercial_property(mls_acct)
177
+ def self.import_commercial_property(mls_acct, save_images = true)
169
178
  self.import("(MLS_ACCT=*#{mls_acct}*)", 'Property', 'COM')
170
179
  p = CabooseRets::CommercialProperty.where(:id => mls_acct.to_i).first
171
- self.download_property_images(p)
180
+ self.download_property_images(p, save_images)
172
181
  self.update_coords(p)
173
182
  end
174
183
 
175
- def self.import_land_property(mls_acct)
184
+ def self.import_land_property(mls_acct, save_images = true)
176
185
  self.import("(MLS_ACCT=*#{mls_acct}*)", 'Property', 'LND')
177
186
  p = CabooseRets::LandProperty.where(:id => mls_acct.to_i).first
178
- self.download_property_images(p)
187
+ self.download_property_images(p, save_images)
179
188
  self.update_coords(p)
180
189
  end
181
190
 
182
- def self.import_multi_family_property(mls_acct)
191
+ def self.import_multi_family_property(mls_acct, save_images = true)
183
192
  self.import("(MLS_ACCT=*#{mls_acct}*)", 'Property', 'MUL')
184
193
  p = CabooseRets::MultiFamilyProperty.where(:id => mls_acct.to_i).first
185
- self.download_property_images(p)
194
+ self.download_property_images(p, save_images)
186
195
  self.update_coords(p)
187
196
  end
188
197
 
189
- def self.import_office(lo_code)
198
+ def self.import_office(lo_code, save_images = true)
190
199
  self.import("(LO_LO_CODE=*#{lo_code}*)", 'Office', 'OFF')
191
200
  office = CabooseRets::Office.where(:lo_code => lo_code.to_s).first
192
- self.download_office_image(office)
201
+ self.download_office_image(office) if save_images == true
193
202
  end
194
203
 
195
- def self.import_agent(la_code)
204
+ def self.import_agent(la_code, save_images = true)
196
205
  self.import("(LA_LA_CODE=*#{la_code}*)", 'Agent', 'AGT')
197
206
  a = CabooseRets::Agent.where(:la_code => la_code.to_s).first
198
- self.download_agent_image(a)
207
+ self.download_agent_image(a) if save_images == true
199
208
  end
200
209
 
201
- def self.import_open_house(id)
210
+ def self.import_open_house(id, save_images = true)
202
211
  self.import("(ID=*#{id}*)", 'OpenHouse', 'OPH')
203
212
  end
204
213
 
@@ -206,8 +215,9 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
206
215
  # Images
207
216
  #=============================================================================
208
217
 
209
- def self.download_property_images(p)
218
+ def self.download_property_images(p, save_images = true)
210
219
  self.refresh_property_media(p)
220
+ return if save_images == false
211
221
 
212
222
  self.log("-- Downloading images and resizing for #{p.mls_acct}")
213
223
  media = []
@@ -265,7 +275,7 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
265
275
  end
266
276
  end
267
277
 
268
- def self.download_office_image(office)
278
+ def self.download_office_image(office)
269
279
  self.log "Saving image for #{office.lo_name}..."
270
280
  begin
271
281
  self.client.get_object(:resource => :Office, :type => :Photo, :location => true, :id => office.lo_code) do |headers, content|
@@ -319,6 +329,45 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
319
329
  end
320
330
  end
321
331
 
332
+ #=============================================================================
333
+ # Purging
334
+ #=============================================================================
335
+
336
+ def self.purge
337
+ self.purge_residential
338
+ self.purge_commercial
339
+ self.purge_land
340
+ self.purge_multi_family
341
+ self.purge_offices
342
+ self.purge_agents
343
+ self.purge_open_houses
344
+ self.purge_media
345
+ end
346
+
347
+ def self.purge_residential() self.purge_helper('Property' , 'RES', 'MLS_ACCT' , 'DATE_MODIFIED' , "delete from rets_residential where mls_acct not in (?)") end
348
+ def self.purge_commercial() self.purge_helper('Property' , 'COM', 'MLS_ACCT' , 'DATE_MODIFIED' , "delete from rets_commercial where mls_acct not in (?)") end
349
+ def self.purge_land() self.purge_helper('Property' , 'LND', 'MLS_ACCT' , 'DATE_MODIFIED' , "delete from rets_land where mls_acct not in (?)") end
350
+ def self.purge_multi_family() self.purge_helper('Property' , 'MUL', 'MLS_ACCT' , 'DATE_MODIFIED' , "delete from rets_multi_family where mls_acct not in (?)") end
351
+ def self.purge_offices() self.purge_helper('Office' , 'OFF', 'LO_LO_CODE' , 'LO_DATE_MODIFIED' , "delete from rets_offices where lo_code not in (?)") end
352
+ def self.purge_agents() self.purge_helper('Agent' , 'AGT', 'LA_LA_CODE' , 'LA_DATE_MODIFIED' , "delete from rets_agents where la_code not in (?)") end
353
+ def self.purge_open_houses() self.purge_helper('OpenHouse' , 'OPH', 'ID' , 'DATE_MODIFIED' , "delete from rets_open_houses where id not in (?)") end
354
+ def self.purge_media() self.purge_helper('Media' , 'GFX', 'MEDIA_ID' , 'DATE_MODIFIED' , "delete from rets_media where media_id not in (?)") end
355
+
356
+ def self.purge_helper(search_type, class_type, key_field, date_modified_field, delete_query)
357
+
358
+ # Get the total number of records
359
+ params = { :search_type => search_type, :class => class_type, :query => "(#{date_modified_field}=2013-08-06T00:00:01+)", :standard_names_only => true, :timeout => -1 }
360
+ self.client.search(params.merge({ :count_mode => :only }))
361
+ count = self.client.rets_data[:code] == "20201" ? 0 : self.client.rets_data[:count]
362
+ batch_count = (count.to_f/5000.0).ceil
363
+
364
+ ids = []
365
+ (0...batch_count).each do |i|
366
+ self.client.search(params.merge({ :select => [key_field], :limit => 5000, :offset => 5000*i })){ |data| ids << data[key_field] }
367
+ end
368
+ ActiveRecord::Base.connection.execute(ActiveRecord::Base.send(:sanitize_sql_array, [delete_query, ids]))
369
+ end
370
+
322
371
  #=============================================================================
323
372
  # Logging
324
373
  #=============================================================================
@@ -336,8 +385,14 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
336
385
  return if self.task_is_locked
337
386
  task_started = self.lock_task
338
387
 
339
- begin
340
- self.update_after(self.last_updated - 30.seconds)
388
+ begin
389
+ overlap = 30.seconds
390
+ if (DateTime.now - self.last_purged) > 24.hours
391
+ self.purge
392
+ self.save_last_purged(task_started)
393
+ overlap = 1.month
394
+ end
395
+ self.update_after(self.last_updated - overlap)
341
396
  self.save_last_updated(task_started)
342
397
  self.unlock_task
343
398
  rescue
@@ -358,12 +413,26 @@ class CabooseRets::RetsImporter # < ActiveRecord::Base
358
413
  return DateTime.parse(s.value)
359
414
  end
360
415
 
416
+ def self.last_purged
417
+ if !Caboose::Setting.exists?(:name => 'rets_last_purged')
418
+ Caboose::Setting.create(:name => 'rets_last_purged', :value => '2013-08-06T00:00:01')
419
+ end
420
+ s = Caboose::Setting.where(:name => 'rets_last_purged').first
421
+ return DateTime.parse(s.value)
422
+ end
423
+
361
424
  def self.save_last_updated(d)
362
425
  s = Caboose::Setting.where(:name => 'rets_last_updated').first
363
426
  s.value = d.strftime('%FT%T')
364
427
  s.save
365
428
  end
366
429
 
430
+ def self.save_last_purged(d)
431
+ s = Caboose::Setting.where(:name => 'rets_last_purged').first
432
+ s.value = d.strftime('%FT%T')
433
+ s.save
434
+ end
435
+
367
436
  def self.task_is_locked
368
437
  return Caboose::Setting.exists?(:name => 'rets_update_running')
369
438
  end
@@ -4,7 +4,8 @@ class CabooseRets::Schema < Caboose::Utilities::Schema
4
4
  def self.removed_columns
5
5
  {
6
6
  CabooseRets::CommercialProperty => [ :virtual_tour ],
7
- CabooseRets::ResidentialProperty => [ :virtual_tour ]
7
+ CabooseRets::ResidentialProperty => [ :virtual_tour ],
8
+ CabooseRets::ResidentialProperty => [ :acreage_temp ]
8
9
  }
9
10
  end
10
11
 
@@ -619,7 +620,9 @@ class CabooseRets::Schema < Caboose::Utilities::Schema
619
620
  [ :elem_school , :text ],
620
621
  [ :price_sqft , :text ],
621
622
  [ :rm_recrm , :text ],
622
- [ :acreage , :text ],
623
+ #[ :acreage , :text ],
624
+ [ :acreage , :float, { :default => 0.0 }],
625
+ #[ :acreage_temp , :float, { :default => 0.0 }],
623
626
  [ :expire_date , :text ],
624
627
  [ :prop_type , :text ],
625
628
  [ :rm_recrm_desc , :text ],
@@ -1,3 +1,3 @@
1
1
  module CabooseRets
2
- VERSION = '0.0.57'
2
+ VERSION = '0.0.58'
3
3
  end
@@ -0,0 +1,290 @@
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
+ #
64
+ # @yield For every group of metadata downloaded
65
+ # @yieldparam [String] :type Type of data that was parsed with "METADATA-" stripped out, for "METADATA-SYSTEM" this will be "SYSTEM"
66
+ # @yieldparam [Hash] :attrs Attributes of the data, generally *Version*, *Date* and *Resource* but can vary depending on what metadata you requested
67
+ # @yieldparam [Array] :metadata Array of hashes with metadata info
68
+ #
69
+ # @raise [RETS::CapabilityNotFound]
70
+ # @raise [RETS::APIError]
71
+ # @raise [RETS::HTTPError]
72
+ # @see #rets_data
73
+ # @see #request_size
74
+ # @see #request_hash
75
+ def get_metadata(args, &block)
76
+ raise ArgumentError, "No block passed" unless block_given?
77
+
78
+ unless @urls[:getmetadata]
79
+ raise RETS::CapabilityNotFound.new("No GetMetadata capability found for given user.")
80
+ end
81
+
82
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
83
+ @http.request(:url => @urls[:getmetadata], :read_timeout => args[:read_timeout], :params => {:Format => :COMPACT, :Type => args[:type], :ID => args[:id]}) do |response|
84
+ stream = RETS::StreamHTTP.new(response)
85
+ sax = RETS::Base::SAXMetadata.new(block)
86
+
87
+ Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
88
+
89
+ @request_size, @request_hash = stream.size, stream.hash
90
+ @rets_data = sax.rets_data
91
+ end
92
+
93
+ nil
94
+ end
95
+
96
+ ##
97
+ # Requests an object from the RETS server.
98
+ #
99
+ # @param [Hash] args
100
+ # @option args [String] :resource Resource to load, typically *Property*
101
+ # @option args [String] :type Type of object you want, usually *Photo*
102
+ # @option args [String] :id What objects to return
103
+ # @option args [Array, Optional] :accept Array of MIME types to accept, by default this is *image/png*, *image/gif* and *image/jpeg*
104
+ # @option args [Boolean, Optional] :location Return the location of the object rather than the contents of it
105
+ # @option args [Integer, Optional] :read_timeout How many seconds to wait before timing out
106
+ #
107
+ # @yield For every object downloaded
108
+ # @yieldparam [Hash] :headers Object headers
109
+ # * *object-id* (String) - Objects ID
110
+ # * *content-id* (String) - Content ID
111
+ # * *content-type* (String) - MIME type of the content
112
+ # * *description* (String, Optional) - A description of the object
113
+ # * *location* (String, Optional) - Where the file is located, only returned is *location* is true
114
+ # @yieldparam [String, Optional] :content Content for the object, not called when *location* is set
115
+ #
116
+ # @raise [RETS::CapabilityNotFound]
117
+ # @raise [RETS::APIError]
118
+ # @raise [RETS::HTTPError]
119
+ # @see #rets_data
120
+ # @see #request_size
121
+ # @see #request_hash
122
+ def get_object(args, &block)
123
+ raise ArgumentError, "No block passed" unless block_given?
124
+
125
+ unless @urls[:getobject]
126
+ raise RETS::CapabilityNotFound.new("No GetObject capability found for given user.")
127
+ end
128
+
129
+ req = {:url => @urls[:getobject], :read_timeout => args[:read_timeout], :headers => {}}
130
+ req[:params] = {:Resource => args[:resource], :Type => args[:type], :Location => (args[:location] ? 1 : 0), :ID => args[:id]}
131
+ if args[:accept].is_a?(Array)
132
+ req[:headers]["Accept"] = args[:accept].join(",")
133
+ else
134
+ req[:headers]["Accept"] = "image/png,image/gif,image/jpeg"
135
+ end
136
+
137
+ # 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
138
+ start = Time.now.utc.to_f
139
+
140
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, nil
141
+ @http.request(req) do |response|
142
+ body = response.read_body
143
+
144
+ @request_time = Time.now.utc.to_f - start
145
+ @request_size, @request_hash = body.length, Digest::SHA1.hexdigest(body)
146
+
147
+ # Make sure we aren't erroring
148
+ if body =~ /(<RETS (.+)\>)/
149
+ # RETSIQ (and probably others) return a <RETS> tag on a location request without any error inside
150
+ # since parsing errors out of full image data calls is a tricky pain. We're going to keep the
151
+ # loose error checking, but will confirm that it has an actual error code
152
+ code, text = @http.get_rets_response(Nokogiri::XML($1).xpath("//RETS").first)
153
+ unless code == "0"
154
+ @rets_data = {:code => code, :text => text}
155
+
156
+ if code == "20403"
157
+ return
158
+ else
159
+ raise RETS::APIError.new("#{code}: #{text}", code, text)
160
+ end
161
+ end
162
+ end
163
+
164
+ # Using a wildcard somewhere
165
+ if response.content_type == "multipart/parallel"
166
+ boundary = response.type_params["boundary"]
167
+ boundary.gsub!(/^"|"$/, "")
168
+
169
+ parts = body.split("--#{boundary}\r\n")
170
+ parts.last.gsub!("\r\n--#{boundary}--", "")
171
+ parts.each do |part|
172
+ part.strip!
173
+ next if part == ""
174
+
175
+ headers, content = part.split("\r\n\r\n", 2)
176
+
177
+ parsed_headers = {}
178
+ headers.split("\r\n").each do |line|
179
+ name, value = line.split(":", 2)
180
+ next unless value and value != ""
181
+
182
+ parsed_headers[name.downcase] = value.strip
183
+ end
184
+
185
+ # Check off the first children because some Rap Rets seems to use RETS-Status
186
+ # and it will include it with an object while returning actual data.
187
+ # It only does this for multipart requests, single image pulls will use <RETS> like it should.
188
+ if parsed_headers["content-type"] == "text/xml"
189
+ code, text = @http.get_rets_response(Nokogiri::XML(content).children.first)
190
+ next if code == "20403"
191
+ end
192
+
193
+ if block.arity == 1
194
+ yield parsed_headers
195
+ else
196
+ yield parsed_headers, content
197
+ end
198
+
199
+ end
200
+
201
+ # Either text (error) or an image of some sorts, which is irrelevant for this
202
+ else
203
+ headers = {}
204
+ GET_OBJECT_DATA.each do |field|
205
+ next unless response.header[field] and response.header[field] != ""
206
+ headers[field] = response.header[field].strip
207
+ end
208
+
209
+ if block.arity == 1
210
+ yield headers
211
+ else
212
+ yield headers, body
213
+ end
214
+ end
215
+ end
216
+
217
+ nil
218
+ end
219
+
220
+ ##
221
+ # Searches the RETS server for data.
222
+ #
223
+ # @param [Hash] args
224
+ # @option args [String] :search_type What to search on, typically *Property*, *Office* or *Agent*
225
+ # @option args [String] :class What class of data to return, varies between RETS implementations and can be anything from *1* to *ResidentialProperty*
226
+ # @option args [String] :query How to filter data, should be unescaped as CGI::escape will be called on the string
227
+ # @option args [Symbol, Optional] :count_mode Either *:only* to return just the total records found or *:both* to get count and records returned
228
+ # @option args [Integer, Optional] :limit Limit total records returned
229
+ # @option args [Integer, Optional] :offset Offset to start returning records from
230
+ # @option args [Array, Optional] :select Restrict the fields the RETS server returns
231
+ # @option args [Boolean, Optional] :standard_names Whether to use standard names for all fields
232
+ # @option args [String, Optional] :restricted String to show in place of a field value for any restricted fields the user cannot see
233
+ # @option args [Integer, Optional] :read_timeout How long to wait for data from the socket before giving up
234
+ # @option args [Boolean, Optional] :disable_stream Disables the streaming setup for data and instead loads it all and then parses
235
+ #
236
+ # @yield Called for every <DATA></DATA> group from the RETS server
237
+ # @yieldparam [Hash] :data One record of data from the RETS server
238
+ #
239
+ # @raise [RETS::CapabilityNotFound]
240
+ # @raise [RETS::APIError]
241
+ # @raise [RETS::HTTPError]
242
+ # @see #rets_data
243
+ # @see #request_size
244
+ # @see #request_hash
245
+ def search(args, &block)
246
+ if !block_given? and args[:count_mode] != :only
247
+ raise ArgumentError, "No block found"
248
+ end
249
+
250
+ unless @urls[:search]
251
+ raise RETS::CapabilityNotFound.new("Cannot find URL for Search call")
252
+ end
253
+
254
+ req = {:url => @urls[:search], :read_timeout => args[:read_timeout]}
255
+ 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]}
256
+ req[:params][:Select] = args[:select].join(",") if args[:select].is_a?(Array)
257
+ req[:params][:StandardNames] = 1 if args[:standard_names]
258
+
259
+ if args[:count_mode] == :only
260
+ req[:params][:Count] = 2
261
+ elsif args[:count_mode] == :both
262
+ req[:params][:Count] = 1
263
+ end
264
+
265
+ @request_size, @request_hash, @request_time, @rets_data = nil, nil, nil, {}
266
+
267
+ start = Time.now.utc.to_f
268
+ @http.request(req) do |response|
269
+ if args[:disable_stream]
270
+ stream = StringIO.new(response.body)
271
+ @request_time = Time.now.utc.to_f - start
272
+ else
273
+ stream = RETS::StreamHTTP.new(response)
274
+ end
275
+
276
+ sax = RETS::Base::SAXSearch.new(@rets_data, block)
277
+ Nokogiri::XML::SAX::Parser.new(sax).parse_io(stream)
278
+
279
+ if args[:disable_stream]
280
+ @request_size, @request_hash = response.body.length, Digest::SHA1.hexdigest(response.body)
281
+ else
282
+ @request_size, @request_hash = stream.size, stream.hash
283
+ end
284
+ end
285
+
286
+ nil
287
+ end
288
+ end
289
+ end
290
+ end