spark_api 1.4.29 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/lib/spark_api/authentication/api_auth.rb +1 -1
  5. data/lib/spark_api/authentication/oauth2.rb +1 -1
  6. data/lib/spark_api/authentication/oauth2_impl/grant_type_base.rb +1 -1
  7. data/lib/spark_api/client.rb +2 -2
  8. data/lib/spark_api/models.rb +2 -0
  9. data/lib/spark_api/models/floplan.rb +24 -0
  10. data/lib/spark_api/models/listing.rb +11 -1
  11. data/lib/spark_api/models/media.rb +30 -0
  12. data/lib/spark_api/models/subresource.rb +2 -2
  13. data/lib/spark_api/models/video.rb +108 -0
  14. data/lib/spark_api/models/virtual_tour.rb +16 -0
  15. data/lib/spark_api/request.rb +2 -2
  16. data/script/reso_middleware_example.rb +70 -0
  17. data/spec/fixtures/listings/floplans_index.json +15 -0
  18. data/spec/spec_helper.rb +9 -4
  19. data/spec/unit/spark_api/authentication/api_auth_spec.rb +21 -22
  20. data/spec/unit/spark_api/authentication/base_auth_spec.rb +3 -3
  21. data/spec/unit/spark_api/authentication/oauth2_impl/faraday_middleware_spec.rb +1 -1
  22. data/spec/unit/spark_api/authentication/oauth2_impl/grant_type_base_spec.rb +1 -1
  23. data/spec/unit/spark_api/authentication/oauth2_impl/single_session_provider_spec.rb +2 -2
  24. data/spec/unit/spark_api/authentication/oauth2_spec.rb +40 -40
  25. data/spec/unit/spark_api/authentication_spec.rb +2 -2
  26. data/spec/unit/spark_api/configuration/yaml_spec.rb +44 -44
  27. data/spec/unit/spark_api/configuration_spec.rb +56 -57
  28. data/spec/unit/spark_api/faraday_middleware_spec.rb +12 -12
  29. data/spec/unit/spark_api/models/account_spec.rb +20 -20
  30. data/spec/unit/spark_api/models/activity_spec.rb +5 -5
  31. data/spec/unit/spark_api/models/base_spec.rb +32 -32
  32. data/spec/unit/spark_api/models/concerns/destroyable_spec.rb +2 -2
  33. data/spec/unit/spark_api/models/concerns/savable_spec.rb +19 -19
  34. data/spec/unit/spark_api/models/connect_prefs_spec.rb +1 -1
  35. data/spec/unit/spark_api/models/constraint_spec.rb +1 -1
  36. data/spec/unit/spark_api/models/contact_spec.rb +50 -50
  37. data/spec/unit/spark_api/models/dirty_spec.rb +12 -12
  38. data/spec/unit/spark_api/models/document_spec.rb +3 -3
  39. data/spec/unit/spark_api/models/fields_spec.rb +17 -17
  40. data/spec/unit/spark_api/models/finders_spec.rb +7 -7
  41. data/spec/unit/spark_api/models/floplan_spec.rb +24 -0
  42. data/spec/unit/spark_api/models/listing_cart_spec.rb +46 -46
  43. data/spec/unit/spark_api/models/listing_meta_translations_spec.rb +6 -6
  44. data/spec/unit/spark_api/models/listing_spec.rb +91 -91
  45. data/spec/unit/spark_api/models/message_spec.rb +10 -10
  46. data/spec/unit/spark_api/models/note_spec.rb +10 -10
  47. data/spec/unit/spark_api/models/notification_spec.rb +6 -6
  48. data/spec/unit/spark_api/models/open_house_spec.rb +4 -4
  49. data/spec/unit/spark_api/models/photo_spec.rb +8 -8
  50. data/spec/unit/spark_api/models/portal_spec.rb +4 -4
  51. data/spec/unit/spark_api/models/property_types_spec.rb +5 -5
  52. data/spec/unit/spark_api/models/rental_calendar_spec.rb +13 -11
  53. data/spec/unit/spark_api/models/rule_spec.rb +2 -2
  54. data/spec/unit/spark_api/models/saved_search_spec.rb +33 -33
  55. data/spec/unit/spark_api/models/search_template/quick_search_spec.rb +5 -5
  56. data/spec/unit/spark_api/models/shared_listing_spec.rb +12 -12
  57. data/spec/unit/spark_api/models/sort_spec.rb +3 -3
  58. data/spec/unit/spark_api/models/standard_fields_spec.rb +12 -12
  59. data/spec/unit/spark_api/models/subresource_spec.rb +33 -15
  60. data/spec/unit/spark_api/models/system_info_spec.rb +7 -7
  61. data/spec/unit/spark_api/models/tour_of_home_spec.rb +3 -3
  62. data/spec/unit/spark_api/models/video_spec.rb +9 -9
  63. data/spec/unit/spark_api/models/virtual_tour_spec.rb +7 -7
  64. data/spec/unit/spark_api/models/vow_account_spec.rb +8 -8
  65. data/spec/unit/spark_api/multi_client_spec.rb +14 -14
  66. data/spec/unit/spark_api/options_hash_spec.rb +4 -4
  67. data/spec/unit/spark_api/paginate_spec.rb +71 -71
  68. data/spec/unit/spark_api/primary_array_spec.rb +5 -5
  69. data/spec/unit/spark_api/request_spec.rb +65 -59
  70. data/spec/unit/spark_api_spec.rb +6 -6
  71. metadata +184 -248
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fe8ad347473ad647fb0da6fd681ebe2c6da50daa
4
- data.tar.gz: 7d17ef58323eb5aad73e09167478d0456a7f1df9
2
+ SHA256:
3
+ metadata.gz: b9c9695944207ba64adf36cd6417e6f534222643a04058581af5451ef4e0361e
4
+ data.tar.gz: 4911595b9144031b4659ef04bbd1ca8689090e23297d686741c26649c359b0ed
5
5
  SHA512:
