caboose-rets 0.0.57 → 0.0.58

Sign up to get free protection for your applications and to get access to all the features.
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