twitter-ads 0.3.4
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +2 -0
- data/CONTRIBUTING.md +77 -0
- data/LICENSE +22 -0
- data/README.md +111 -0
- data/Rakefile +86 -0
- data/bin/twitter-ads +42 -0
- data/lib/twitter-ads.rb +54 -0
- data/lib/twitter-ads/account.rb +229 -0
- data/lib/twitter-ads/audiences/tailored_audience.rb +177 -0
- data/lib/twitter-ads/campaign/app_list.rb +42 -0
- data/lib/twitter-ads/campaign/campaign.rb +40 -0
- data/lib/twitter-ads/campaign/funding_instrument.rb +33 -0
- data/lib/twitter-ads/campaign/line_item.rb +91 -0
- data/lib/twitter-ads/campaign/promotable_user.rb +28 -0
- data/lib/twitter-ads/campaign/targeting_criteria.rb +77 -0
- data/lib/twitter-ads/campaign/tweet.rb +83 -0
- data/lib/twitter-ads/client.rb +92 -0
- data/lib/twitter-ads/creative/app_download_card.rb +44 -0
- data/lib/twitter-ads/creative/image_app_download_card.rb +44 -0
- data/lib/twitter-ads/creative/image_conversation_card.rb +44 -0
- data/lib/twitter-ads/creative/lead_gen_card.rb +46 -0
- data/lib/twitter-ads/creative/promoted_account.rb +38 -0
- data/lib/twitter-ads/creative/promoted_tweet.rb +87 -0
- data/lib/twitter-ads/creative/video.rb +43 -0
- data/lib/twitter-ads/creative/video_app_download_card.rb +47 -0
- data/lib/twitter-ads/creative/video_conversation_card.rb +46 -0
- data/lib/twitter-ads/creative/website_card.rb +48 -0
- data/lib/twitter-ads/cursor.rb +127 -0
- data/lib/twitter-ads/enum.rb +135 -0
- data/lib/twitter-ads/error.rb +93 -0
- data/lib/twitter-ads/http/request.rb +127 -0
- data/lib/twitter-ads/http/response.rb +74 -0
- data/lib/twitter-ads/http/ton_upload.rb +140 -0
- data/lib/twitter-ads/legacy.rb +7 -0
- data/lib/twitter-ads/resources/analytics.rb +90 -0
- data/lib/twitter-ads/resources/dsl.rb +108 -0
- data/lib/twitter-ads/resources/persistence.rb +43 -0
- data/lib/twitter-ads/resources/resource.rb +92 -0
- data/lib/twitter-ads/targeting/reach_estimate.rb +69 -0
- data/lib/twitter-ads/utils.rb +76 -0
- data/lib/twitter-ads/version.rb +6 -0
- data/spec/fixtures/accounts_all.json +65 -0
- data/spec/fixtures/accounts_features.json +18 -0
- data/spec/fixtures/accounts_load.json +19 -0
- data/spec/fixtures/app_lists_all.json +22 -0
- data/spec/fixtures/app_lists_load.json +31 -0
- data/spec/fixtures/campaigns_all.json +208 -0
- data/spec/fixtures/campaigns_load.json +27 -0
- data/spec/fixtures/funding_instruments_all.json +74 -0
- data/spec/fixtures/funding_instruments_load.json +28 -0
- data/spec/fixtures/line_items_all.json +292 -0
- data/spec/fixtures/line_items_load.json +36 -0
- data/spec/fixtures/placements.json +35 -0
- data/spec/fixtures/promotable_users_all.json +57 -0
- data/spec/fixtures/promotable_users_load.json +18 -0
- data/spec/fixtures/promoted_tweets_all.json +212 -0
- data/spec/fixtures/promoted_tweets_load.json +19 -0
- data/spec/fixtures/reach_estimate.json +19 -0
- data/spec/fixtures/tailored_audiences_all.json +67 -0
- data/spec/fixtures/tailored_audiences_load.json +29 -0
- data/spec/fixtures/tweet_preview.json +24 -0
- data/spec/fixtures/videos_all.json +50 -0
- data/spec/fixtures/videos_load.json +22 -0
- data/spec/quality_spec.rb +15 -0
- data/spec/shared/properties.rb +20 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/support/helpers.rb +42 -0
- data/spec/twitter-ads/account_spec.rb +315 -0
- data/spec/twitter-ads/audiences/tailored_audience_spec.rb +45 -0
- data/spec/twitter-ads/campaign/app_list_spec.rb +108 -0
- data/spec/twitter-ads/campaign/line_item_spec.rb +95 -0
- data/spec/twitter-ads/campaign/reach_estimate_spec.rb +98 -0
- data/spec/twitter-ads/campaign/targeting_criteria_spec.rb +39 -0
- data/spec/twitter-ads/campaign/tweet_spec.rb +83 -0
- data/spec/twitter-ads/client_spec.rb +115 -0
- data/spec/twitter-ads/creative/app_download_card_spec.rb +44 -0
- data/spec/twitter-ads/creative/image_app_download_card_spec.rb +43 -0
- data/spec/twitter-ads/creative/image_conversation_card_spec.rb +40 -0
- data/spec/twitter-ads/creative/lead_gen_card_spec.rb +46 -0
- data/spec/twitter-ads/creative/promoted_account_spec.rb +30 -0
- data/spec/twitter-ads/creative/promoted_tweet_spec.rb +46 -0
- data/spec/twitter-ads/creative/video_app_download_card_spec.rb +42 -0
- data/spec/twitter-ads/creative/video_conversation_card_spec.rb +52 -0
- data/spec/twitter-ads/creative/video_legacy_spec.rb +43 -0
- data/spec/twitter-ads/creative/video_spec.rb +43 -0
- data/spec/twitter-ads/creative/website_card_spec.rb +37 -0
- data/spec/twitter-ads/cursor_spec.rb +67 -0
- data/spec/twitter-ads/placements_spec.rb +36 -0
- data/spec/twitter-ads/utils_spec.rb +101 -0
- data/twitter-ads.gemspec +37 -0
- metadata +247 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
module TwitterAds
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
|
|
8
|
+
attr_reader :code, :headers, :response, :details
|
|
9
|
+
|
|
10
|
+
def initialize(*args)
|
|
11
|
+
if args.size == 1 && args[0].respond_to?(:body) && args[0].respond_to?(:code)
|
|
12
|
+
@response = args[0]
|
|
13
|
+
@code = args[0].code
|
|
14
|
+
@details = args[0].body[:errors] if args[0].body.is_a?(Hash) && args[0].body[:errors]
|
|
15
|
+
elsif args.size == 3
|
|
16
|
+
@response = args[0]
|
|
17
|
+
@details = args[1]
|
|
18
|
+
@code = args[2]
|
|
19
|
+
end
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def inspect
|
|
24
|
+
str = String.new("#<#{self.class.name}:0x#{object_id}")
|
|
25
|
+
str << " code=#{@code}" if @code
|
|
26
|
+
str << " details=\"#{@details}\"" if @details
|
|
27
|
+
str << '>'
|
|
28
|
+
end
|
|
29
|
+
alias to_s inspect
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
|
|
33
|
+
ERRORS = {
|
|
34
|
+
400 => 'TwitterAds::BadRequest',
|
|
35
|
+
401 => 'TwitterAds::NotAuthorized',
|
|
36
|
+
403 => 'TwitterAds::Forbidden',
|
|
37
|
+
404 => 'TwitterAds::NotFound',
|
|
38
|
+
429 => 'TwitterAds::RateLimit',
|
|
39
|
+
500 => 'TwitterAds::ServerError',
|
|
40
|
+
503 => 'TwitterAds::ServiceUnavailable'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Returns an appropriately typed Error object based from an API response.
|
|
44
|
+
#
|
|
45
|
+
# @param object [Hash] The parsed JSON API response.
|
|
46
|
+
#
|
|
47
|
+
# @return [Error] The error object instance.
|
|
48
|
+
#
|
|
49
|
+
# @since 0.1.0
|
|
50
|
+
# @api private
|
|
51
|
+
def from_response(object)
|
|
52
|
+
return class_eval(ERRORS[object.code]).new(object) if ERRORS.key?(object.code)
|
|
53
|
+
new(object) # fallback, unknown error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Server Errors (5XX)
|
|
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
|
|
74
|
+
|
|
75
|
+
# Client Errors (4XX)
|
|
76
|
+
class ClientError < Error; end
|
|
77
|
+
class NotAuthorized < ClientError; end
|
|
78
|
+
class Forbidden < ClientError; end
|
|
79
|
+
class NotFound < ClientError; end
|
|
80
|
+
class BadRequest < ClientError; end
|
|
81
|
+
|
|
82
|
+
class RateLimit < ClientError
|
|
83
|
+
attr_reader :reset_at, :retry_after
|
|
84
|
+
|
|
85
|
+
def initialize(object)
|
|
86
|
+
super object
|
|
87
|
+
@retry_after = object.headers['retry-after']
|
|
88
|
+
@reset_at = object.headers['rate_limit_reset']
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
module TwitterAds
|
|
5
|
+
|
|
6
|
+
# Generic container for API requests.
|
|
7
|
+
class Request
|
|
8
|
+
|
|
9
|
+
attr_reader :client, :method, :resource, :options
|
|
10
|
+
|
|
11
|
+
HTTP_METHOD = {
|
|
12
|
+
get: Net::HTTP::Get,
|
|
13
|
+
post: Net::HTTP::Post,
|
|
14
|
+
put: Net::HTTP::Put,
|
|
15
|
+
delete: Net::HTTP::Delete
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
DEFAULT_DOMAIN = 'https://ads-api.twitter.com'.freeze
|
|
19
|
+
SANDBOX_DOMAIN = 'https://ads-api-sandbox.twitter.com'.freeze
|
|
20
|
+
|
|
21
|
+
private_constant :DEFAULT_DOMAIN, :SANDBOX_DOMAIN, :HTTP_METHOD
|
|
22
|
+
|
|
23
|
+
# Creates a new Request object instance.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# request = Request.new(client, :get, '/0/accounts')
|
|
27
|
+
#
|
|
28
|
+
# @param client [Client] The Client object instance.
|
|
29
|
+
# @param method [Symbol] The HTTP method to be used.
|
|
30
|
+
# @param resource [String] The resource path for the request.
|
|
31
|
+
#
|
|
32
|
+
# @param opts [Hash] An optional Hash of extended options.
|
|
33
|
+
# @option opts [String] :domain Forced override for default domain to use for the request. This
|
|
34
|
+
# value will also override :sandbox mode on the client.
|
|
35
|
+
#
|
|
36
|
+
# @since 0.1.0
|
|
37
|
+
#
|
|
38
|
+
# @return [Request] The Request object instance.
|
|
39
|
+
def initialize(client, method, resource, opts = {})
|
|
40
|
+
@client = client
|
|
41
|
+
@method = method
|
|
42
|
+
@resource = resource
|
|
43
|
+
@options = opts
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Executes the current Request object.
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# request = Request.new(client, :get, '/0/accounts')
|
|
51
|
+
# request.perform
|
|
52
|
+
#
|
|
53
|
+
# @since 0.1.0
|
|
54
|
+
#
|
|
55
|
+
# @return [Response] The Response object instance generated by the Request.
|
|
56
|
+
def perform
|
|
57
|
+
handle_error(oauth_request)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def domain
|
|
63
|
+
@domain ||= begin
|
|
64
|
+
@options[:domain] || (@client.options[:sandbox] ? SANDBOX_DOMAIN : DEFAULT_DOMAIN)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def oauth_request
|
|
69
|
+
request = http_request
|
|
70
|
+
consumer = OAuth::Consumer.new(@client.consumer_key, @client.consumer_secret, site: domain)
|
|
71
|
+
token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
|
|
72
|
+
request.oauth!(consumer.http, consumer, token)
|
|
73
|
+
|
|
74
|
+
write_log(request) if @client.options[:trace]
|
|
75
|
+
response = consumer.http.request(request)
|
|
76
|
+
write_log(response) if @client.options[:trace]
|
|
77
|
+
|
|
78
|
+
Response.new(response.code, response.each {}, response.body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def http_request
|
|
82
|
+
request_url = @resource
|
|
83
|
+
if @options[:params] && !@options[:params].empty?
|
|
84
|
+
request_url += "?#{URI.encode_www_form(@options[:params])}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
request = HTTP_METHOD[@method].new(request_url)
|
|
88
|
+
request.body = @options[:body] if @options[:body]
|
|
89
|
+
|
|
90
|
+
@options[:headers].each { |header, value| request[header] = value } if @options[:headers]
|
|
91
|
+
request['user-agent'] = user_agent
|
|
92
|
+
|
|
93
|
+
request
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def user_agent
|
|
97
|
+
"twitter-ads version: #{TwitterAds::VERSION} " \
|
|
98
|
+
"platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write_log(object)
|
|
102
|
+
if object.respond_to?(:code)
|
|
103
|
+
@client.logger.info("Status: #{object.code} #{object.message}")
|
|
104
|
+
else
|
|
105
|
+
@client.logger.info("Send: #{object.method} #{domain}#{@resource} #{@options[:params]}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
object.each { |header| @client.logger.info("Header: #{header}: #{object[header]}") }
|
|
109
|
+
|
|
110
|
+
# suppresses body content for non-Ads API domains (eg. upload.twitter.com)
|
|
111
|
+
if object.body && !object.body.empty?
|
|
112
|
+
if @domain == SANDBOX_DOMAIN || @domain == DEFAULT_DOMAIN
|
|
113
|
+
@client.logger.info("Body: #{object.body}")
|
|
114
|
+
else
|
|
115
|
+
@client.logger.info('Body: **OMITTED**')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def handle_error(response)
|
|
121
|
+
raise TwitterAds::Error.from_response(response) unless response.code < 400
|
|
122
|
+
response
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
module TwitterAds
|
|
5
|
+
|
|
6
|
+
# Generic container for API responses.
|
|
7
|
+
class Response
|
|
8
|
+
|
|
9
|
+
attr_reader :code,
|
|
10
|
+
:headers,
|
|
11
|
+
:raw_body,
|
|
12
|
+
:body,
|
|
13
|
+
:rate_limit_remaining,
|
|
14
|
+
:rate_limit_reset
|
|
15
|
+
|
|
16
|
+
# Creates a new Response object instance.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# response = Response.new(code, headers, body)
|
|
20
|
+
#
|
|
21
|
+
# @param code [String] The HTTP status code.
|
|
22
|
+
# @param headers [Hash] A Hash object containing HTTP response headers.
|
|
23
|
+
# @param body [String] The response body.
|
|
24
|
+
#
|
|
25
|
+
# @since 0.1.0
|
|
26
|
+
#
|
|
27
|
+
# @return [Response] The Response object instance.
|
|
28
|
+
def initialize(code, headers, body)
|
|
29
|
+
@code = code.to_i
|
|
30
|
+
@headers = headers
|
|
31
|
+
@raw_body = body
|
|
32
|
+
|
|
33
|
+
# handle non-JSON responses
|
|
34
|
+
begin
|
|
35
|
+
@body = TwitterAds::Utils.symbolize!(MultiJson.load(body))
|
|
36
|
+
rescue MultiJson::ParseError
|
|
37
|
+
@body = raw_body
|
|
38
|
+
end
|
|
39
|
+
|
|
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
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns an inspection string for the current Response instance.
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# response.inspect
|
|
57
|
+
#
|
|
58
|
+
# @since 0.1.0
|
|
59
|
+
#
|
|
60
|
+
# @return [String] The inspection string.
|
|
61
|
+
def inspect
|
|
62
|
+
"#<#{self.class.name}:0x#{object_id} code=\"#{@code}\" error=\"#{error?}\">"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Helper method for determining if the current Response contains an error.
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean] True or false indicating if this Response contains an error.
|
|
68
|
+
def error?
|
|
69
|
+
@error ||= (@code >= 400 && @code <= 599)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
module TwitterAds
|
|
5
|
+
|
|
6
|
+
# Specialized request class for TON API uploads.
|
|
7
|
+
class TONUpload
|
|
8
|
+
|
|
9
|
+
DEFAULT_DOMAIN = 'https://ton.twitter.com'.freeze # @api private
|
|
10
|
+
DEFAULT_RESOURCE = '/1.1/ton/bucket/'.freeze # @api private
|
|
11
|
+
DEFAULT_BUCKET = 'ta_partner'.freeze # @api private
|
|
12
|
+
DEFAULT_EXPIRE = (Time.now + 10 * 24 * 60 * 60).httpdate # @api private
|
|
13
|
+
MIN_FILE_SIZE = 1024 * 1024 * 1 # @api private
|
|
14
|
+
|
|
15
|
+
private_constant :DEFAULT_DOMAIN,
|
|
16
|
+
:DEFAULT_BUCKET,
|
|
17
|
+
:DEFAULT_EXPIRE,
|
|
18
|
+
:DEFAULT_RESOURCE,
|
|
19
|
+
:MIN_FILE_SIZE
|
|
20
|
+
|
|
21
|
+
# Creates a new TONUpload object instance.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# request = TONUpload.new(client, '/path/to/file')
|
|
25
|
+
#
|
|
26
|
+
# @param client [Client] The Client object instance.
|
|
27
|
+
# @param file_path [String] The path to the file to be uploaded.
|
|
28
|
+
#
|
|
29
|
+
# @since 0.3.0
|
|
30
|
+
#
|
|
31
|
+
# @return [TONUpload] The TONUpload request instance.
|
|
32
|
+
def initialize(client, file_path, opts = {})
|
|
33
|
+
@file_path = File.expand_path(file_path)
|
|
34
|
+
unless File.exist?(file_path)
|
|
35
|
+
raise ArgumentError.new("Error! The specified file does not exist. (#{file_path})")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@file_size = File.size(@file_path)
|
|
39
|
+
|
|
40
|
+
@client = client
|
|
41
|
+
@bucket = opts.delete(:bucket) || DEFAULT_BUCKET
|
|
42
|
+
@options = opts
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Executes the current TONUpload object.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# request = TONUpload.new(client, '/path/to/file')
|
|
50
|
+
# request.perform
|
|
51
|
+
#
|
|
52
|
+
# @since 0.3.0
|
|
53
|
+
#
|
|
54
|
+
# @return [String] The upload location provided by the TON API.
|
|
55
|
+
def perform
|
|
56
|
+
if @file_size < MIN_FILE_SIZE
|
|
57
|
+
resource = "#{DEFAULT_RESOURCE}#{@bucket}"
|
|
58
|
+
response = upload(resource, File.read(@file_path))
|
|
59
|
+
response.headers['location'][0]
|
|
60
|
+
else
|
|
61
|
+
response = init_chunked_upload
|
|
62
|
+
chunk_size = response.headers['x-ton-min-chunk-size'][0].to_i
|
|
63
|
+
location = response.headers['location'][0]
|
|
64
|
+
|
|
65
|
+
File.open(@file_path) do |file|
|
|
66
|
+
bytes_read = 0
|
|
67
|
+
while bytes = file.read(chunk_size)
|
|
68
|
+
bytes_start = bytes_read
|
|
69
|
+
bytes_read += bytes.size
|
|
70
|
+
upload_chunk(location, chunk_size, bytes, bytes_start, bytes_read)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
location
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns an inspection string for the current object instance.
|
|
79
|
+
#
|
|
80
|
+
# @since 0.3.0
|
|
81
|
+
#
|
|
82
|
+
# @return [String] The object instance detail.
|
|
83
|
+
def inspect
|
|
84
|
+
"#<#{self.class.name}:0x#{object_id} bucket=\"#{@bucket}\" file=\"#{@file_path}\">"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# performs a single chunk upload
|
|
90
|
+
def upload(resource, bytes)
|
|
91
|
+
headers = {
|
|
92
|
+
'x-ton-expires' => DEFAULT_EXPIRE,
|
|
93
|
+
'content-length' => @file_size,
|
|
94
|
+
'content-type' => content_type
|
|
95
|
+
}
|
|
96
|
+
TwitterAds::Request.new(
|
|
97
|
+
@client, :post, resource, domain: DEFAULT_DOMAIN, headers: headers, body: bytes).perform
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# initialization for a multi-chunk upload
|
|
101
|
+
def init_chunked_upload
|
|
102
|
+
headers = {
|
|
103
|
+
'x-ton-content-type' => content_type,
|
|
104
|
+
'x-ton-content-length' => @file_size,
|
|
105
|
+
'x-ton-expires' => DEFAULT_EXPIRE,
|
|
106
|
+
'content-length' => 0,
|
|
107
|
+
'content-type' => content_type
|
|
108
|
+
}
|
|
109
|
+
resource = "#{DEFAULT_RESOURCE}#{@bucket}?resumable=true"
|
|
110
|
+
TwitterAds::Request.new(
|
|
111
|
+
@client, :post, resource, domain: DEFAULT_DOMAIN, headers: headers).perform
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# uploads a single chunk of a multi-chunk upload
|
|
115
|
+
def upload_chunk(resource, chunk_size, bytes, bytes_start, bytes_read)
|
|
116
|
+
headers = {
|
|
117
|
+
'content-type' => content_type,
|
|
118
|
+
'content-length' => [chunk_size, @file_size - bytes_read].min,
|
|
119
|
+
'content-range' => "bytes #{bytes_start}-#{bytes_read - 1}/#{@file_size}"
|
|
120
|
+
}
|
|
121
|
+
TwitterAds::Request.new(
|
|
122
|
+
@client, :put, resource, domain: DEFAULT_DOMAIN, headers: headers, body: bytes).perform
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def content_type
|
|
126
|
+
@content_type ||= begin
|
|
127
|
+
extension = File.extname(@file_path).downcase
|
|
128
|
+
if extension == '.csv'
|
|
129
|
+
'text/csv'
|
|
130
|
+
elsif extension == '.tsv'
|
|
131
|
+
'text/tab-separated-values'
|
|
132
|
+
else
|
|
133
|
+
'text/plain'
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
# legacy namespace support, to be removed in v1.0.0 (next major)
|
|
5
|
+
TwitterAds::Objective = TwitterAds::Enum::Objective
|
|
6
|
+
TwitterAds::Product = TwitterAds::Enum::Product
|
|
7
|
+
TwitterAds::Placement = TwitterAds::Enum::Placement
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Copyright (C) 2015 Twitter, Inc.
|
|
3
|
+
|
|
4
|
+
module TwitterAds
|
|
5
|
+
module Analytics
|
|
6
|
+
|
|
7
|
+
CLASS_ID_MAP = {
|
|
8
|
+
'TwitterAds::LineItem' => :line_item_ids,
|
|
9
|
+
'TwitterAds::OrganicTweet' => :tweet_ids,
|
|
10
|
+
'TwitterAds::Tweet' => :tweet_ids,
|
|
11
|
+
'TwitterAds::Creative::PromotedTweet' => :promoted_tweet_ids
|
|
12
|
+
}.freeze # @api private
|
|
13
|
+
|
|
14
|
+
def self.included(klass)
|
|
15
|
+
klass.send :include, InstanceMethods
|
|
16
|
+
klass.extend ClassMethods
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module InstanceMethods
|
|
20
|
+
|
|
21
|
+
# Pulls a list of metrics for the current object instance.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# metrics = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
|
|
25
|
+
# object.stats(metrics)
|
|
26
|
+
#
|
|
27
|
+
# @param metrics [Array] A collection of valid metrics to be fetched.
|
|
28
|
+
# @param opts [Hash] An optional Hash of extended options.
|
|
29
|
+
# @option opts [Time] :start_time The starting time to use (default: 7 days ago).
|
|
30
|
+
# @option opts [Time] :end_time The end time to use (default: now).
|
|
31
|
+
# @option opts [Symbol] :granularity The granularity to use (default: :hour).
|
|
32
|
+
# @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
|
|
33
|
+
#
|
|
34
|
+
# @return [Array] The collection of stats requested.
|
|
35
|
+
#
|
|
36
|
+
# @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
|
|
37
|
+
# @since 0.1.0
|
|
38
|
+
def stats(metrics, opts = {})
|
|
39
|
+
self.class.stats(account, [id], metrics, opts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module ClassMethods
|
|
45
|
+
|
|
46
|
+
# Pulls a list of metrics for a specified set of object IDs.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# ids = ['7o4em', 'oc9ce', '1c5lji']
|
|
50
|
+
# metrics = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
|
|
51
|
+
# object.stats(account, ids, metrics)
|
|
52
|
+
#
|
|
53
|
+
# @param account [Account] The Account object instance.
|
|
54
|
+
# @param ids [Array] A collection of object IDs being targeted.
|
|
55
|
+
# @param metrics [Array] A collection of valid metrics to be fetched.
|
|
56
|
+
# @param opts [Hash] An optional Hash of extended options.
|
|
57
|
+
# @option opts [Time] :start_time The starting time to use (default: 7 days ago).
|
|
58
|
+
# @option opts [Time] :end_time The end time to use (default: now).
|
|
59
|
+
# @option opts [Symbol] :granularity The granularity to use (default: :hour).
|
|
60
|
+
# @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
|
|
61
|
+
#
|
|
62
|
+
# @return [Array] The collection of stats requested.
|
|
63
|
+
#
|
|
64
|
+
# @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
|
|
65
|
+
# @since 0.1.0
|
|
66
|
+
def stats(account, ids, metrics, opts = {})
|
|
67
|
+
# set default metric values
|
|
68
|
+
end_time = opts.fetch(:end_time, Time.now)
|
|
69
|
+
start_time = opts.fetch(:start_time, end_time - 604_800) # 7 days ago
|
|
70
|
+
granularity = opts.fetch(:granularity, :hour)
|
|
71
|
+
segmentation_type = opts.fetch(:segmentation_type, nil)
|
|
72
|
+
|
|
73
|
+
params = {
|
|
74
|
+
metrics: metrics.join(','),
|
|
75
|
+
start_time: TwitterAds::Utils.to_time(start_time, granularity),
|
|
76
|
+
end_time: TwitterAds::Utils.to_time(end_time, granularity),
|
|
77
|
+
granularity: granularity.to_s.upcase
|
|
78
|
+
}
|
|
79
|
+
params[:segmentation_type] = segmentation_type.to_s.upcase if segmentation_type
|
|
80
|
+
params[TwitterAds::Analytics::CLASS_ID_MAP[name]] = ids.join(',')
|
|
81
|
+
|
|
82
|
+
resource = self::RESOURCE_STATS % { account_id: account.id }
|
|
83
|
+
response = Request.new(account.client, :get, resource, params: params).perform
|
|
84
|
+
response.body[:data]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
end
|