twitter-ads 5.1.0 → 7.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -2
  3. data/README.md +1 -1
  4. data/lib/twitter-ads.rb +8 -4
  5. data/lib/twitter-ads/account.rb +6 -25
  6. data/lib/twitter-ads/audiences/tailored_audience.rb +98 -3
  7. data/lib/twitter-ads/{targeting_criteria/behavior_taxonomy.rb → campaign/advertiser_business_categories.rb} +6 -6
  8. data/lib/twitter-ads/campaign/campaign.rb +1 -2
  9. data/lib/twitter-ads/campaign/content_categories.rb +23 -0
  10. data/lib/twitter-ads/campaign/funding_instrument.rb +1 -2
  11. data/lib/twitter-ads/campaign/line_item.rb +4 -4
  12. data/lib/twitter-ads/campaign/organic_tweet.rb +1 -3
  13. data/lib/twitter-ads/campaign/targeting_criteria.rb +0 -1
  14. data/lib/twitter-ads/campaign/tweet.rb +4 -49
  15. data/lib/twitter-ads/client.rb +2 -2
  16. data/lib/twitter-ads/creative/account_media.rb +4 -6
  17. data/lib/twitter-ads/creative/draft_tweet.rb +40 -0
  18. data/lib/twitter-ads/creative/image_app_download_card.rb +2 -2
  19. data/lib/twitter-ads/creative/image_conversation_card.rb +3 -2
  20. data/lib/twitter-ads/creative/media_creative.rb +2 -3
  21. data/lib/twitter-ads/creative/media_library.rb +12 -13
  22. data/lib/twitter-ads/creative/promoted_account.rb +1 -2
  23. data/lib/twitter-ads/creative/promoted_tweet.rb +1 -2
  24. data/lib/twitter-ads/creative/scheduled_tweet.rb +1 -12
  25. data/lib/twitter-ads/creative/tweets.rb +52 -0
  26. data/lib/twitter-ads/creative/video_app_download_card.rb +4 -6
  27. data/lib/twitter-ads/creative/video_conversation_card.rb +6 -6
  28. data/lib/twitter-ads/creative/video_website_card.rb +3 -5
  29. data/lib/twitter-ads/creative/website_card.rb +2 -2
  30. data/lib/twitter-ads/cursor.rb +6 -0
  31. data/lib/twitter-ads/enum.rb +19 -9
  32. data/lib/twitter-ads/error.rb +5 -15
  33. data/lib/twitter-ads/http/request.rb +37 -2
  34. data/lib/twitter-ads/http/response.rb +1 -13
  35. data/lib/twitter-ads/resources/analytics.rb +99 -47
  36. data/lib/twitter-ads/resources/dsl.rb +8 -1
  37. data/lib/twitter-ads/restapi.rb +29 -0
  38. data/lib/twitter-ads/settings/tax.rb +13 -1
  39. data/lib/twitter-ads/targeting/audience_summary.rb +47 -0
  40. data/lib/twitter-ads/targeting_criteria/{behavior.rb → conversation.rb} +3 -7
  41. data/lib/twitter-ads/utils.rb +23 -0
  42. data/lib/twitter-ads/version.rb +1 -1
  43. data/spec/fixtures/audience_summary.json +14 -0
  44. data/spec/fixtures/line_items_all.json +2 -10
  45. data/spec/fixtures/line_items_load.json +0 -1
  46. data/spec/fixtures/tweet_previews.json +23 -0
  47. data/spec/spec_helper.rb +1 -4
  48. data/spec/twitter-ads/campaign/line_item_spec.rb +0 -1
  49. data/spec/twitter-ads/campaign/targeting_criteria_spec.rb +0 -1
  50. data/spec/twitter-ads/campaign/tweet_spec.rb +0 -59
  51. data/spec/twitter-ads/client_spec.rb +17 -1
  52. data/spec/twitter-ads/creative/media_creative_spec.rb +1 -1
  53. data/spec/twitter-ads/creative/tweet_previews_spec.rb +41 -0
  54. data/spec/twitter-ads/rate_limit_spec.rb +247 -0
  55. data/spec/twitter-ads/retry_count_spec.rb +61 -0
  56. data/spec/twitter-ads/{creative/image_app_download_card_spec.rb → targeting/audience_summary_spec.rb} +16 -18
  57. metadata +46 -47
  58. data/lib/twitter-ads/audiences/audience_intelligence.rb +0 -68
  59. data/lib/twitter-ads/targeting/reach_estimate.rb +0 -78
  60. data/spec/fixtures/tweet_preview.json +0 -24
  61. data/spec/twitter-ads/campaign/reach_estimate_spec.rb +0 -103
  62. data/spec/twitter-ads/creative/account_media_spec.rb +0 -32
  63. data/spec/twitter-ads/creative/image_conversation_card_spec.rb +0 -40
  64. data/spec/twitter-ads/creative/video_app_download_card_spec.rb +0 -42
  65. data/spec/twitter-ads/creative/video_conversation_card_spec.rb +0 -51
  66. data/spec/twitter-ads/creative/website_card_spec.rb +0 -42
