pinterest-ads 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/README.md +230 -0
- data/lib/pinterest/client.rb +54 -0
- data/lib/pinterest/configuration.rb +16 -0
- data/lib/pinterest/connection.rb +100 -0
- data/lib/pinterest/error.rb +33 -0
- data/lib/pinterest/resources/audiences.rb +41 -0
- data/lib/pinterest/resources/base.rb +15 -0
- data/lib/pinterest/resources/customer_list_uploads.rb +40 -0
- data/lib/pinterest/resources/customer_lists.rb +40 -0
- data/lib/pinterest/resources/oauth.rb +93 -0
- data/lib/pinterest/response.rb +15 -0
- data/lib/pinterest/version.rb +3 -0
- data/lib/pinterest-ads.rb +1 -0
- data/lib/pinterest.rb +28 -0
- data/pinterest-ads.gemspec +21 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 739ce285547b3fa9389e1c0d249bd02bc7d7451cda3481227e8232e217ecc58f
|
|
4
|
+
data.tar.gz: f3cdfcf37725494e02ef16193a114420bb4827141dbd480f9b97b8ebe0584c64
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '09f64ca58d1dc980b50fe01b11655f30bc34e307ab02a0e9f7afc6dd084abcd6c77ade4920e9f65a4ac97732bddd45b1f4d3fa9b8bdd2eaccb60f98ef6b8b7f9'
|
|
7
|
+
data.tar.gz: 7678b6122e14f6e2e34b4c887583410b2a585ba5bf08087a63ecd5870b6fdaeeae5122d19dc455ab984b5ed4c08d43f570114cd7f401d176741d4d997458847e
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Pinterest API
|
|
2
|
+
|
|
3
|
+
Ruby wrapper for the [Pinterest REST API v5](https://developers.pinterest.com/docs/api/v5/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "pinterest-ads", git: "https://github.com/stitchfix/pinterest-api"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
### Global configuration
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Pinterest.configure do |c|
|
|
25
|
+
c.client_id = ENV["PINTEREST_CLIENT_ID"]
|
|
26
|
+
c.client_secret = ENV["PINTEREST_CLIENT_SECRET"]
|
|
27
|
+
c.access_token = ENV["PINTEREST_ACCESS_TOKEN"]
|
|
28
|
+
c.redirect_uri = ENV["PINTEREST_REDIRECT_URI"]
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Per-client configuration
|
|
33
|
+
|
|
34
|
+
You can also pass credentials directly when instantiating a client. Per-client values take precedence over the global configuration.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
client = Pinterest::Client.new(
|
|
38
|
+
client_id: "your_app_id",
|
|
39
|
+
client_secret: "your_app_secret",
|
|
40
|
+
access_token: "your_bearer_token",
|
|
41
|
+
redirect_uri: "https://example.com/callback"
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Optional settings
|
|
46
|
+
|
|
47
|
+
| Option | Default | Description |
|
|
48
|
+
|--------|---------|-------------|
|
|
49
|
+
| `base_url` | `https://api.pinterest.com/v5` | API base URL |
|
|
50
|
+
| `auth_url` | `https://www.pinterest.com/oauth/` | OAuth authorization URL |
|
|
51
|
+
| `timeout` | `30` | Request timeout in seconds |
|
|
52
|
+
| `open_timeout` | `10` | Connection open timeout in seconds |
|
|
53
|
+
|
|
54
|
+
These can be set via `Pinterest.configure` or passed as keyword arguments to `Pinterest::Client.new`.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
### OAuth
|
|
59
|
+
|
|
60
|
+
#### 1. Generate the authorization URL
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
url = client.oauth.authorization_url(
|
|
64
|
+
redirect_uri: "https://example.com/callback",
|
|
65
|
+
scope: %w[ads:read ads:write],
|
|
66
|
+
state: SecureRandom.hex(16)
|
|
67
|
+
)
|
|
68
|
+
# Redirect the user to this URL
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### 2. Exchange the authorization code for tokens
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
tokens = client.oauth.exchange_code(
|
|
75
|
+
code: params[:code],
|
|
76
|
+
redirect_uri: "https://example.com/callback"
|
|
77
|
+
)
|
|
78
|
+
# tokens => { "access_token" => "...", "refresh_token" => "...", "expires_in" => 86400, ... }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### 3. Refresh an access token
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
tokens = client.oauth.refresh(refresh_token: "your_refresh_token")
|
|
85
|
+
client.access_token = tokens["access_token"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### 4. Revoke a token
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
client.oauth.revoke(token: "token_to_revoke", token_type_hint: "access_token")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### 5. Generate a conversion API token
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
result = client.oauth.conversion_token
|
|
98
|
+
# result => { "access_token" => "...", "token_type" => "conversion" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Audiences
|
|
102
|
+
|
|
103
|
+
All audience methods require an `ad_account_id`.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# List audiences
|
|
107
|
+
audiences = client.audiences.list(ad_account_id: "123456")
|
|
108
|
+
# audiences => { "items" => [...], "bookmark" => "..." }
|
|
109
|
+
|
|
110
|
+
# Create an audience
|
|
111
|
+
audience = client.audiences.create(
|
|
112
|
+
ad_account_id: "123456",
|
|
113
|
+
name: "My Audience",
|
|
114
|
+
audience_type: "CUSTOMER_LIST",
|
|
115
|
+
rule: { ... }
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Get a specific audience
|
|
119
|
+
audience = client.audiences.find(ad_account_id: "123456", audience_id: "789")
|
|
120
|
+
|
|
121
|
+
# Update an audience
|
|
122
|
+
client.audiences.update(ad_account_id: "123456", audience_id: "789", name: "New Name")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Customer Lists
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# List customer lists
|
|
129
|
+
lists = client.customer_lists.list(ad_account_id: "123456")
|
|
130
|
+
|
|
131
|
+
# Create a customer list
|
|
132
|
+
list = client.customer_lists.create(
|
|
133
|
+
ad_account_id: "123456",
|
|
134
|
+
name: "Email Subscribers",
|
|
135
|
+
list_type: "EMAIL",
|
|
136
|
+
records: "user1@example.com\nuser2@example.com"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Get a specific customer list
|
|
140
|
+
list = client.customer_lists.find(ad_account_id: "123456", customer_list_id: "789")
|
|
141
|
+
|
|
142
|
+
# Add or remove records
|
|
143
|
+
client.customer_lists.update(
|
|
144
|
+
ad_account_id: "123456",
|
|
145
|
+
customer_list_id: "789",
|
|
146
|
+
operation_type: "ADD",
|
|
147
|
+
records: "user3@example.com"
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Customer List Uploads (Multipart S3)
|
|
152
|
+
|
|
153
|
+
For large customer lists, use the multipart upload workflow:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
# 1. Create the upload (returns presigned S3 URLs)
|
|
157
|
+
upload = client.customer_list_uploads.create(
|
|
158
|
+
ad_account_id: "123456",
|
|
159
|
+
customer_list_id: "789",
|
|
160
|
+
operation: "ADD",
|
|
161
|
+
total_parts: 3
|
|
162
|
+
)
|
|
163
|
+
# upload => { "customer_list_upload" => {...}, "s3_multipart_upload_data" => {...} }
|
|
164
|
+
|
|
165
|
+
# 2. Upload parts to S3 using the presigned URLs (outside this gem)
|
|
166
|
+
|
|
167
|
+
# 3. Trigger processing
|
|
168
|
+
client.customer_list_uploads.run(
|
|
169
|
+
ad_account_id: "123456",
|
|
170
|
+
customer_list_id: "789",
|
|
171
|
+
customer_list_upload_id: upload.dig("customer_list_upload", "id")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Check upload status
|
|
175
|
+
status = client.customer_list_uploads.find(
|
|
176
|
+
ad_account_id: "123456",
|
|
177
|
+
customer_list_id: "789",
|
|
178
|
+
customer_list_upload_id: "upload_id"
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Pagination
|
|
183
|
+
|
|
184
|
+
List endpoints return a `bookmark` value for cursor-based pagination:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
all_audiences = []
|
|
188
|
+
bookmark = nil
|
|
189
|
+
|
|
190
|
+
loop do
|
|
191
|
+
result = client.audiences.list(ad_account_id: "123456", bookmark: bookmark, page_size: 25)
|
|
192
|
+
all_audiences.concat(result["items"])
|
|
193
|
+
bookmark = result["bookmark"]
|
|
194
|
+
break if bookmark.nil?
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Error Handling
|
|
199
|
+
|
|
200
|
+
The gem raises typed exceptions for HTTP errors:
|
|
201
|
+
|
|
202
|
+
| Status | Exception |
|
|
203
|
+
|--------|-----------|
|
|
204
|
+
| 400 | `Pinterest::BadRequestError` |
|
|
205
|
+
| 401 | `Pinterest::AuthenticationError` |
|
|
206
|
+
| 403 | `Pinterest::ForbiddenError` |
|
|
207
|
+
| 404 | `Pinterest::NotFoundError` |
|
|
208
|
+
| 429 | `Pinterest::RateLimitError` |
|
|
209
|
+
| 5xx | `Pinterest::ServerError` |
|
|
210
|
+
|
|
211
|
+
All exceptions inherit from `Pinterest::Error` and expose `status`, `code`, and `response` attributes.
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
begin
|
|
215
|
+
client.audiences.find(ad_account_id: "123", audience_id: "bad_id")
|
|
216
|
+
rescue Pinterest::NotFoundError => e
|
|
217
|
+
puts e.message # API error message
|
|
218
|
+
puts e.status # 404
|
|
219
|
+
rescue Pinterest::Error => e
|
|
220
|
+
puts "Unexpected error: #{e.message} (HTTP #{e.status})"
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Requirements
|
|
225
|
+
|
|
226
|
+
- Ruby >= 3.0.0
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require_relative "resources/base"
|
|
2
|
+
require_relative "resources/oauth"
|
|
3
|
+
require_relative "resources/audiences"
|
|
4
|
+
require_relative "resources/customer_lists"
|
|
5
|
+
require_relative "resources/customer_list_uploads"
|
|
6
|
+
|
|
7
|
+
module Pinterest
|
|
8
|
+
class Client
|
|
9
|
+
# @param client_id [String] Pinterest app ID (required for OAuth token calls)
|
|
10
|
+
# @param client_secret [String] Pinterest app secret (required for OAuth token calls)
|
|
11
|
+
# @param access_token [String] Bearer token (required for authenticated API calls)
|
|
12
|
+
# @param redirect_uri [String] default redirect URI for authorization_url
|
|
13
|
+
# @param options [Hash] overrides for timeout, open_timeout, base_url, auth_url
|
|
14
|
+
def initialize(client_id: nil, client_secret: nil, access_token: nil,
|
|
15
|
+
redirect_uri: nil, default_scope: nil, **options)
|
|
16
|
+
@config = Configuration.new
|
|
17
|
+
@config.client_id = client_id || Pinterest.configuration.client_id
|
|
18
|
+
@config.client_secret = client_secret || Pinterest.configuration.client_secret
|
|
19
|
+
@config.access_token = access_token || Pinterest.configuration.access_token
|
|
20
|
+
@config.redirect_uri = redirect_uri || Pinterest.configuration.redirect_uri
|
|
21
|
+
@config.default_scope = default_scope || Pinterest.configuration.default_scope
|
|
22
|
+
|
|
23
|
+
options.each do |key, value|
|
|
24
|
+
@config.public_send(:"#{key}=", value) if @config.respond_to?(:"#{key}=")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Resources::OAuth]
|
|
29
|
+
def oauth
|
|
30
|
+
@oauth ||= Resources::OAuth.new(@config)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Resources::Audiences]
|
|
34
|
+
def audiences
|
|
35
|
+
@audiences ||= Resources::Audiences.new(@config)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Resources::CustomerLists]
|
|
39
|
+
def customer_lists
|
|
40
|
+
@customer_lists ||= Resources::CustomerLists.new(@config)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Resources::CustomerListUploads]
|
|
44
|
+
def customer_list_uploads
|
|
45
|
+
@customer_list_uploads ||= Resources::CustomerListUploads.new(@config)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Update the stored access token (e.g. after a refresh).
|
|
49
|
+
def access_token=(token)
|
|
50
|
+
@config.access_token = token
|
|
51
|
+
@oauth = nil # reset memoized resource so it picks up the new token
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
class Configuration
|
|
3
|
+
PINTEREST_BASE_URL = "https://api.pinterest.com/v5"
|
|
4
|
+
PINTEREST_AUTH_URL = "https://www.pinterest.com/oauth/"
|
|
5
|
+
|
|
6
|
+
attr_accessor :client_id, :client_secret, :access_token, :redirect_uri,
|
|
7
|
+
:base_url, :auth_url, :timeout, :open_timeout, :default_scope
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@base_url = PINTEREST_BASE_URL
|
|
11
|
+
@auth_url = PINTEREST_AUTH_URL
|
|
12
|
+
@timeout = 30
|
|
13
|
+
@open_timeout = 10
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/net_http"
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Pinterest
|
|
6
|
+
module Connection
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Bearer-authed connection for standard API calls.
|
|
10
|
+
def api_connection
|
|
11
|
+
@api_connection ||= build_connection(config.base_url) do |conn|
|
|
12
|
+
conn.request :authorization, "Bearer", -> { config.access_token }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Basic-authed connection used exclusively for token-endpoint calls.
|
|
17
|
+
def auth_connection
|
|
18
|
+
@auth_connection ||= build_connection(config.base_url) do |conn|
|
|
19
|
+
conn.request :authorization, :basic, config.client_id, config.client_secret
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_connection(url)
|
|
24
|
+
url = "#{url.chomp("/")}/"
|
|
25
|
+
Faraday.new(url: url) do |conn|
|
|
26
|
+
yield conn if block_given?
|
|
27
|
+
conn.options.timeout = config.timeout
|
|
28
|
+
conn.options.open_timeout = config.open_timeout
|
|
29
|
+
conn.response :raise_error
|
|
30
|
+
conn.adapter :net_http
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Faraday treats paths beginning with "/" as root-relative, stripping the
|
|
35
|
+
# /v5 prefix from the base URL. Strip leading slashes so Faraday appends
|
|
36
|
+
# them to the full base URL instead.
|
|
37
|
+
def normalize_path(path)
|
|
38
|
+
path.delete_prefix("/")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get(path, params = {})
|
|
42
|
+
handle { api_connection.get(normalize_path(path), params) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def post(path, body = {}, json: true, basic_auth: false)
|
|
46
|
+
conn = basic_auth ? auth_connection : api_connection
|
|
47
|
+
path = normalize_path(path)
|
|
48
|
+
handle do
|
|
49
|
+
if json
|
|
50
|
+
conn.post(path) do |req|
|
|
51
|
+
req.headers["Content-Type"] = "application/json"
|
|
52
|
+
req.headers["Accept"] = "application/json"
|
|
53
|
+
req.body = JSON.generate(body)
|
|
54
|
+
end
|
|
55
|
+
else
|
|
56
|
+
conn.post(path) do |req|
|
|
57
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
58
|
+
req.headers["Accept"] = "application/json"
|
|
59
|
+
req.body = URI.encode_www_form(body.compact)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def patch(path, body = {})
|
|
66
|
+
handle do
|
|
67
|
+
api_connection.patch(normalize_path(path)) do |req|
|
|
68
|
+
req.headers["Content-Type"] = "application/json"
|
|
69
|
+
req.headers["Accept"] = "application/json"
|
|
70
|
+
req.body = JSON.generate(body)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def delete(path, params = {})
|
|
76
|
+
handle { api_connection.delete(normalize_path(path), params) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle
|
|
80
|
+
raw = yield
|
|
81
|
+
response = Response.new(raw)
|
|
82
|
+
body = parse_body(raw.body)
|
|
83
|
+
raise Pinterest.error_for(raw.status, body, response) unless response.success?
|
|
84
|
+
body
|
|
85
|
+
rescue Faraday::ClientError, Faraday::ServerError => e
|
|
86
|
+
status = e.response&.dig(:status)
|
|
87
|
+
body = safe_parse(e.response&.dig(:body))
|
|
88
|
+
raise Pinterest.error_for(status || 0, body, nil)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_body(raw)
|
|
92
|
+
return nil if raw.nil? || raw.empty?
|
|
93
|
+
JSON.parse(raw)
|
|
94
|
+
rescue JSON::ParserError
|
|
95
|
+
raw
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias safe_parse parse_body
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
attr_reader :status, :code, :response
|
|
4
|
+
|
|
5
|
+
def initialize(message = nil, status: nil, code: nil, response: nil)
|
|
6
|
+
@status = status
|
|
7
|
+
@code = code
|
|
8
|
+
@response = response
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class BadRequestError < Error; end # 400
|
|
14
|
+
class AuthenticationError < Error; end # 401
|
|
15
|
+
class ForbiddenError < Error; end # 403
|
|
16
|
+
class NotFoundError < Error; end # 404
|
|
17
|
+
class RateLimitError < Error; end # 429
|
|
18
|
+
class ServerError < Error; end # 5xx
|
|
19
|
+
|
|
20
|
+
HTTP_ERROR_MAP = {
|
|
21
|
+
400 => BadRequestError,
|
|
22
|
+
401 => AuthenticationError,
|
|
23
|
+
403 => ForbiddenError,
|
|
24
|
+
404 => NotFoundError,
|
|
25
|
+
429 => RateLimitError,
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def self.error_for(status, body, response)
|
|
29
|
+
klass = HTTP_ERROR_MAP.fetch(status) { status >= 500 ? ServerError : Error }
|
|
30
|
+
message = body.is_a?(Hash) ? body["message"] : body.to_s
|
|
31
|
+
klass.new(message, status: status, code: body.is_a?(Hash) ? body["code"] : nil, response: response)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
module Resources
|
|
3
|
+
class Audiences < Base
|
|
4
|
+
# @param ad_account_id [String]
|
|
5
|
+
# @param params [Hash] audience attributes (ad_account_id, audience_type, name, rule, …)
|
|
6
|
+
# @return [Hash]
|
|
7
|
+
def create(ad_account_id:, **params)
|
|
8
|
+
post("/ad_accounts/#{ad_account_id}/audiences", params)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param ad_account_id [String]
|
|
12
|
+
# @param bookmark [String, nil]
|
|
13
|
+
# @param page_size [Integer, nil]
|
|
14
|
+
# @param order [String, nil] "ASCENDING" or "DESCENDING"
|
|
15
|
+
# @param ownership_type [String, nil] "OWNED" or "RECEIVED"
|
|
16
|
+
# @param exclude_nca [Boolean, nil]
|
|
17
|
+
# @return [Hash] { "items" => [...], "bookmark" => "..." }
|
|
18
|
+
def list(ad_account_id:, bookmark: nil, page_size: nil, order: nil,
|
|
19
|
+
ownership_type: nil, exclude_nca: nil)
|
|
20
|
+
get("/ad_accounts/#{ad_account_id}/audiences",
|
|
21
|
+
{ bookmark: bookmark, page_size: page_size, order: order,
|
|
22
|
+
ownership_type: ownership_type, exclude_nca: exclude_nca }.compact)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param ad_account_id [String]
|
|
26
|
+
# @param audience_id [String]
|
|
27
|
+
# @return [Hash]
|
|
28
|
+
def find(ad_account_id:, audience_id:)
|
|
29
|
+
get("/ad_accounts/#{ad_account_id}/audiences/#{audience_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param ad_account_id [String]
|
|
33
|
+
# @param audience_id [String]
|
|
34
|
+
# @param params [Hash] fields to update
|
|
35
|
+
# @return [Hash]
|
|
36
|
+
def update(ad_account_id:, audience_id:, **params)
|
|
37
|
+
patch("/ad_accounts/#{ad_account_id}/audiences/#{audience_id}", params)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
module Resources
|
|
3
|
+
# Handles multipart S3 upload lifecycle for customer list records.
|
|
4
|
+
# Workflow: create → (upload parts to S3 presigned URLs) → run
|
|
5
|
+
class CustomerListUploads < Base
|
|
6
|
+
# Request a multipart S3 upload for a customer list.
|
|
7
|
+
# Each part must be ≥ 5 MB except the final part.
|
|
8
|
+
#
|
|
9
|
+
# @param ad_account_id [String]
|
|
10
|
+
# @param customer_list_id [String]
|
|
11
|
+
# @param operation [String] "ADD" or "REMOVE"
|
|
12
|
+
# @param total_parts [Integer] number of S3 parts
|
|
13
|
+
# @return [Hash] { "customer_list_upload" => {...}, "s3_multipart_upload_data" => {...} }
|
|
14
|
+
def create(ad_account_id:, customer_list_id:, operation:, total_parts:)
|
|
15
|
+
post("/ad_accounts/#{ad_account_id}/customer_lists/#{customer_list_id}/uploads",
|
|
16
|
+
{ operation: operation, total_parts: total_parts })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param ad_account_id [String]
|
|
20
|
+
# @param customer_list_id [String]
|
|
21
|
+
# @param customer_list_upload_id [String]
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
def find(ad_account_id:, customer_list_id:, customer_list_upload_id:)
|
|
24
|
+
get("/ad_accounts/#{ad_account_id}/customer_lists/#{customer_list_id}" \
|
|
25
|
+
"/uploads/#{customer_list_upload_id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Begin processing a customer list upload after all S3 parts are uploaded.
|
|
29
|
+
#
|
|
30
|
+
# @param ad_account_id [String]
|
|
31
|
+
# @param customer_list_id [String]
|
|
32
|
+
# @param customer_list_upload_id [String]
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def run(ad_account_id:, customer_list_id:, customer_list_upload_id:)
|
|
35
|
+
post("/ad_accounts/#{ad_account_id}/customer_lists/#{customer_list_id}" \
|
|
36
|
+
"/uploads/#{customer_list_upload_id}/run")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
module Resources
|
|
3
|
+
class CustomerLists < Base
|
|
4
|
+
# @param ad_account_id [String]
|
|
5
|
+
# @param bookmark [String, nil]
|
|
6
|
+
# @param page_size [Integer, nil]
|
|
7
|
+
# @param order [String, nil] "ASCENDING" or "DESCENDING"
|
|
8
|
+
# @param exclude_nca [Boolean, nil]
|
|
9
|
+
# @return [Hash] { "items" => [...], "bookmark" => "..." }
|
|
10
|
+
def list(ad_account_id:, bookmark: nil, page_size: nil, order: nil, exclude_nca: nil)
|
|
11
|
+
get("/ad_accounts/#{ad_account_id}/customer_lists",
|
|
12
|
+
{ bookmark: bookmark, page_size: page_size,
|
|
13
|
+
order: order, exclude_nca: exclude_nca }.compact)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param ad_account_id [String]
|
|
17
|
+
# @param params [Hash] name, list_type, records, records_v2, is_nca
|
|
18
|
+
# @return [Hash]
|
|
19
|
+
def create(ad_account_id:, **params)
|
|
20
|
+
post("/ad_accounts/#{ad_account_id}/customer_lists", params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param ad_account_id [String]
|
|
24
|
+
# @param customer_list_id [String]
|
|
25
|
+
# @return [Hash]
|
|
26
|
+
def find(ad_account_id:, customer_list_id:)
|
|
27
|
+
get("/ad_accounts/#{ad_account_id}/customer_lists/#{customer_list_id}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Append or remove records from an existing customer list.
|
|
31
|
+
# @param ad_account_id [String]
|
|
32
|
+
# @param customer_list_id [String]
|
|
33
|
+
# @param params [Hash] operation_type ("ADD"/"REMOVE"), records or records_v2
|
|
34
|
+
# @return [Hash]
|
|
35
|
+
def update(ad_account_id:, customer_list_id:, **params)
|
|
36
|
+
patch("/ad_accounts/#{ad_account_id}/customer_lists/#{customer_list_id}", params)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
module Resources
|
|
3
|
+
# Wraps POST /oauth/token, /oauth/token/revoke, and /oauth/conversion_token.
|
|
4
|
+
# Token-endpoint calls use HTTP Basic auth (client_id:client_secret).
|
|
5
|
+
# The conversion-token call requires a valid Bearer access_token.
|
|
6
|
+
class OAuth < Base
|
|
7
|
+
# Exchange an authorization code for access + refresh tokens.
|
|
8
|
+
#
|
|
9
|
+
# @param code [String] the code returned by the Pinterest OAuth redirect
|
|
10
|
+
# @param redirect_uri [String] must match the URI used in the auth request
|
|
11
|
+
# @param continuous_refresh [Boolean, nil] set true for apps created before
|
|
12
|
+
# 2025-09-25 to opt into the 60-day continuous refresh token
|
|
13
|
+
# @return [Hash] access_token, refresh_token, expires_in, scope, …
|
|
14
|
+
def exchange_code(code:, redirect_uri: config.redirect_uri, continuous_refresh: nil)
|
|
15
|
+
resp = post("/oauth/token",
|
|
16
|
+
{
|
|
17
|
+
grant_type: "authorization_code",
|
|
18
|
+
code: code,
|
|
19
|
+
redirect_uri: redirect_uri,
|
|
20
|
+
continuous_refresh: continuous_refresh
|
|
21
|
+
},
|
|
22
|
+
json: false,
|
|
23
|
+
basic_auth: true
|
|
24
|
+
)
|
|
25
|
+
config.access_token = resp["access_token"]
|
|
26
|
+
resp
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Refresh an existing access token using a continuous refresh token.
|
|
30
|
+
#
|
|
31
|
+
# @param refresh_token [String]
|
|
32
|
+
# @param scope [String, nil] space-separated scope string; omit to keep current scope
|
|
33
|
+
# @param continuous_refresh [Boolean, nil] same semantics as #exchange_code
|
|
34
|
+
# @return [Hash] new access_token, refresh_token, expires_in, …
|
|
35
|
+
def refresh(refresh_token:, scope: nil, continuous_refresh: nil)
|
|
36
|
+
resp = post("/oauth/token",
|
|
37
|
+
{ grant_type: "refresh_token",
|
|
38
|
+
refresh_token: refresh_token,
|
|
39
|
+
scope: scope,
|
|
40
|
+
continuous_refresh: continuous_refresh
|
|
41
|
+
},
|
|
42
|
+
json: false,
|
|
43
|
+
basic_auth: true
|
|
44
|
+
)
|
|
45
|
+
config.access_token = resp["access_token"]
|
|
46
|
+
resp
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Generate a long-lived conversion API token from the current access token.
|
|
50
|
+
# Requires config.access_token to be set (Bearer auth).
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash] access_token, token_type: "conversion"
|
|
53
|
+
def conversion_token
|
|
54
|
+
post("/oauth/conversion_token", {})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Revoke an access or refresh token.
|
|
58
|
+
# Only tokens issued for system users are supported.
|
|
59
|
+
#
|
|
60
|
+
# @param token [String] the token to revoke
|
|
61
|
+
# @param token_type_hint [String, nil] "access_token" or "refresh_token"
|
|
62
|
+
# @return [nil]
|
|
63
|
+
def revoke(token:, token_type_hint: nil)
|
|
64
|
+
post("/oauth/token/revoke",
|
|
65
|
+
{
|
|
66
|
+
token: token,
|
|
67
|
+
token_type_hint: token_type_hint
|
|
68
|
+
},
|
|
69
|
+
json: false, basic_auth: true
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build the authorization URL users visit to grant permissions.
|
|
74
|
+
#
|
|
75
|
+
# @param redirect_uri [String]
|
|
76
|
+
# @param scope [Array<String>, String] required scopes
|
|
77
|
+
# @param state [String] CSRF token you generate and later verify
|
|
78
|
+
# @param response_type [String] always "code" for the auth-code flow
|
|
79
|
+
# @return [String] full URL
|
|
80
|
+
def authorization_url(scope: config.default_scope, redirect_uri: config.redirect_uri, state: SecureRandom.hex(16), response_type: "code")
|
|
81
|
+
scope_str = Array(scope).join(" ")
|
|
82
|
+
params = URI.encode_www_form(
|
|
83
|
+
response_type: response_type,
|
|
84
|
+
client_id: config.client_id,
|
|
85
|
+
redirect_uri: redirect_uri,
|
|
86
|
+
scope: scope_str,
|
|
87
|
+
state: state
|
|
88
|
+
)
|
|
89
|
+
"#{config.auth_url}?#{params}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Pinterest
|
|
2
|
+
class Response
|
|
3
|
+
attr_reader :status, :headers, :body
|
|
4
|
+
|
|
5
|
+
def initialize(faraday_response)
|
|
6
|
+
@status = faraday_response.status
|
|
7
|
+
@headers = faraday_response.headers
|
|
8
|
+
@body = faraday_response.body
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def success?
|
|
12
|
+
status.between?(200, 299)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "pinterest"
|
data/lib/pinterest.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "pinterest/version"
|
|
2
|
+
require_relative "pinterest/configuration"
|
|
3
|
+
require_relative "pinterest/error"
|
|
4
|
+
require_relative "pinterest/response"
|
|
5
|
+
require_relative "pinterest/connection"
|
|
6
|
+
require_relative "pinterest/client"
|
|
7
|
+
|
|
8
|
+
module Pinterest
|
|
9
|
+
class << self
|
|
10
|
+
def configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Configure the gem globally.
|
|
15
|
+
#
|
|
16
|
+
# Pinterest.configure do |c|
|
|
17
|
+
# c.client_id = ENV["PINTEREST_CLIENT_ID"]
|
|
18
|
+
# c.client_secret = ENV["PINTEREST_CLIENT_SECRET"]
|
|
19
|
+
# end
|
|
20
|
+
def configure
|
|
21
|
+
yield configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset_configuration!
|
|
25
|
+
@configuration = Configuration.new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "lib/pinterest/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "pinterest-ads"
|
|
5
|
+
spec.version = Pinterest::VERSION
|
|
6
|
+
spec.authors = ["johndavid400"]
|
|
7
|
+
spec.summary = "Ruby wrapper for the Pinterest REST API v5"
|
|
8
|
+
spec.homepage = "https://github.com/johndavid400/pinterest"
|
|
9
|
+
spec.license = "MIT"
|
|
10
|
+
|
|
11
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
12
|
+
|
|
13
|
+
spec.files = Dir["lib/**/*.rb", "pinterest-ads.gemspec", "LICENSE", "README.md"]
|
|
14
|
+
|
|
15
|
+
spec.add_dependency "faraday", "~> 2.0"
|
|
16
|
+
spec.add_dependency "faraday-net_http", "~> 3.0"
|
|
17
|
+
|
|
18
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
19
|
+
spec.add_development_dependency "webmock", "~> 3.23"
|
|
20
|
+
spec.add_development_dependency "dotenv", "~> 3.0"
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pinterest-ads
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- johndavid400
|
|
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: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-net_http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webmock
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.23'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.23'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: dotenv
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
executables: []
|
|
83
|
+
extensions: []
|
|
84
|
+
extra_rdoc_files: []
|
|
85
|
+
files:
|
|
86
|
+
- README.md
|
|
87
|
+
- lib/pinterest-ads.rb
|
|
88
|
+
- lib/pinterest.rb
|
|
89
|
+
- lib/pinterest/client.rb
|
|
90
|
+
- lib/pinterest/configuration.rb
|
|
91
|
+
- lib/pinterest/connection.rb
|
|
92
|
+
- lib/pinterest/error.rb
|
|
93
|
+
- lib/pinterest/resources/audiences.rb
|
|
94
|
+
- lib/pinterest/resources/base.rb
|
|
95
|
+
- lib/pinterest/resources/customer_list_uploads.rb
|
|
96
|
+
- lib/pinterest/resources/customer_lists.rb
|
|
97
|
+
- lib/pinterest/resources/oauth.rb
|
|
98
|
+
- lib/pinterest/response.rb
|
|
99
|
+
- lib/pinterest/version.rb
|
|
100
|
+
- pinterest-ads.gemspec
|
|
101
|
+
homepage: https://github.com/johndavid400/pinterest
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata: {}
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 3.0.0
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 3.6.9
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: Ruby wrapper for the Pinterest REST API v5
|
|
122
|
+
test_files: []
|