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 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,15 @@
1
+ module Pinterest
2
+ module Resources
3
+ class Base
4
+ include Connection
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :config
13
+ end
14
+ end
15
+ 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,3 @@
1
+ module Pinterest
2
+ VERSION = "0.1.0"
3
+ 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: []