Skip to content

Commit 0748020

Browse files
authored
upgrade Pinterest-Generated-Client to 0.1.11; fix: support single-exception API responses and update integration tests (#176)
1 parent ba1f5e1 commit 0748020

9 files changed

Lines changed: 117 additions & 39 deletions

File tree

.env.example

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,21 @@ PINTEREST_STATE=dev
99
PINTEREST_ACCESS_TOKEN_JSON_PATH=./
1010
PINTEREST_ACCESS_TOKEN='<access token>'
1111
PINTEREST_REFRESH_ACCESS_TOKEN='<refresh token>'
12-
PINTEREST_API_URI=https://api.pinterest.com/v5
12+
PINTEREST_API_URI=https://api.pinterest.com/v5
13+
14+
# Required to run integration_tests/ads/test_conversion_events.py. This is a Conversions API
15+
# access token, generated manually in Pinterest Ads Manager (Ad Account > Conversions >
16+
# Conversion Access Token). It cannot be fetched via any Pinterest API.
17+
CONVERSION_ACCESS_TOKEN='<conversion access token>'
18+
19+
# Test fixtures used throughout integration_tests/. Provision these by running:
20+
# python -m integration_tests.bin.setup_test_account <existing ad account id>
21+
# which creates a persistent board/section/pin and looks up your user account id, then writes
22+
# all six values below into this file. DEFAULT_AD_ACCOUNT_ID must be an ad account id that the
23+
# token's user_account already has access to; ad accounts are never created or deleted by the script.
24+
OWNER_USER_ID=<user account id>
25+
DEFAULT_AD_ACCOUNT_ID=<ad account id>
26+
DEFAULT_BOARD_ID=<board id>
27+
DEFAULT_BOARD_NAME='<board name>'
28+
DEFAULT_BOARD_SECTION_ID=<board section id>
29+
DEFAULT_PIN_ID=<pin id>

integration_tests/ads/test_ad_groups.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from integration_tests.base_test import BaseTestCase
66
from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
77

8+
from openapi_generated.pinterest_client.exceptions import ApiException
9+
810
from pinterest.ads.ad_groups import AdGroup
911

1012

@@ -84,6 +86,11 @@ def test_update_success(self):
8486
def test_update_fail_with_invalid_tracking_urls(self):
8587
"""
8688
Test update with invalid tracking url
89+
90+
Note: the API now validates the tracking URL format up front and
91+
raises a raw ApiException (HTTP 400) rather than accepting the
92+
malformed URLs and failing the SDK's own post-update value check
93+
(AssertionError).
8794
"""
8895
ad_group = AdGroup(
8996
ad_account_id=DEFAULT_AD_ACCOUNT_ID,
@@ -103,7 +110,7 @@ def test_update_fail_with_invalid_tracking_urls(self):
103110
tracking_urls=new_tracking_url
104111
)
105112

106-
with self.assertRaises(AssertionError):
113+
with self.assertRaises((AssertionError, ApiException)):
107114
ad_group.update_fields(**update_argument)
108115

109116

integration_tests/ads/test_campaigns.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def test_create_campaign_success(self):
3131
name="SDK Test Campaign",
3232
objective_type="AWARENESS",
3333
daily_spend_cap=10000000,
34+
is_campaign_budget_optimization=True,
3435
)
3536

3637
assert campaign
@@ -48,6 +49,7 @@ def test_create_campaign_failure_without_budget(self):
4849
ad_account_id=DEFAULT_AD_ACCOUNT_ID,
4950
name="SDK Test Campaign",
5051
objective_type="AWARENESS",
52+
is_campaign_budget_optimization=True,
5153
)
5254

5355
self.assertRaisesRegex(

integration_tests/ads/test_keywords.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pinterest.ads.keywords import Keyword
1111
from pinterest.utils.sdk_exceptions import SdkException
1212

13+
from openapi_generated.pinterest_client.exceptions import ApiException
1314
from openapi_generated.pinterest_client.model.match_type_response import MatchTypeResponse
1415

1516

@@ -37,14 +38,18 @@ def test_create_keyword_success(self):
3738
def test_create_fail_without_matchtype(self):
3839
"""
3940
Test creating a new keyword
41+
42+
Note: the API validates the missing match_type before evaluating
43+
keyword-creation business logic, so it raises a raw ApiException
44+
(HTTP 400) rather than the wrapped SdkException.
4045
"""
4146
keyword_arguments = dict(
4247
ad_account_id=DEFAULT_AD_ACCOUNT_ID,
4348
parent_id=self.ad_group_utils.get_ad_group_id(),
4449
value="string",
4550
)
4651

47-
with self.assertRaises(SdkException):
52+
with self.assertRaises((SdkException, ApiException)):
4853
Keyword.create(**keyword_arguments)
4954

5055

integration_tests/clean_organic_data.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@
99
def test_delete_organic_data():
1010
"""
1111
Delete organic boards from default client
12+
13+
Note: This test was updated to skip protected boards,
14+
as they are not deletable. This will return 401 and cause the test to fail.
1215
"""
1316
all_boards, _ = Board.get_all()
1417
for board in all_boards:
15-
if board.id == DEFAULT_BOARD_ID:
18+
if board.id == DEFAULT_BOARD_ID or board.privacy == "PROTECTED":
1619
continue
1720
Board.delete(board_id=board.id)
18-
assert len(Board.get_all()[0]) == 1
21+
remaining, _ = Board.get_all()
22+
assert all(
23+
board.id == DEFAULT_BOARD_ID or board.privacy == "PROTECTED"
24+
for board in remaining
25+
)
1926

2027
all_pins, _ = Pin.get_all()
2128
for pin in all_pins:

integration_tests/utils/ads_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def __init__(self, client=None):
109109
name="SDK Test Campaign",
110110
objective_type="AWARENESS",
111111
daily_spend_cap=10000000,
112+
is_campaign_budget_optimization=True,
112113
)
113114
self.campaign_id = self.campaign._id
114115