@@ -59,18 +59,7 @@ module TwitterAds
59
59
 
60
60
  # Server Errors (5XX)
61
61
  class ServerError < Error; end
62
-
63
- class ServiceUnavailable < ServerError
64
- attr_reader :retry_after
65
-
66
- def initialize(object)
67
- super object
68
- if object.headers['retry-after']
69
- @retry_after = object.headers['retry-after']
70
- end
71
- self
72
- end
73
- end
62
+ class ServiceUnavailable < ServerError; end
74
63
 
75
64
  # Client Errors (4XX)
76
65
  class ClientError < Error; end
@@ -80,12 +69,13 @@ module TwitterAds
80
69
  class BadRequest < ClientError; end
81
70
 
82
71
  class RateLimit < ClientError
83
- attr_reader :reset_at, :retry_after
72
+ attr_reader :reset_at
84
73
 
85
74
  def initialize(object)
86
75
  super object
87
- @retry_after = object.headers['retry-after']
88
- @reset_at = object.headers['rate_limit_reset']
76
+ header = object.headers.fetch('x-account-rate-limit-reset', nil) ||
77
+ object.headers.fetch('x-rate-limit-reset', nil)
78
+ @reset_at = header.first.to_i
89
79
  self
90
80
  end
91
81
  end
@@ -71,18 +71,53 @@ module TwitterAds
71
71
  token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
72
72
  request.oauth!(consumer.http, consumer, token)
73
73
 
74
+ handle_rate_limit = @client.options.fetch(:handle_rate_limit, false)
75
+ retry_max = @client.options.fetch(:retry_max, 0)
76
+ retry_delay = @client.options.fetch(:retry_delay, 1500)
77
+ retry_on_status = @client.options.fetch(:retry_on_status, [500, 503])
78
+ retry_count = 0
79
+ retry_after = nil
80
+
74
81
  write_log(request) if @client.options[:trace]
75
- response = consumer.http.request(request)
82
+ while retry_count <= retry_max
83
+ response = consumer.http.request(request)
84
+ status_code = response.code.to_i
85
+ break if status_code >= 200 && status_code < 300
86
+
87
+ if handle_rate_limit && retry_after.nil?
88
+ rate_limit_reset = response.fetch('x-account-rate-limit-reset', nil) ||
89
+ response.fetch('x-rate-limit-reset', nil)
90
+ if status_code == 429
91
+ retry_after = rate_limit_reset.to_i - Time.now.to_i
92
+ @client.logger.warn('Request reached Rate Limit: resume in %d seconds' % retry_after)
93
+ sleep(retry_after + 5)
94
+ next
95
+ end
96
+ end
97
+
98
+ if retry_max.positive?
99
+ break unless retry_on_status.include?(status_code)
100
+ sleep(retry_delay / 1000)
101
+ end
102
+
103
+ retry_count += 1
104
+ end
76
105
  write_log(response) if @client.options[:trace]
