buzzrb 0.1.0
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
- data/LICENSE.md +8 -0
- data/buzzrb.gemspec +20 -0
- data/lib/buzz/client.rb +240 -0
- data/lib/buzz/configuration.rb +25 -0
- data/lib/buzz/cookie_jar.rb +100 -0
- data/lib/buzz/error.rb +26 -0
- data/lib/buzz/paginator.rb +52 -0
- data/lib/buzz/resource.rb +41 -0
- data/lib/buzz/resources/advertiser.rb +13 -0
- data/lib/buzz/resources/campaign.rb +13 -0
- data/lib/buzz/resources/creative.rb +13 -0
- data/lib/buzz/resources/creative_asset.rb +13 -0
- data/lib/buzz/resources/creative_line_item.rb +39 -0
- data/lib/buzz/resources/line_item.rb +13 -0
- data/lib/buzz/resources/reporting.rb +37 -0
- data/lib/buzz/resources/search.rb +13 -0
- data/lib/buzz/resources/segment.rb +13 -0
- data/lib/buzz/resources/targeting.rb +13 -0
- data/lib/buzz/response.rb +45 -0
- data/lib/buzz/version.rb +5 -0
- data/lib/buzz.rb +36 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 77debe2be42e0aaef64398c094531219d7a7bebaa5be3855035f8047f7ff5807
|
|
4
|
+
data.tar.gz: 4d34d129f1d710eb5f5f87a849aefe0853a91c4be6659f2161610feb32b44656
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 88998110007260bf612ece0a21f5674f2d6ecb87db008ea183ec0ecba4c32a0d42f49038b1552ed97d03ae5b4155ff2b37ebe6def2521f7fe3beeda82ae777c8
|
|
7
|
+
data.tar.gz: 81d7ef463ce8eda9c599932037c5bd7236e9907d2d38831baadcc4ddec7aecaa581163c0a928d4b309309d1cc84b8f9fef6704f36b05fb76a29b1e4bde2e07b5
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright © 2026 swlkr
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/buzzrb.gemspec
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/buzz/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "buzzrb"
|
|
7
|
+
spec.version = Buzz::VERSION
|
|
8
|
+
spec.authors = ["Letterpress"]
|
|
9
|
+
spec.summary = "Ruby client for the Buzz/Freewheel Advertiser API v2.0"
|
|
10
|
+
spec.homepage = "https://github.com/getletterpress/buzzrb"
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
spec.required_ruby_version = ">= 3.0"
|
|
13
|
+
|
|
14
|
+
spec.files = Dir["lib/**/*.rb"] + ["buzzrb.gemspec", "LICENSE.md"]
|
|
15
|
+
spec.require_paths = ["lib"]
|
|
16
|
+
|
|
17
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
18
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
19
|
+
spec.add_development_dependency "webrick", "~> 1.8"
|
|
20
|
+
end
|
data/lib/buzz/client.rb
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Buzz
|
|
8
|
+
class Client
|
|
9
|
+
attr_reader :config, :cookie_jar
|
|
10
|
+
|
|
11
|
+
def initialize(buzz_key: nil, email: nil, password: nil, **options)
|
|
12
|
+
@config = if buzz_key
|
|
13
|
+
Configuration.new.tap do |c|
|
|
14
|
+
c.buzz_key = buzz_key
|
|
15
|
+
c.email = email
|
|
16
|
+
c.password = password
|
|
17
|
+
options.each { |k, v| c.public_send(:"#{k}=", v) }
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
Buzz.configuration.dup
|
|
21
|
+
end
|
|
22
|
+
@config.validate!
|
|
23
|
+
@cookie_jar = CookieJar.new
|
|
24
|
+
@http = nil
|
|
25
|
+
@authenticated = false
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get(path, params = {})
|
|
30
|
+
request(:get, path, params: params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def post(path, body = {})
|
|
34
|
+
request(:post, path, body: body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def put(path, body = {})
|
|
38
|
+
request(:put, path, body: body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(path, params = {})
|
|
42
|
+
request(:delete, path, params: params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def authenticate
|
|
46
|
+
uri = build_uri("/rest/v2/authenticate")
|
|
47
|
+
body = { email: config.email, password: config.password }
|
|
48
|
+
body[:account_id] = config.account_id if config.account_id
|
|
49
|
+
|
|
50
|
+
req = build_request(:post, uri)
|
|
51
|
+
req.body = JSON.generate(body)
|
|
52
|
+
|
|
53
|
+
response = execute(uri, req)
|
|
54
|
+
|
|
55
|
+
unless response.success?
|
|
56
|
+
raise AuthenticationError.new(
|
|
57
|
+
"Authentication failed: #{response.body}",
|
|
58
|
+
status: response.status,
|
|
59
|
+
body: response.data
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@authenticated = true
|
|
64
|
+
|
|
65
|
+
if config.keep_logged_in
|
|
66
|
+
keep_logged_in
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
response
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def authenticated?
|
|
73
|
+
@authenticated
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Resource accessors
|
|
77
|
+
def advertisers
|
|
78
|
+
Resources::Advertiser.new(self)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def campaigns
|
|
82
|
+
Resources::Campaign.new(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def line_items
|
|
86
|
+
Resources::LineItem.new(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def creatives
|
|
90
|
+
Resources::Creative.new(self)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def segments
|
|
94
|
+
Resources::Segment.new(self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def targeting
|
|
98
|
+
Resources::Targeting.new(self)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def creative_assets
|
|
102
|
+
Resources::CreativeAsset.new(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def creative_line_items(line_item_id)
|
|
106
|
+
Resources::CreativeLineItem.new(self, line_item_id)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def reporting
|
|
110
|
+
Resources::Reporting.new(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def search(query:, types: nil)
|
|
114
|
+
params = { q: query }
|
|
115
|
+
params[:entity_types] = Array(types).map(&:to_s).join(",") if types
|
|
116
|
+
get("/rest/v2/search", params)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def keep_logged_in
|
|
122
|
+
uri = build_uri("/rest/v2/authenticate/keep_logged_in")
|
|
123
|
+
req = build_request(:post, uri)
|
|
124
|
+
req.body = JSON.generate({})
|
|
125
|
+
execute(uri, req)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def request(method, path, params: {}, body: nil)
|
|
129
|
+
ensure_authenticated
|
|
130
|
+
|
|
131
|
+
uri = build_uri(path, params)
|
|
132
|
+
req = build_request(method, uri)
|
|
133
|
+
req.body = JSON.generate(body) if body
|
|
134
|
+
|
|
135
|
+
response = execute(uri, req)
|
|
136
|
+
|
|
137
|
+
if response.status == 401
|
|
138
|
+
@authenticated = false
|
|
139
|
+
authenticate
|
|
140
|
+
uri = build_uri(path, params)
|
|
141
|
+
req = build_request(method, uri)
|
|
142
|
+
req.body = JSON.generate(body) if body
|
|
143
|
+
response = execute(uri, req)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
handle_errors(response)
|
|
147
|
+
response
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ensure_authenticated
|
|
151
|
+
authenticate unless @authenticated
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_uri(path, params = {})
|
|
155
|
+
uri = URI.parse("#{config.base_url}#{path}")
|
|
156
|
+
if params && !params.empty?
|
|
157
|
+
query = params.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }
|
|
158
|
+
uri.query = query.join("&")
|
|
159
|
+
end
|
|
160
|
+
uri
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_request(method, uri)
|
|
164
|
+
klass = case method
|
|
165
|
+
when :get then Net::HTTP::Get
|
|
166
|
+
when :post then Net::HTTP::Post
|
|
167
|
+
when :put then Net::HTTP::Put
|
|
168
|
+
when :delete then Net::HTTP::Delete
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
req = klass.new(uri)
|
|
172
|
+
req["Content-Type"] = "application/json"
|
|
173
|
+
req["Accept"] = "application/json"
|
|
174
|
+
req["X-Timezone"] = config.timezone if config.timezone
|
|
175
|
+
|
|
176
|
+
cookie = cookie_jar.cookie_header(uri)
|
|
177
|
+
req["Cookie"] = cookie if cookie
|
|
178
|
+
|
|
179
|
+
req
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def execute(uri, req)
|
|
183
|
+
http = connection_for(uri)
|
|
184
|
+
raw = http.request(req)
|
|
185
|
+
|
|
186
|
+
# Parse cookies from response
|
|
187
|
+
if raw.get_fields("Set-Cookie")
|
|
188
|
+
raw.get_fields("Set-Cookie").each do |cookie_str|
|
|
189
|
+
cookie_jar.parse(cookie_str, uri)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
Response.new(raw)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def connection_for(uri)
|
|
197
|
+
@mutex.synchronize do
|
|
198
|
+
if @http.nil? || !@http.started?
|
|
199
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
|
200
|
+
@http.use_ssl = uri.scheme == "https"
|
|
201
|
+
@http.open_timeout = config.open_timeout
|
|
202
|
+
@http.read_timeout = config.read_timeout
|
|
203
|
+
@http.keep_alive_timeout = 30
|
|
204
|
+
@http.start
|
|
205
|
+
end
|
|
206
|
+
@http
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def handle_errors(response)
|
|
211
|
+
case response.status
|
|
212
|
+
when 200..299
|
|
213
|
+
# success
|
|
214
|
+
when 400
|
|
215
|
+
raise ValidationError.new(error_message(response), status: 400, body: response.data)
|
|
216
|
+
when 401
|
|
217
|
+
raise AuthenticationError.new(error_message(response), status: 401, body: response.data)
|
|
218
|
+
when 404
|
|
219
|
+
raise NotFoundError.new(error_message(response), status: 404, body: response.data)
|
|
220
|
+
when 429
|
|
221
|
+
retry_after = response.headers["retry-after"]&.first&.to_i
|
|
222
|
+
raise RateLimitError.new(error_message(response), status: 429, body: response.data, retry_after: retry_after)
|
|
223
|
+
when 500..599
|
|
224
|
+
raise ServerError.new(error_message(response), status: response.status, body: response.data)
|
|
225
|
+
else
|
|
226
|
+
raise Error.new(error_message(response), status: response.status, body: response.data)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def error_message(response)
|
|
231
|
+
if response.data.is_a?(Hash) && response.data["message"]
|
|
232
|
+
response.data["message"]
|
|
233
|
+
elsif response.data.is_a?(Hash) && response.data["detail"]
|
|
234
|
+
response.data["detail"]
|
|
235
|
+
else
|
|
236
|
+
"HTTP #{response.status}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :buzz_key, :email, :password, :keep_logged_in,
|
|
6
|
+
:account_id, :timezone, :open_timeout, :read_timeout
|
|
7
|
+
attr_writer :base_url
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@keep_logged_in = true
|
|
11
|
+
@open_timeout = 10
|
|
12
|
+
@read_timeout = 30
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def base_url
|
|
16
|
+
@base_url || "https://#{buzz_key}.api.beeswax.com"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate!
|
|
20
|
+
raise ArgumentError, "buzz_key is required" unless buzz_key && !buzz_key.empty?
|
|
21
|
+
raise ArgumentError, "email is required" unless email && !email.empty?
|
|
22
|
+
raise ArgumentError, "password is required" unless password && !password.empty?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Buzz
|
|
7
|
+
class CookieJar
|
|
8
|
+
Cookie = Struct.new(:name, :value, :domain, :path, :expires, :secure, :httponly, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@cookies = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse(set_cookie_header, request_uri)
|
|
16
|
+
return unless set_cookie_header
|
|
17
|
+
|
|
18
|
+
headers = set_cookie_header.is_a?(Array) ? set_cookie_header : [set_cookie_header]
|
|
19
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
|
20
|
+
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
headers.each do |header|
|
|
23
|
+
cookie = parse_cookie(header, uri)
|
|
24
|
+
@cookies[cookie.name] = cookie if cookie
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cookie_header(uri)
|
|
30
|
+
uri = uri.is_a?(URI) ? uri : URI.parse(uri)
|
|
31
|
+
now = Time.now
|
|
32
|
+
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
matching = @cookies.values.select do |cookie|
|
|
35
|
+
next false if cookie.expires && cookie.expires < now
|
|
36
|
+
next false if cookie.secure && uri.scheme != "https"
|
|
37
|
+
path_matches?(uri.path, cookie.path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return nil if matching.empty?
|
|
41
|
+
matching.map { |c| "#{c.name}=#{c.value}" }.join("; ")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clear
|
|
46
|
+
@mutex.synchronize { @cookies.clear }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def empty?
|
|
50
|
+
@mutex.synchronize { @cookies.empty? }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size
|
|
54
|
+
@mutex.synchronize { @cookies.size }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def parse_cookie(header, uri)
|
|
60
|
+
parts = header.split(";").map(&:strip)
|
|
61
|
+
return nil if parts.empty?
|
|
62
|
+
|
|
63
|
+
name_value = parts.shift
|
|
64
|
+
eq_index = name_value.index("=")
|
|
65
|
+
return nil unless eq_index
|
|
66
|
+
|
|
67
|
+
name = name_value[0...eq_index].strip
|
|
68
|
+
value = name_value[(eq_index + 1)..].strip
|
|
69
|
+
return nil if name.empty?
|
|
70
|
+
|
|
71
|
+
attrs = { name: name, value: value, domain: uri.host, path: "/", secure: false, httponly: false }
|
|
72
|
+
|
|
73
|
+
parts.each do |part|
|
|
74
|
+
key, val = part.split("=", 2).map(&:strip)
|
|
75
|
+
case key.downcase
|
|
76
|
+
when "domain"
|
|
77
|
+
attrs[:domain] = val&.delete_prefix(".") if val
|
|
78
|
+
when "path"
|
|
79
|
+
attrs[:path] = val if val
|
|
80
|
+
when "expires"
|
|
81
|
+
attrs[:expires] = Time.parse(val) rescue nil if val
|
|
82
|
+
when "max-age"
|
|
83
|
+
attrs[:expires] = Time.now + val.to_i if val
|
|
84
|
+
when "secure"
|
|
85
|
+
attrs[:secure] = true
|
|
86
|
+
when "httponly"
|
|
87
|
+
attrs[:httponly] = true
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Cookie.new(**attrs)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def path_matches?(request_path, cookie_path)
|
|
95
|
+
request_path = "/" if request_path.nil? || request_path.empty?
|
|
96
|
+
cookie_path = "/" if cookie_path.nil? || cookie_path.empty?
|
|
97
|
+
request_path.start_with?(cookie_path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/buzz/error.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :status, :body
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
8
|
+
@status = status
|
|
9
|
+
@body = body
|
|
10
|
+
super(message)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthenticationError < Error; end
|
|
15
|
+
class RateLimitError < Error
|
|
16
|
+
attr_reader :retry_after
|
|
17
|
+
|
|
18
|
+
def initialize(message = nil, status: nil, body: nil, retry_after: nil)
|
|
19
|
+
@retry_after = retry_after
|
|
20
|
+
super(message, status: status, body: body)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
class NotFoundError < Error; end
|
|
24
|
+
class ValidationError < Error; end
|
|
25
|
+
class ServerError < Error; end
|
|
26
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
class Paginator
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
attr_reader :response
|
|
8
|
+
|
|
9
|
+
def initialize(client, path, params = {})
|
|
10
|
+
@client = client
|
|
11
|
+
@path = path
|
|
12
|
+
@params = params
|
|
13
|
+
@response = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def each_page
|
|
17
|
+
return enum_for(:each_page) unless block_given?
|
|
18
|
+
|
|
19
|
+
@response = @client.get(@path, @params)
|
|
20
|
+
yield @response
|
|
21
|
+
|
|
22
|
+
while @response.next_url
|
|
23
|
+
next_path = URI.parse(@response.next_url).path
|
|
24
|
+
next_params = parse_query(URI.parse(@response.next_url).query)
|
|
25
|
+
@response = @client.get(next_path, next_params)
|
|
26
|
+
yield @response
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
return enum_for(:each) unless block_given?
|
|
32
|
+
|
|
33
|
+
each_page do |page|
|
|
34
|
+
results = page.results || []
|
|
35
|
+
results.each(&block)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_a
|
|
40
|
+
entries = []
|
|
41
|
+
each { |item| entries << item }
|
|
42
|
+
entries
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse_query(query_string)
|
|
48
|
+
return {} if query_string.nil? || query_string.empty?
|
|
49
|
+
URI.decode_www_form(query_string).to_h.transform_keys(&:to_sym)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
class Resource
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def list(params = {})
|
|
12
|
+
Paginator.new(client, resource_path, params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find(id)
|
|
16
|
+
response = client.get("#{resource_path}/#{id}")
|
|
17
|
+
response.data
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create(attributes = {})
|
|
21
|
+
response = client.post(resource_path, attributes)
|
|
22
|
+
response.data
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update(id, attributes = {})
|
|
26
|
+
response = client.put("#{resource_path}/#{id}", attributes)
|
|
27
|
+
response.data
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete(id)
|
|
31
|
+
response = client.delete("#{resource_path}/#{id}")
|
|
32
|
+
response.data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resource_path
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #resource_path"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
module Resources
|
|
5
|
+
class CreativeLineItem
|
|
6
|
+
attr_reader :client, :line_item_id
|
|
7
|
+
|
|
8
|
+
def initialize(client, line_item_id)
|
|
9
|
+
@client = client
|
|
10
|
+
@line_item_id = line_item_id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def list(params = {})
|
|
14
|
+
Paginator.new(client, base_path, params)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create(attributes = {})
|
|
18
|
+
response = client.post(base_path, attributes)
|
|
19
|
+
response.data
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find(id)
|
|
23
|
+
response = client.get("#{base_path}/#{id}")
|
|
24
|
+
response.data
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete(id)
|
|
28
|
+
response = client.delete("#{base_path}/#{id}")
|
|
29
|
+
response.data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def base_path
|
|
35
|
+
"/rest/v2/line-items/#{line_item_id}/creatives"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Buzz
|
|
4
|
+
module Resources
|
|
5
|
+
class Reporting
|
|
6
|
+
attr_reader :client
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Query performance data (spend, impressions, clicks, etc.)
|
|
13
|
+
def query(dimensions: [], metrics: [], **params)
|
|
14
|
+
body = { dimensions: dimensions, metrics: metrics }.merge(params)
|
|
15
|
+
response = client.post("/rest/v2/report-data", body)
|
|
16
|
+
response.data["results"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# CRUD on saved report definitions
|
|
20
|
+
def list(params = {})
|
|
21
|
+
Paginator.new(client, "/rest/v2/reports", params)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find(id)
|
|
25
|
+
client.get("/rest/v2/reports/#{id}").data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create(attributes = {})
|
|
29
|
+
client.post("/rest/v2/reports", attributes).data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(id)
|
|
33
|
+
client.delete("/rest/v2/reports/#{id}").data
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Buzz
|
|
6
|
+
class Response
|
|
7
|
+
attr_reader :status, :headers, :body, :data
|
|
8
|
+
|
|
9
|
+
def initialize(http_response)
|
|
10
|
+
@status = http_response.code.to_i
|
|
11
|
+
@headers = http_response.to_hash
|
|
12
|
+
@body = http_response.body
|
|
13
|
+
@data = parse_body
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success?
|
|
17
|
+
status >= 200 && status < 300
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def results
|
|
21
|
+
data["results"] if data.is_a?(Hash)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def count
|
|
25
|
+
data["count"] if data.is_a?(Hash)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def next_url
|
|
29
|
+
data["next"] if data.is_a?(Hash)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def previous_url
|
|
33
|
+
data["previous"] if data.is_a?(Hash)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def parse_body
|
|
39
|
+
return nil if body.nil? || body.empty?
|
|
40
|
+
JSON.parse(body)
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/buzz/version.rb
ADDED
data/lib/buzz.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "buzz/version"
|
|
4
|
+
require_relative "buzz/configuration"
|
|
5
|
+
require_relative "buzz/error"
|
|
6
|
+
require_relative "buzz/cookie_jar"
|
|
7
|
+
require_relative "buzz/response"
|
|
8
|
+
require_relative "buzz/paginator"
|
|
9
|
+
require_relative "buzz/resource"
|
|
10
|
+
require_relative "buzz/resources/advertiser"
|
|
11
|
+
require_relative "buzz/resources/campaign"
|
|
12
|
+
require_relative "buzz/resources/line_item"
|
|
13
|
+
require_relative "buzz/resources/creative"
|
|
14
|
+
require_relative "buzz/resources/segment"
|
|
15
|
+
require_relative "buzz/resources/targeting"
|
|
16
|
+
require_relative "buzz/resources/creative_asset"
|
|
17
|
+
require_relative "buzz/resources/creative_line_item"
|
|
18
|
+
require_relative "buzz/resources/search"
|
|
19
|
+
require_relative "buzz/resources/reporting"
|
|
20
|
+
require_relative "buzz/client"
|
|
21
|
+
|
|
22
|
+
module Buzz
|
|
23
|
+
class << self
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
yield(configuration)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_configuration!
|
|
33
|
+
@configuration = Configuration.new
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: buzzrb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Letterpress
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webrick
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.8'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.8'
|
|
54
|
+
executables: []
|
|
55
|
+
extensions: []
|
|
56
|
+
extra_rdoc_files: []
|
|
57
|
+
files:
|
|
58
|
+
- LICENSE.md
|
|
59
|
+
- buzzrb.gemspec
|
|
60
|
+
- lib/buzz.rb
|
|
61
|
+
- lib/buzz/client.rb
|
|
62
|
+
- lib/buzz/configuration.rb
|
|
63
|
+
- lib/buzz/cookie_jar.rb
|
|
64
|
+
- lib/buzz/error.rb
|
|
65
|
+
- lib/buzz/paginator.rb
|
|
66
|
+
- lib/buzz/resource.rb
|
|
67
|
+
- lib/buzz/resources/advertiser.rb
|
|
68
|
+
- lib/buzz/resources/campaign.rb
|
|
69
|
+
- lib/buzz/resources/creative.rb
|
|
70
|
+
- lib/buzz/resources/creative_asset.rb
|
|
71
|
+
- lib/buzz/resources/creative_line_item.rb
|
|
72
|
+
- lib/buzz/resources/line_item.rb
|
|
73
|
+
- lib/buzz/resources/reporting.rb
|
|
74
|
+
- lib/buzz/resources/search.rb
|
|
75
|
+
- lib/buzz/resources/segment.rb
|
|
76
|
+
- lib/buzz/resources/targeting.rb
|
|
77
|
+
- lib/buzz/response.rb
|
|
78
|
+
- lib/buzz/version.rb
|
|
79
|
+
homepage: https://github.com/getletterpress/buzzrb
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata: {}
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
require_paths:
|
|
85
|
+
- lib
|
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3.0'
|
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
requirements: []
|
|
97
|
+
rubygems_version: 3.7.2
|
|
98
|
+
specification_version: 4
|
|
99
|
+
summary: Ruby client for the Buzz/Freewheel Advertiser API v2.0
|
|
100
|
+
test_files: []
|