@@ -125,6 +126,7 @@ def get_default_params(self):
125126
name="SDK Test Campaign",
126127
objective_type="AWARENESS",
127128
daily_spend_cap=10000000,
129+
is_campaign_budget_optimization=True,
128130
)
129131

130132
def create_new_campaign(self, **kwargs):

pinterest/utils/error_handling.py

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,38 @@
33
"""
44
from pinterest.utils.sdk_exceptions import SdkException
55

6+
7+
def _get_field(obj, field):
8+
"""
9+
Read `field` off either a raw dict response or a generated model object
10+
"""
11+
if isinstance(obj, dict):
12+
return obj.get(field)
13+
return getattr(obj, field, None)
14+
15+
16+
def _get_first_exception(response):
17+
"""
18+
Return the first exception reported in `response`, or None if there is none.
19+
20+
Bulk endpoints return one element per requested entity. Depending on the endpoint that
21+
element's `exceptions` field is either a list of exceptions (campaigns, ad groups) or a
22+
single exception object (ads).
23+
"""
24+
items = _get_field(response, 'items')
25+
if not items:
26+
return None
27+
28+
exceptions = _get_field(items[0], 'exceptions')
29+
if not exceptions:
30+
return None
31+
32+
if isinstance(exceptions, list):
33+
return exceptions[0]
34+
return exceptions
35+
36+
637
def verify_api_response(response) -> bool:
7-
# pylint: disable=too-many-boolean-expressions
838
"""
939
Verify that there are no errors in `response` received from api
1040
@@ -14,36 +44,16 @@ def verify_api_response(response) -> bool:
1444
Returns:
1545
bool: If the `response` is without any exceptions
1646
"""
17-
if isinstance(response, dict):
18-
if (
19-
response.get('items')
20-
and len(response.get('items')) > 0
21-
and response.get('items')[0].get('exceptions')
22-
and isinstance(response.get('items')[0].get('exceptions'), list)
23-
and len(response.get('items')[0].get('exceptions')) > 0
24-
and response.get('items')[0].get('exceptions')[0].get('code')
25-
and response.get('items')[0].get('exceptions')[0].get('message')
26-
): # pylint: disable-msg=too-many-boolean-expressions
27-
raise SdkException(
28-
status=f"Failed with code {response.get('items')[0].get('exceptions')[0].get('code')}",
29-
reason=response.get('items')[0].get('exceptions')[0].get('message')
30-
)
31-
else:
32-
if (
33-
hasattr(response, "items")
34-
and response.items
35-
and len(response.items) > 0
36-
and hasattr(response.items[0], "exceptions")
37-
and response.items[0].exceptions
38-
and isinstance(response.items[0].exceptions, list)
39-
and len(response.items[0].exceptions) > 0
40-
and hasattr(response.items[0].exceptions[0], "code")
41-
and response.items[0].exceptions[0].code
42-
and hasattr(response.items[0].exceptions[0], "message")
43-
and response.items[0].exceptions[0].message
44-
):
45-
raise SdkException(
46-
status=f"Failed with code {response.items[0].exceptions[0].code}",
47-
reason=response.items[0].exceptions[0].message
48-
)
47+
exception = _get_first_exception(response)
48+
if exception is None:
49+
return True
50+
51+
code = _get_field(exception, 'code')
52+
message = _get_field(exception, 'message')
53+
if code and message:
54+
raise SdkException(
55+
status=f"Failed with code {code}",
56+
reason=message,
57+
)
58+
4959
return True

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Pinterest-Generated-Client==0.1.10
1+
Pinterest-Generated-Client==0.1.11
22
python-dateutil==2.8.2
33
six==1.16.0
44
urllib3>=1.26.12

tests/src/pinterest/utils/test_error_handling.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from unittest import TestCase
66

7+
from openapi_generated.pinterest_client.model.ad_array_response import AdArrayResponse
8+
from openapi_generated.pinterest_client.model.ad_array_response_element import AdArrayResponseElement
79
from openapi_generated.pinterest_client.model.campaign_create_response import CampaignCreateResponse
810
from openapi_generated.pinterest_client.model.campaign_create_response_item import CampaignCreateResponseItem
911
from openapi_generated.pinterest_client.model.campaign_create_response_data import CampaignCreateResponseData
@@ -53,3 +55,29 @@ def test_verify_api_response_with_exceptions(self):
5355
],
5456
)
5557
self.assertRaises(SdkException, verify_api_response, response=test_api_response)
58+
59+
def test_verify_api_response_with_single_exception_object(self):
60+
"""
61+
Verify if the function throws `SdkException` when the api reports a single exception
62+
object instead of a list of exceptions, as the ads endpoints do
63+
"""
64+
test_api_response = AdArrayResponse(
65+
items=[
66+
AdArrayResponseElement(
67+
exceptions=GeneratedException(
68+
code=1025,
69+
message="Ad has invalid creative type.",
70+
)
71+
)
72+
],
73+
)
74+
self.assertRaises(SdkException, verify_api_response, response=test_api_response)
75+
76+
def test_verify_api_response_with_dict_response(self):
77+
"""
78+
Verify if the function throws `SdkException` for exceptions in a raw dict response
79+
"""
80+
test_api_response = {
81+
"items": [{"exceptions": {"code": 1025, "message": "Ad has invalid creative type."}}]
82+
}
83+
self.assertRaises(SdkException, verify_api_response, response=test_api_response)

0 commit comments

Comments
 (0)