77
106
 
78
107
  Response.new(response.code, response.each {}, response.body)
79
108
  end
80
109
 
110
+ def escape_params(input)
111
+ input.map do |key, value|
112
+ "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"
113
+ end.join('&')
114
+ end
115
+
81
116
  def http_request
82
117
  request_url = @resource
83
118
 
84
119
  if @options[:params] && !@options[:params].empty?
85
- request_url += "?#{URI.encode_www_form(@options[:params])}"
120
+ request_url += "?#{escape_params(@options[:params])}"
86
121
  end
87
122
 
88
123
  request = HTTP_METHOD[@method].new(request_url)
@@ -9,9 +9,7 @@ module TwitterAds
9
9
  attr_reader :code,
10
10
  :headers,
11
11
  :raw_body,
12
- :body,
13
- :rate_limit_remaining,
14
- :rate_limit_reset
12
+ :body
15
13
 
16
14
  # Creates a new Response object instance.
17
15
  #
@@ -37,16 +35,6 @@ module TwitterAds
37
35
  @body = raw_body
38
36
  end
39
37
 
40
- if headers.key?('x-rate-limit-reset')
41
- @rate_limit = headers['x-rate-limit-limit'].first
42
- @rate_limit_remaining = headers['x-rate-limit-remaining'].first
43
- @rate_limit_reset = headers['x-rate-limit-reset'].first.to_i
44
- elsif headers.key?('x-cost-rate-limit-reset')
45
- @rate_limit = headers['x-cost-rate-limit-limit'].first
46
- @rate_limit_remaining = headers['x-cost-rate-limit-remaining'].first
47
- @rate_limit_reset = Time.at(headers['x-cost-rate-limit-reset'].first.to_i)
48
- end
49
-
50
38
  self
51
39
  end
52
40
 
@@ -5,15 +5,35 @@ require 'zlib'
5
5
  require 'open-uri'
6
6
 
7
7
  module TwitterAds
8
- module Analytics
8
+ class Analytics
9
9
 
10
+ include TwitterAds::DSL
11
+ include TwitterAds::Resource
10
12
  include TwitterAds::Enum
11
13
 
14
+ attr_reader :account
15
+
16
+ property :id, read_only: true
17
+ property :id_str, read_only: true
18
+ property :status, read_only: true
19
+ property :url, read_only: true
20
+ property :created_at, type: :time, read_only: true
21
+ property :expires_at, type: :time, read_only: true
22
+ property :updated_at, type: :time, read_only: true
23
+ property :start_time, type: :time, read_only: true
24
+ property :end_time, type: :time, read_only: true
25
+
26
+ property :entity, read_only: true
27
+ property :entity_ids, read_only: true
28
+ property :placement, read_only: true
29
+ property :granularity, read_only: true
30
+ property :metric_groups, read_only: true
31
+
12
32
  ANALYTICS_MAP = {
13
33
  'TwitterAds::Campaign' => Entity::CAMPAIGN,
14
34
  'TwitterAds::LineItem' => Entity::LINE_ITEM,
15
35
  'TwitterAds::OrganicTweet' => Entity::ORGANIC_TWEET,
16
- 'TwitterAds::Creative::PromotedAccount' => Entity::ACCOUNT,
36
+ 'TwitterAds::Creative::PromotedAccount' => Entity::PROMOTED_ACCOUNT,
17
37
  'TwitterAds::Creative::PromotedTweet' => Entity::PROMOTED_TWEET,
18
38
  'TwitterAds::Creative::MediaCreative' => Entity::MEDIA_CREATIVE
19
39
  }.freeze