6
- metadata.gz: 0c2df47ef7d780b071df24b586c35838ef1537b68fbd4aac78d8adc99786c2b7de3e81f5d322e781bd0d06defd27fd4bb9f2058788d169d6e48184e346b629ef
7
- data.tar.gz: a23bbccaab494a0007fc75ba5f7317473bc66bfaa268e221b008ad9eef1accbea77008fdc2ba246ebe5508a4f328164c27876f6404bfa3bf1bbfb049e3bed26e
6
+ metadata.gz: d1e45e5ebb03f2561b2d1a8234510598ced6b3816f961dd84e2200f66359bd22a6360b4bdba06bfcaad46e2a5bbac5c22cb03611dc0b4b249ea001ac5022964b
7
+ data.tar.gz: 5259a965c9131db98ef5c9ca876f59189a43274fa30c780c98b1d56b1d762fd974f9a618a407e0daf0d16c9f1e0b690ffd0a500dfe4f225c9eadc2b81091ec6e
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Spark API
2
2
  =====================
3
- [![Build Status](https://travis-ci.org/sparkapi/spark_api.png?branch=master)](http://travis-ci.org/sparkapi/spark_api) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/sparkapi/spark_api)
3
+ ![CI](https://github.com/sparkapi/spark_api/workflows/CI/badge.svg) ![Code Climate](https://codeclimate.com/badge.png)
4
4
 
5
5
  A Ruby wrapper for the Spark REST API. Loosely based on ActiveResource to provide models to interact with remote services.
6
6
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.29
1
+ 1.5.1
@@ -62,7 +62,7 @@ module SparkApi
62
62
 
63
63
  # Perform an HTTP request (no data)
64
64
  def request(method, path, body, options)
65
- escaped_path = URI.escape(path)
65
+ escaped_path = Addressable::URI.escape(path)
66
66
  request_opts = {
67
67
  :AuthToken => @session.auth_token
68
68
  }
@@ -40,7 +40,7 @@ module SparkApi
40
40
 
41
41
  # Perform an HTTP request (no data)
42
42
  def request(method, path, body, options={})
43
- escaped_path = URI.escape(path)
43
+ escaped_path = Addressable::URI.escape(path)
44
44
  connection = @client.connection(true) # SSL Only!
45
45
  connection.headers.merge!(self.auth_header)
46
46
 
@@ -45,7 +45,7 @@ module SparkApi
45
45
  response.expires_in = provider.session_timeout if response.expires_in.nil?
46
46
  SparkApi.logger.debug { "[oauth2] New session created #{response}" }
47
47
  response
48
- rescue Faraday::Error::ConnectionFailed => e
48
+ rescue Faraday::ConnectionFailed => e
49
49
  if @client.ssl_verify && e.message =~ /certificate verify failed/
50
50
  SparkApi.logger.error { SparkApi::Errors.ssl_verification_error }
51
51
  end
@@ -2,7 +2,7 @@ module SparkApi
2
2
  # =API Client
3
3
  # Main class to setup and run requests on the API. A default client is accessible globally as
4
4
  # SparkApi::client if the global configuration has been set as well. Otherwise, this class may
5
- # be instanciated separately with the configuration information.
5
+ # be instantiated separately with the configuration information.
6
6
  class Client
7
7
  include Connection
8
8
  include Authentication
@@ -21,7 +21,7 @@ module SparkApi
21
21
  Configuration::VALID_OPTION_KEYS.each do |key|
22
22
  send("#{key}=", options[key])
23
23
  end
24
- # Instanciate the authenication class passed in.
24
+ # Instantiate the authentication class passed in.
25
25
  @authenticator = authentication_mode.send("new", self)
26
26
  end
27
27
 
@@ -20,10 +20,12 @@ require 'spark_api/models/fields'
20
20
  require 'spark_api/models/idx'
21
21
  require 'spark_api/models/idx_link'
22
22
  require 'spark_api/models/incomplete_listing'
23
+ require 'spark_api/models/floplan'
23
24
  require 'spark_api/models/listing'
24
25
  require 'spark_api/models/listing_cart'
25
26
  require 'spark_api/models/listing_meta_translations'
26
27
  require 'spark_api/models/market_statistics'
28
+ require 'spark_api/models/media'
27
29
  require 'spark_api/models/message'
28
30
  require 'spark_api/models/news_feed_meta'
29
31
  require 'spark_api/models/newsfeed'
@@ -0,0 +1,24 @@
1
+ module SparkApi
2
+ module Models
3
+ class FloPlan < Base
4
+ extend Subresource
5
+ self.element_name = 'floplans'
6
+
7
+ attr_accessor :images, :thumbnails
8
+
9
+ def initialize(attributes={})
10
+ @images = []
11
+ @thumbnails = []
12
+
13
+ attributes['Images'].each do |img|
14
+ if img["Type"].include?('thumbnail')
15
+ @thumbnails << img
16
+ else
17
+ @images << img
18
+ end
19
+ end
20
+ super(attributes)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,7 +2,7 @@ module SparkApi
2
2
  module Models
3
3
  class Listing < Base
4
4
  extend Finders
5
- attr_accessor :photos, :videos, :virtual_tours, :documents, :open_houses, :tour_of_homes, :rental_calendars
5
+ attr_accessor :photos, :videos, :virtual_tours, :documents, :open_houses, :tour_of_homes, :rental_calendars, :floplans
6
6
  attr_accessor :constraints
7
7
  self.element_name="listings"
8
8
  DATA_MASK = "********"
@@ -17,6 +17,7 @@ module SparkApi
17
17
  @constraints = []
18
18
  @tour_of_homes = []
19
19
  @open_houses = []
20
+ @floplans = []
20
21
 
21
22
  if attributes.has_key?('StandardFields')
22
23
  pics, vids, tours, docs, ohouses, tourhomes = attributes['StandardFields'].values_at('Photos','Videos', 'VirtualTours', 'Documents', 'OpenHouses', 'TourOfHomes')
@@ -26,6 +27,10 @@ module SparkApi
26
27
  rentalcalendars = attributes['RentalCalendar']
27
28
  end
28
29
 
30
+ if attributes.has_key?('FloPlans')
31
+ floplans = attributes['FloPlans']
32
+ end
33
+
29
34
  if pics != nil
30
35
  setup_attribute(@photos, pics, Photo)
31
36
  attributes['StandardFields'].delete('Photos')
@@ -61,6 +66,11 @@ module SparkApi
61
66
  attributes.delete('RentalCalendar')
62
67
  end
63
68
 
69
+ if floplans != nil
70
+ setup_attribute(@floplans, floplans, FloPlan)
71
+ attributes.delete('FloPlans')
72
+ end
73
+
64
74
  super(attributes)
65
75
  end
66
76
 
@@ -0,0 +1,30 @@
1
+ module SparkApi
2
+ module Models
3
+ module Media
4
+ # This module is effectively an interface and helper to combine common media
5
+ # actions and information. Media types (videos, virtual tours, etc)
6
+ # should include this module and implement the methods contained
7
+
8
+ def url
9
+ raise "Not Implemented"
10
+ end
11
+
12
+ def description
13
+ raise "Not Implemented"
14
+ end
15
+
16
+ def private?
17
+ attributes['Privacy'] == 'Private'
18
+ end
19
+
20
+ def public?
21
+ attributes['Privacy'] == 'Public'
22
+ end
23
+
24
+ def automatic?
25
+ attributes['Privacy'] == 'Automatic'
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -16,10 +16,10 @@ module SparkApi
16
16
 
17
17
  def parse_date_start_and_end_times(attributes)
18
18
  # Transform the date strings
19
- unless attributes['Date'].nil?
19
+ unless attributes['Date'].nil? || attributes['Date'].empty?
20
20
  date = Date.strptime attributes['Date'], '%m/%d/%Y'
21
21
  ['StartTime','EndTime'].each do |time|
22
- next if attributes[time].nil?
22
+ next if attributes[time].nil? || attributes[time].empty?
23
23
  formatted_date = "#{attributes['Date']}T#{attributes[time]}"
24
24
  datetime = nil
25
25
 
@@ -1,7 +1,12 @@
1
+ require 'net/http'
1
2
  module SparkApi
2
3
  module Models
3
4
  class Video < Base
4
5
  extend Subresource
6
+ include Media
7
+ include Concerns::Savable,
8
+ Concerns::Destroyable
9
+
5
10
  self.element_name = 'videos'
6
11
 
7
12
  def branded?
@@ -11,6 +16,109 @@ module SparkApi
11
16
  def unbranded?
12
17
  attributes['Type'] == 'unbranded'
13
18
  end
19
+
20
+ def url
21
+ attributes['ObjectHtml']
22
+ end
23
+
24
+ def description
25
+ attributes['Name']
26
+ end
27
+
28
+ # Some youtube URLS are youtu.be instead of youtube
29
+ SUPPORTED_VIDEO_TYPES = %w[vimeo youtu].freeze
30
+
31
+ def is_supported_type?
32
+ # Unfortunately there are so many formats of vimeo videos that we canot support all vimeo videos
33
+ # Therefore, we need to do a little more checking here and validate that we can get video codes out of the urls
34
+ (self.ObjectHtml.include?('youtu') && youtube_video_code.present?) || (self.ObjectHtml.include?('vimeo') && vimeo_video_code.present?)
35
+ end
36
+
37
+ def is_valid_iframe?
38
+ self.ObjectHtml.include?('<iframe') && self.ObjectHtml.include?('</iframe>')
39
+ end
40
+
41
+ # gets the thumbnail to be shown on supported (Vimeo and Youtube) videos
42
+ # YouTube provides a predictable url for each video's images
43
+ # for Vimeo, a get request is necessary
44
+ def display_image
45
+ url = self.video_link
46
+ if url
47
+ if(url.include?('youtube'))
48
+ youtube_thumbnail_url
49
+ else
50
+ vimeo_thumbnail_url
51
+ end
52
+ end
53
+ end
54
+
55
+ def video_link
56
+ return nil unless is_supported_type?
57
+
58
+ if self.ObjectHtml.include?('youtu')
59
+ youtube_link
60
+ elsif self.ObjectHtml.include?('vimeo')
61
+ vimeo_link
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def vimeo_video_code
68
+ html = self.ObjectHtml
69
+ if html.match(/(src=)('|")((https:)?\/\/player\.vimeo\.com\/video\/)/)
70
+ new_url = html.split(/(src=')|(src=")/)
71
+ if new_url[2]
72
+ html = new_url[2].split(/("|')/)[0]
73
+ end
74
+ end
75
+ if html.match(/(?:.+?)?(player\.vimeo\.com|vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?))/)
76
+ code = html.split('/').last.split('?').first
77
+ # Vimeo Ids are always numerical
78
+ code.to_i.to_s === code ? code : nil
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ # This if correctly embedded by the user is an embed
85
+ # If not, it could be pretty much anything
86
+ def youtube_video_code
87
+ html = self.ObjectHtml
88
+ if html.match(/(?:.+?)?(?:\/v\/|watch\/|\?v=|\&v=|youtu\.be\/|\/v=|^youtu\.be\/|embed\/|watch\%3Fv\%3D)([a-zA-Z0-9_-]{11})/) || html.match(/(iframe)(.*)(src=)('|")(https:\/\/www\.youtube\.com\/embed)/)
89
+ html.split(/([a-zA-Z0-9_-]{11})/)[1]
90
+ else
91
+ nil
92
+ end
93
+ end
94
+
95
+ def youtube_link
96
+ normalize_youtube_url
97
+ code = youtube_video_code
98
+ code ? "https://www.youtube.com/watch?v=#{code}" : nil
99
+ end
100
+
101
+ def vimeo_link
102
+ code = vimeo_video_code
103
+ code ? "https://vimeo.com/#{code}" : nil
104
+ end
105
+
106
+ def youtube_thumbnail_url
107
+ code = youtube_video_code
108
+ code ? "https://i1.ytimg.com/vi/#{code}/hqdefault.jpg" : nil
109
+ end
110
+
111
+ def vimeo_thumbnail_url
112
+ # due to the rate limiting issue that surfaced shortly before launch,
113
+ # we will temporarily not return vimeo thumbnails until
114
+ # there is bandwidth to implement the solution in FLEX-9959
115
+ return nil
116
+ end
117
+
118
+ def normalize_youtube_url
119
+ self.ObjectHtml.sub!('-nocookie', '')
120
+ end
121
+
14
122
  end
15
123
  end
16
124
  end
@@ -2,6 +2,10 @@ module SparkApi
2
2
  module Models
3
3
  class VirtualTour < Base
4
4
  extend Subresource
5
+ include Media
6
+ include Concerns::Savable,
7
+ Concerns::Destroyable
8
+
5
9
  self.element_name="virtualtours"
6
10
 
7
11
 
@@ -13,6 +17,18 @@ module SparkApi
13
17
  attributes["Type"] == "unbranded"
14
18
  end
15
19
 
20
+ def url
21
+ attributes['Uri']
22
+ end
23
+
24
+ def description
25
+ attributes['Name']
26
+ end
27
+
28
+ def display_image
29
+ # Currently we have no universally good mechanism to get images for virtual tours
30
+ return nil
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -99,7 +99,7 @@ module SparkApi
99
99
  else
100
100
  return response.body
101
101
  end
102
- rescue Faraday::Error::ConnectionFailed => e
102
+ rescue Faraday::ConnectionFailed => e
103
103
  if self.ssl_verify && e.message =~ /certificate verify failed/
104
104
  SparkApi.logger.error { SparkApi::Errors.ssl_verification_error }
105
105
  end
@@ -107,7 +107,7 @@ module SparkApi
107
107
  end
108
108
 
109
109
  def process_request_body(body)
110
- if body.is_a?(Hash)
110
+ if body.is_a?(Hash) || body.is_a?(Array)
111
111
  body.empty? ? nil : {"D" => body }.to_json
112
112
  else
113
113
  body
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script demonstrates how to use the RESO Web API with the Ruby client by pulling listings
4
+ # and replacing encoded field values with their corresponding human-readable values, which are
5
+ # pulled from the XML returned by the RESO metadata endpoint.
6
+
7
+ require "spark_api"
8
+ require "nokogiri"
9
+
10
+ # set up session and RESO Web API middleware
11
+ SparkApi.configure do |config|
12
+ config.authentication_mode = SparkApi::Authentication::OAuth2
13
+ config.middleware = :reso_api
14
+ end
15
+
16
+ SparkApi.client.session = SparkApi::Authentication::OAuthSession.new({ :access_token => "OAUTH2_ACCESS_TOKEN" })
17
+
18
+ # pull metadata from RESO Web API
19
+ metadata_res = (SparkApi.client.get("/$metadata", {:$ApiUser => "FLEXMLS_TECH_ID"}) )
20
+ metadata_xml = Nokogiri::XML(metadata_res).remove_namespaces!
21
+
22
+ # make an array of fields which need to be checked for readable values
23
+ fields_to_lookup = []
24
+ metadata_xml.xpath('//Schema/EnumType/@Name').each do |el|
25
+ fields_to_lookup << el.to_str
26
+ end
27
+
28
+ # get 25 listings
29
+ listings = (SparkApi.client.get("/Property", {:$top => 25, :$ApiUser => "FLEXMLS_TECH_ID"} ))
30
+
31
+ listings['value'].each do |listing| # for each listing,
32
+ fields_to_lookup.each do |field| # go through the array of fields to be checked.
33
+ if !!listing[field] # when one of the fields that needs to be checked exists in a listing,
34
+ if listing[field].is_a? String
35
+ readable = metadata_xml.xpath( # check for readable value to be swapped in
36
+ "//Schema/
37
+ EnumType[@Name=\"#{field}\"]/
38
+ Member[@Name=\"#{listing[field]}\"]/
39
+ Annotation"
40
+ ).attr("String")
41
+
42
+ # if there is a readable value, swap it in
43
+ if !!readable
44
+ listing[field] = readable.to_str
45
+ end
46
+
47
+ elsif listing[field].is_a? Array
48
+ readable_arr = []
49
+ listing[field].each do |el|
50
+ readable = metadata_xml.xpath( # check for readable value to be swapped in
51
+ "//Schema/
52
+ EnumType[@Name=\"#{field}\"]/
53
+ Member[@Name=\"#{el}\"]/
54
+ Annotation"
55
+ ).attr("String")
56
+
57
+ # assemble a new array with readable values and swap it in
58
+ if !!readable
59
+ readable_arr << readable.to_str
60
+ else
61
+ readable_arr << el
62
+ end
63
+ listing[field] = readable_arr
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ puts listings
@@ -0,0 +1,15 @@
1
+ {
2
+ "D":{
3
+ "Results":[{
4
+ "Id": 4300,
5
+ "Name": "Feb 13",
6
+ "Images": [
7
+ {"Uri": "https://foo.bar",
8
+ "Type":"all_in_one_png"},
9
+ {"Uri": "https://foo.bar",
10
+ "Type": "all_in_one_thumbnail_png"}
11
+ ],
12
+ "Success":true
13
+ }]
14
+ }
15
+ }