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 +9 -9
- data/app/models/caboose_rets/rets_importer.rb +102 -33
- data/app/models/caboose_rets/schema.rb +5 -2
- data/lib/caboose_rets/version.rb +1 -1
- data/lib/rets/base/core.rb +290 -0
- data/lib/rets/base/sax_metadata.rb +63 -0
- data/lib/rets/base/sax_search.rb +58 -0
- data/lib/rets/client.rb +70 -0
- data/lib/rets/exceptions.rb +37 -0
- data/lib/rets/http.rb +329 -0
- data/lib/rets/stream_http.rb +159 -0
- data/lib/rets/version.rb +3 -0
- metadata +17 -9
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YzA2ZjFiZWNjODQ0YTQ1NWI3ZmMwZTY4MGUzOWEwZGE5YTU0NzQ0Nw==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
7
|
-
|
6
|
+
YzNjZmJhMWM2NjI4YjI5MjZjYTI3MGFkNGZiNzliYTQwYjk3Yjg4NQ==
|
7
|
+
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZDM2YTE0OGM1ZjdkNjMzMTk1N2Q2NzAwYWQwZWQ2Nzg4NmI0MGRjNjJmZDZh
|
10
|
+
MjNlNjBmOTY5NGI5Yjg2NWM3YjI5ZDBhMTBmMTRmNWZhYTgwODJjZGFlN2U4
|
11
|
+
NjJkNmFhYjJmZGNmMDRlOWMyYjYyOWU5YjRjZjYzYjQ3MjVhYWQ=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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 ],
|
data/lib/caboose_rets/version.rb
CHANGED
@@ -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
|