@@ -25,42 +45,38 @@ module TwitterAds
25
45
  RESOURCE_ACTIVE_ENTITIES = "/#{TwitterAds::API_VERSION}/" +
26
46
  'stats/accounts/%{account_id}/active_entities' # @api private
27
47
 
28
- def self.included(klass)
29
- klass.send :include, InstanceMethods
30
- klass.extend ClassMethods
48
+ def initialize(account)
49
+ @account = account
50
+ self
31
51
  end
32
52
 
33
- module InstanceMethods
34
-
35
- # Pulls a list of metrics for the current object instance.
36
- #
37
- # @example
38
- # metric_groups = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
39
- # object.stats(metrics)
40
- #
41
- # @param metric_groups [Array] A collection of metric groups to fetch.
42
- # @param opts [Hash] An optional Hash of extended options.
43
- # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
44
- # @option opts [Time] :end_time The end time to use (default: now).
45
- # @option opts [Symbol] :granularity The granularity to use (default: :hour).
46
- #
47
- # @return [Array] The collection of stats requested.
48
- #
49
- # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
50
- # @since 1.0.0
51
- def stats(metric_groups, opts = {})
52
- self.class.stats(account, [id], metric_groups, opts)
53
- end
54
-
53
+ # Pulls a list of metrics for the current object instance.
54
+ #
55
+ # @example
56
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
57
+ # object.stats(metrics)
58
+ #
59
+ # @param metric_groups [Array] A collection of metric groups to fetch.
60
+ # @param opts [Hash] An optional Hash of extended options.
61
+ # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
62
+ # @option opts [Time] :end_time The end time to use (default: now).
63
+ # @option opts [Symbol] :granularity The granularity to use (default: :hour).
64
+ #
65
+ # @return [Array] The collection of stats requested.
66
+ #
67
+ # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
68
+ # @since 1.0.0
69
+ def stats(metric_groups, opts = {})
70
+ self.class.stats(account, [id], metric_groups, opts)
55
71
  end
56
72
 
57
- module ClassMethods
73
+ class << self
58
74
 
59
75
  # Pulls a list of metrics for a specified set of object IDs.
60
76
  #
61
77
  # @example
62
78
  # ids = ['7o4em', 'oc9ce', '1c5lji']
63
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
79
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
64
80
  # object.stats(account, ids, metric_groups)
65
81
  #
66
82
  # @param account [Account] The Account object instance.
@@ -70,7 +86,7 @@ module TwitterAds
70
86
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
71
87
  # @option opts [Time] :end_time The end time to use (default: now).
72
88
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
73
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
89
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
74
90
  #
75
91
  # @return [Array] The collection of stats requested.
76
92
  #
@@ -108,7 +124,7 @@ module TwitterAds
108
124
  #
109
125
  # @example
110
126
  # ids = ['7o4em', 'oc9ce', '1c5lji']
111
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
127
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
112
128
  # object.create_async_job(account, ids, metric_groups)
113
129
  #
114
130
  # @param account [Account] The Account object instance.
@@ -118,16 +134,17 @@ module TwitterAds
118
134
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
119
135
  # @option opts [Time] :end_time The end time to use (default: now).
120
136
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
121
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
122
- # @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
137
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
138
+ # @option opts [String] :segmentation_type The segmentation type to use (default: none).
123
139
  #
124
140
  # @return The response of creating job
125
141
  #
126
142
  # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
127
- # @sync 1.0.0
143
+ # @since 1.0.0
128
144
 
129
145
  def create_async_job(account, ids, metric_groups, opts = {})
130
146
  # set default metric values
147
+ entity = opts.fetch(:entity, name)
131
148
  end_time = opts.fetch(:end_time, (Time.now - Time.now.sec - (60 * Time.now.min)))
132
149
  start_time = opts.fetch(:start_time, end_time - 604_800) # 7 days ago
133
150
  granularity = opts.fetch(:granularity, :hour)
