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