@@ -143,7 +160,7 @@ module TwitterAds
143
160
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
144
161
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset),
145
162
  granularity: granularity.to_s.upcase,
146
- entity: ANALYTICS_MAP[name],
163
+ entity: ANALYTICS_MAP[entity],
147
164
  placement: placement,
148
165
  country: country,
149
166
  platform: platform
@@ -153,19 +170,18 @@ module TwitterAds
153
170
  params['entity_ids'] = ids.join(',')
154
171
 
155
172
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
156
- puts 'my resource is ' + resource
157
173
  response = Request.new(account.client, :post, resource, params: params).perform
158
- response.body[:data]
174
+ TwitterAds::Analytics.new(account).from_response(response.body[:data], response.headers)
159
175
  end
160
176
 
161
177
  # Check async job status.
162
178
  # GET /#{TwitterAds::API_VERSION}/stats/jobs/accounts/:account_id
163
179
  #
164
180
  # @example
165
- # TwitterAds::LineItem.check_async_job_status(account, job_id: '1357343438724431305')
181
+ # TwitterAds::LineItem.check_async_job_status(account, job_ids: ['1357343438724431305'])
166
182
  #
167
183
  # @param account [Account] The Account object instance.
168
- # @option opts [String] :job_id The starting time to use (default: 7 days ago).
184
+ # @option opts [Array] :job_ids A collection of job IDs to fetch.
169
185
  #
170
186
  # @return A cursor of job statuses
171
187
 
@@ -173,18 +189,18 @@ module TwitterAds
173
189
  # set default values
174
190
  job_ids = opts.fetch(:job_ids, nil)
175
191
  params = {}
176
- params[:job_ids] = Array.wrap(job_ids).join(',') if job_ids
192
+ params[:job_ids] = job_ids.join(',') if job_ids
177
193
 
178
194
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
179
195
  request = Request.new(account.client, :get, resource, params: params)
180
- Cursor.new(nil, request, init_with: [account])
196
+ Cursor.new(TwitterAds::Analytics, request, init_with: [account])
181
197
  end
182
198
 
183
199
  # Fetch async job data for a completed job.
184
200
  # Raises HTTP 404 exception, otherwise retries up to 5 times with exponential backoff.
185
201
  #
186
202
  # @example
187
- # response_data = TwitterAds::LineItem.fetch_async_job_data(account, file_url)
203
+ # response_data = TwitterAds::LineItem.fetch_async_job_data(account, data_url)
188
204
  #
189
205
  # @param data_url [String] The URL from the successful completion of an async job.
190
206
  #
@@ -209,21 +225,57 @@ module TwitterAds
209
225
  end
210
226
  end
211
227
 
228
+ # Retrieve details about which entities' analytics metrics
229
+ # have changed in a given time period.
230
+ #
231
+ # @example
232
+ # time = Time.now
233
+ # utc_offset = '+09:00'
234
+ # start_time = time - (60 * 60 * 24) # -1 day
235
+ # end_time = time
236
+ # active_entities = TwitterAds::LineItem.active_entities(
237
+ # account,
238
+ # line_item_ids: %w(exrfs),
239
+ # start_time: start_time,
240
+ # end_time: end_time,
241
+ # utc_offset: utc_offset,
242
+ # granularity: :day)
243
+ #
244
+ # @param account [Account] The Account object instance.
245
+ # @param entity [String] The entity type to retrieve data for.
246
+ # @param start_time [Time] Scopes the retrieved data to the specified start time.
247
+ # @param end_time [Time] Scopes the retrieved data to the specified end time.
248
+ # @option opts [Array] :campaign_ids A collection of IDs to be fetched.
249
+ # @option opts [Array] :funding_instrument_ids A collection of IDs to be fetched.
250
+ # @option opts [Array] :line_item_ids A collection of IDs to be fetched.
251
+ #
252
+ # @return A list of entity details.
253
+ #
254
+ # @see https://developer.twitter.com/en/docs/ads/analytics/api-reference/active-entities
255
+
212
256
  def active_entities(account, start_time:, end_time:, **opts)
213
- entity_type = name
257
+ entity = opts.fetch(:entity, name)
214
258
  granularity = opts.fetch(:granularity, nil)
215
259
  start_utc_offset = opts[:start_utc_offset] || opts[:utc_offset]
216
260
  end_utc_offset = opts[:end_utc_offset] || opts[:utc_offset]
217
261
 
218
- if entity_type == 'OrganicTweet'
219
- raise "'OrganicTweet' not support with 'active_entities'"
262
+ if entity == 'OrganicTweet'
263
+ raise "'OrganicTweet' is not supported with 'active_entities'"
220
264
  end
221
265
 
222
266
  params = {
223
- entity: ANALYTICS_MAP[entity_type],
267
+ entity: ANALYTICS_MAP[entity],
224
268
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
225
269
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset)
226
- }.merge!(opts)
270
+ }
271
+
272
+ opts.each { |k, v|
273
+ params[k] = if v.instance_of?(Array)
274
+ v.join(',')
275
+ else
276
+ v
277
+ end
278
+ }
227
279
 
228
280
  resource = self::RESOURCE_ACTIVE_ENTITIES % { account_id: account.id }
229
281
  response = Request.new(account.client, :get, resource, params: params).perform
@@ -20,7 +20,14 @@ module TwitterAds
20
20
  # @return [self] A fully hydrated instance of the current class.
21
21
  #
22
22
  # @since 0.1.0
23
- def from_response(object)
23
+ def from_response(object, headers = nil)
24
+ if !headers.nil?
25
+ TwitterAds::Utils.extract_response_headers(headers).each { |key, value|
26
+ singleton_class.class_eval { attr_accessor key }
27
+ instance_variable_set("@#{key}", value)
28
+ }
29
+ end
30
+
24
31
  self.class.properties.each do |name, type|
25
32
  value = nil
26
33
  if type == :time && object[name] && !object[name].empty?
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ module TwitterRestApi
5
+ class UserIdLookup
6
+ include TwitterAds::DSL
7
+ include TwitterAds::Resource
8
+
9
+ attr_reader :account
10
+
11
+ property :id, read_only: true
12
+ property :id_str, read_only: true
13
+ property :screen_name, read_only: true
14
+
15
+ DOMAIN = 'https://api.twitter.com'
16
+ RESOURCE = '/1.1/users/show.json'
17
+
18
+ def self.load(account, opts = {})
19
+ response = TwitterAds::Request.new(
20
+ account.client,
21
+ :get,
22
+ RESOURCE,
23
+ params: opts,
24
+ domain: DOMAIN
25
+ ).perform
26
+ new.from_response(response.body, response.headers)
27
+ end
28
+ end
29
+ end
@@ -41,12 +41,24 @@ module TwitterAds
41
41
  property :to_delete, type: :bool
42
42
 
43
43
  RESOURCE = "/#{TwitterAds::API_VERSION}/" \
44
- 'accounts/%{account_id}/user_settings/%{id}' # @api private
44
+ 'accounts/%{account_id}/tax_settings' # @api private
45
45
 
46
46
  def initialize(account)
47
47
  @account = account
48
48
  self
49
49
  end
50
50
 
51
+ def self.load(account)
52
+ resource = RESOURCE % { account_id: account.id }
53
+ response = Request.new(account.client, :get, resource).perform
54
+ new(account).from_response(response.body[:data])
55
+ end
56
+
57
+ def save
58
+ resource = RESOURCE % { account_id: account.id }
59
+ params = to_params
60
+ response = Request.new(account.client, :put, resource, params: params).perform
61
+ from_response(response.body[:data])
62
+ end
51
63
  end
52
64
  end