diff --git a/integration_tests/ads/test_ad_groups.py b/integration_tests/ads/test_ad_groups.py index d147e26..f40c5b8 100644 --- a/integration_tests/ads/test_ad_groups.py +++ b/integration_tests/ads/test_ad_groups.py @@ -63,9 +63,7 @@ def test_update_success(self): ) new_name = "SDK_AD_GROUP_NEW_NAME" - new_spec = { - "GENDER": ["male"] - } + new_spec = {"gender": ["male"]} ad_group.update_fields( name=new_name, @@ -74,7 +72,7 @@ def test_update_success(self): assert ad_group assert getattr(ad_group, "_name") == new_name - assert getattr(ad_group,"_targeting_spec") == new_spec + assert str(getattr(ad_group,"_targeting_spec")) == str(new_spec) def test_update_fail_with_invalid_tracking_urls(self): """ diff --git a/integration_tests/ads/test_conversion_events.py b/integration_tests/ads/test_conversion_events.py index b3b9fba..0806c8c 100644 --- a/integration_tests/ads/test_conversion_events.py +++ b/integration_tests/ads/test_conversion_events.py @@ -2,12 +2,17 @@ Test Conversion Model """ import os as _os +from datetime import datetime + from integration_tests.base_test import BaseTestCase from integration_tests.config import DEFAULT_AD_ACCOUNT_ID +from openapi_generated.pinterest_client import ApiException from pinterest.client import PinterestSDKClient from pinterest.ads.conversion_events import Conversion +_get_event_time = lambda: int(datetime.now().timestamp()) + class TestSendConversionEvent(BaseTestCase): """ Test send Conversion Event @@ -21,7 +26,7 @@ def test_send_conversion_success(self): NUMBER_OF_CONVERSION_EVENTS = 2 raw_user_data = dict( - em = ["964bbaf162703657e787eb4455197c8b35c18940c75980b0285619fe9b8acec8"] #random hash256 + em = ["f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a"] #random hash256 ) raw_custom_data = dict() @@ -29,7 +34,7 @@ def test_send_conversion_success(self): Conversion.create_conversion_event( event_name = "add_to_cart", action_source = "app_ios", - event_time = 1670026573, + event_time = _get_event_time(), event_id = "eventId0001", user_data = raw_user_data, custom_data= raw_custom_data, @@ -71,7 +76,7 @@ def test_send_conversion_fail(self): Conversion.create_conversion_event( event_name = "add_to_cart", action_source = "app_ios", - event_time = 1670026573, + event_time = _get_event_time(), event_id = "eventId0001", user_data = raw_user_data, custom_data = raw_custom_data, @@ -79,17 +84,12 @@ def test_send_conversion_fail(self): for _ in range(NUMBER_OF_CONVERSION_EVENTS) ] - response = Conversion.send_conversion_events( - client = client, - ad_account_id = DEFAULT_AD_ACCOUNT_ID, - conversion_events = conversion_events, - test = True, - ) - - assert response - assert response.num_events_received == 2 - assert response.num_events_processed == 0 - assert len(response.events) == 2 - - assert 'hashed format' in response.events[0].error_message - assert 'hashed format' in response.events[0].error_message + try: + Conversion.send_conversion_events( + client = client, + ad_account_id = DEFAULT_AD_ACCOUNT_ID, + conversion_events = conversion_events, + test = True, + ) + except ApiException as e: + assert e.status == 422 diff --git a/integration_tests/clean_organic_data.py b/integration_tests/clean_organic_data.py index 92339c0..80ed43e 100644 --- a/integration_tests/clean_organic_data.py +++ b/integration_tests/clean_organic_data.py @@ -3,7 +3,8 @@ """ from pinterest.organic.boards import Board -from integration_tests.config import DEFAULT_BOARD_ID +from pinterest.organic.pins import Pin +from integration_tests.config import DEFAULT_BOARD_ID, DEFAULT_PIN_ID def test_delete_organic_data(): """ @@ -14,5 +15,11 @@ def test_delete_organic_data(): if board.id == DEFAULT_BOARD_ID: continue Board.delete(board_id=board.id) - assert len(Board.get_all()[0]) == 1 + + all_pins, _ = Pin.get_all() + for pin in all_pins: + if pin.id == DEFAULT_PIN_ID: + continue + Pin.delete(pin_id=pin.id) + assert len(Pin.get_all()[0]) == 1 diff --git a/integration_tests/utils/organic_utils.py b/integration_tests/utils/organic_utils.py index b1ef35b..5eac342 100644 --- a/integration_tests/utils/organic_utils.py +++ b/integration_tests/utils/organic_utils.py @@ -47,6 +47,8 @@ def create_new_board(self, **kwargs): return Board.create(**_merge_default_params_with_params(self.get_default_params(), kwargs)) def delete_board(self, board_id): + if board_id == DEFAULT_BOARD_ID: + return return Board.delete(board_id=board_id, client=self.test_client) @@ -79,4 +81,6 @@ def create_new_pin(self, **kwargs): return Pin.create(**_merge_default_params_with_params(self.get_default_params(), kwargs)) def delete_pin(self, pin_id): + if pin_id == DEFAULT_PIN_ID: + return return Pin.delete(pin_id=pin_id, client=self.test_client) diff --git a/pinterest/ads/ad_groups.py b/pinterest/ads/ad_groups.py index 1df2599..fcfac7d 100644 --- a/pinterest/ads/ad_groups.py +++ b/pinterest/ads/ad_groups.py @@ -10,6 +10,7 @@ from openapi_generated.pinterest_client.model.ad_group_response import AdGroupResponse from openapi_generated.pinterest_client.model.ad_group_create_request import AdGroupCreateRequest from openapi_generated.pinterest_client.model.ad_group_update_request import AdGroupUpdateRequest +from openapi_generated.pinterest_client.model.targeting_spec import TargetingSpec from pinterest.client import PinterestSDKClient from pinterest.utils.base_model import PinterestBaseModel @@ -64,6 +65,7 @@ def __init__( self._feed_profile_id = None self._dca_assets = None self._optimization_goal_metadata = None + self._targeting_template_ids = None PinterestBaseModel.__init__( self, @@ -207,6 +209,11 @@ def optimization_goal_metadata(self): #pylint: disable=missing-function-docstring return self._optimization_goal_metadata + @property + def targeting_template_ids(self): + #pylint: disable=missing-function-docstring + return self._targeting_template_ids + @classmethod def create( @@ -351,6 +358,9 @@ def update_fields(self, **kwargs) -> bool: kwargs["billable_event"] = ActionType(kwargs["billable_event"]) if "budget_type" in kwargs: kwargs["budget_type"] = BudgetType(kwargs["budget_type"]) + if "targeting_spec" in kwargs: + kwargs["targeting_spec"] = TargetingSpec(**kwargs["targeting_spec"]) + return self._update( params={ "ad_account_id": self._ad_account_id, diff --git a/pinterest/ads/ads.py b/pinterest/ads/ads.py index 1d98774..be9da3a 100644 --- a/pinterest/ads/ads.py +++ b/pinterest/ads/ads.py @@ -17,7 +17,7 @@ class Ad(PinterestBaseModel): - # pylint: disable=R0903,duplicate-code + # pylint: disable=R0903,duplicate-code,R0902 """ Ad model used to view, create, update its attributes """ @@ -63,6 +63,11 @@ def __init__( self._type = None self._updated_time = None self._summary_status = None + self._grid_click_type = None + self._customizable_cta_type = None + self._lead_form_id = None + self._quiz_pin_data = None + PinterestBaseModel.__init__( self, _id=str(ad_id), @@ -210,6 +215,25 @@ def summary_status(self) -> str: # pylint: disable=missing-function-docstring return self._summary_status + @property + def grid_click_type(self) -> str: + # pylint: disable=missing-function-docstring + return self._grid_click_type + + @property + def customizable_cta_type(self) -> str: + # pylint: disable=missing-function-docstring + return self._customizable_cta_type + + @property + def lead_form_id(self) -> str: + # pylint: disable=missing-function-docstring + return self._lead_form_id + + @property + def quiz_pin_data(self) -> str: + # pylint: disable=missing-function-docstring + return self._quiz_pin_data @classmethod def create(cls, diff --git a/pinterest/client/__init__.py b/pinterest/client/__init__.py index 87dfba1..4a11555 100644 --- a/pinterest/client/__init__.py +++ b/pinterest/client/__init__.py @@ -141,7 +141,8 @@ def _get_config(cls, access_token: str, api_uri: str = config.PINTEREST_API_URI, debug: str = config.PINTEREST_DEBUG, log_file: str = config.PINTEREST_LOG_FILE, - logger_format: str = config.PINTEREST_LOGGER_FORMAT): + logger_format: str = config.PINTEREST_LOGGER_FORMAT, + disabled_client_side_validations: str = config.PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS): _config = Configuration( access_token=access_token, host=api_uri, @@ -150,6 +151,7 @@ def _get_config(cls, access_token: str, _config.debug = debug _config.logger_file = log_file _config.logger_format = logger_format + _config.disabled_client_side_validations = disabled_client_side_validations return _config @classmethod diff --git a/pinterest/config.py b/pinterest/config.py index 04f1aec..aff4337 100644 --- a/pinterest/config.py +++ b/pinterest/config.py @@ -25,7 +25,15 @@ PINTEREST_REFRESH_ACCESS_TOKEN = _os.environ.get('PINTEREST_REFRESH_ACCESS_TOKEN') PINTEREST_API_URI = _os.environ.get('PINTEREST_API_URI', 'https://api.pinterest.com/v5') PINTEREST_LOG_FILE = _os.environ.get('PINTEREST_LOG_FILE', None) -PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS = _os.environ.get('PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS', None) +DEFAULT_DISABLE_VALIDATIONS = ",".join([ + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems', +]) +PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS = _os.environ.get( + 'PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS', + DEFAULT_DISABLE_VALIDATIONS +) PINTEREST_LOGGER_FORMAT = _os.environ.get('PINTEREST_LOGGER_FORMAT', '%(asctime)s %(levelname)s %(message)s') PINTEREST_SDK_VERSION = __version__ PINTEREST_USER_AGENT = f'pins-sdk/python/v{PINTEREST_SDK_VERSION}' diff --git a/pinterest/organic/boards.py b/pinterest/organic/boards.py index 0b57eee..0dc2163 100644 --- a/pinterest/organic/boards.py +++ b/pinterest/organic/boards.py @@ -3,6 +3,8 @@ """ from __future__ import annotations +from datetime import datetime + from openapi_generated.pinterest_client.api.boards_api import BoardsApi from openapi_generated.pinterest_client.model.board import Board as GeneratedBoard from openapi_generated.pinterest_client.model.board_update import BoardUpdate @@ -209,6 +211,12 @@ def __init__( self._description = None self._owner = None self._privacy = None + self._board_pins_modified_at = None + self._pin_count = None + self._created_at = None + self._media = None + self._collaborator_count = None + self._follower_count = None PinterestBaseModel.__init__( self, @@ -247,6 +255,36 @@ def privacy(self) -> str: # pylint: disable=missing-function-docstring return self._privacy + @property + def board_pins_modified_at(self) -> datetime: + # pylint: disable=missing-function-docstring + return self._board_pins_modified_at + + @property + def pin_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._pin_count + + @property + def created_at(self) -> datetime: + # pylint: disable=missing-function-docstring + return self._created_at + + @property + def media(self) -> str: + # pylint: disable=missing-function-docstring + return self._media + + @property + def collaborator_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._collaborator_count + + @property + def follower_count(self) -> int: + # pylint: disable=missing-function-docstring + return self._follower_count + def __repr__(self): return f"{self.__class__.__name__}(board_id={self._id})" diff --git a/pinterest/organic/pins.py b/pinterest/organic/pins.py index 5e7829c..066c4ac 100644 --- a/pinterest/organic/pins.py +++ b/pinterest/organic/pins.py @@ -6,11 +6,12 @@ from openapi_generated.pinterest_client.api.pins_api import PinsApi from openapi_generated.pinterest_client.model.pin import Pin as GeneratedPin from openapi_generated.pinterest_client.model.pin_create import PinCreate as GeneratedPinCreate -from openapi_generated.pinterest_client.model.inline_object import InlineObject +from openapi_generated.pinterest_client.model.inline_object1 import InlineObject1 from pinterest.client import PinterestSDKClient from pinterest.utils.base_model import PinterestBaseModel from pinterest.utils.error_handling import verify_api_response +from pinterest.utils.bookmark import Bookmark class Pin(PinterestBaseModel): """ @@ -55,6 +56,12 @@ def __init__( self._media = None self._media_source = None self._parent_pin_id = None + self._is_owner = None + self._pin_metrics = None + self._has_been_promoted = None + self._note = None + self._creative_type = None + self._is_standard = None PinterestBaseModel.__init__( self, @@ -135,6 +142,36 @@ def parent_pin_id(self) -> str: # pylint: disable=missing-function-docstring return self._parent_pin_id + @property + def is_owner(self) -> str: + # pylint: disable=missing-function-docstring + return self._is_owner + + @property + def pin_metrics(self) -> str: + # pylint: disable=missing-function-docstring + return self._pin_metrics + + @property + def has_been_promoted(self) -> str: + # pylint: disable=missing-function-docstring + return self._has_been_promoted + + @property + def note(self) -> str: + # pylint: disable=missing-function-docstring + return self._note + + @property + def creative_type(self) -> str: + # pylint: disable=missing-function-docstring + return self._creative_type + + @property + def is_standard(self) -> str: + # pylint: disable=missing-function-docstring + return self._is_standard + def __repr__(self): return f"{self.__class__.__name__}(pin_id={self._id}, ad_account_id={self._ad_account_id})" @@ -270,9 +307,86 @@ def save( api_response = self._generated_api.pins_save( pin_id=self.id, - inline_object=InlineObject(**inline_object_kwargs) + inline_object1=InlineObject1(**inline_object_kwargs) ) verify_api_response(api_response) self._populate_fields(_model_data=api_response.to_dict()) + + @classmethod + def get_all( + cls, + pin_filter:str = None, + include_protected_pins:bool = None, + pin_type:str = None, + creative_types:list[str] = None, + ad_account_id:str = None, + pin_metrics:bool = None, + page_size:int = None, + bookmark:str = None, + client:PinterestSDKClient = None, + **kwargs + ) -> tuple[list[Pin], Bookmark]: + """ + Get a list of the pins owned by the "operation user_account" + + Args: + pin_filter (str): Pin filter. [optional] + include_protected_pins (bool): Specify if return pins from protected boards. [optional] + if omitted the server will use the default value of False + pin_type (str): The type of pins to return, currently only enabled for private pins. [optional] + if omitted the server will use the default value of "PRIVATE" + creative_types ([str]): Pin creative types filter.

Note: SHOP_THE_PIN has + been deprecated. Please use COLLECTION instead. [optional] + ad_account_id (str): Unique identifier of an ad account. [optional] + pin_metrics (bool): Specify whether to return 90d and lifetime Pin metrics. Total comments + and total reactions are only available with lifetime Pin metrics. If Pin was created before + 2023-03-20 lifetime metrics will only be available for Video and Idea Pin formats. + Lifetime metrics are available for all Pin formats since then.. [optional] if omitted the server + will use the default value of False + page_size (int[1..100], optional): Maximum number of items to include in a single page of the response. + See documentation on Pagination for more information. Defaults to None which will + return default page size campaigns. + bookmark (str, optional): Cursor used to fetch the next page of items. Defaults to None. + client (PinterestSDKClient, optional): _description_. Defaults to None. + + Keyword Args: + Any valid keyword arguments or query parameters for endpoint. + + Returns: + list[Pin]: List of Pin Objects + Bookmark: Bookmark for pagination if present, else None. + """ + params = {} + + if pin_filter: + kwargs['pin_filter'] = pin_filter + if include_protected_pins: + kwargs['include_protected_pins'] = include_protected_pins + if pin_type: + kwargs['pin_type'] = pin_type + if creative_types: + kwargs['creative_types'] = creative_types + if ad_account_id: + kwargs['ad_account_id'] = ad_account_id + if pin_metrics: + kwargs['pin_metrics'] = pin_metrics + + def _map_function(obj): + return Pin( + pin_id=obj.get('id'), + client=client, + _model_data=obj + ) + + return cls._list( + params=params, + page_size=page_size, + bookmark=bookmark, + api=PinsApi, + list_fn=PinsApi.pins_list, + map_fn=_map_function, + client=client, + **kwargs + ) diff --git a/requirements.txt b/requirements.txt index 101fee2..5ee48de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Pinterest-Generated-Client==0.1.7 +Pinterest-Generated-Client==0.1.9 python-dateutil==2.8.2 six==1.16.0 urllib3==1.26.12 diff --git a/tests/src/pinterest/ads/test_ad_groups.py b/tests/src/pinterest/ads/test_ad_groups.py index af61403..c271899 100644 --- a/tests/src/pinterest/ads/test_ad_groups.py +++ b/tests/src/pinterest/ads/test_ad_groups.py @@ -8,6 +8,7 @@ from openapi_generated.pinterest_client.model.ad_group_response import AdGroupResponse from openapi_generated.pinterest_client.model.ad_group_array_response import AdGroupArrayResponse from openapi_generated.pinterest_client.model.ad_group_array_response_element import AdGroupArrayResponseElement +from openapi_generated.pinterest_client.model.targeting_spec import TargetingSpec from pinterest.ads.ad_groups import AdGroup from pinterest.ads.ads import Ad @@ -102,7 +103,7 @@ def test_update_ad_group(self, get_mock, update_mock): update_mock.__name__ = "ad_groups_update" new_name = "SDK_AD_GROUP_NEW_NAME" new_spec = { - "GENDER": ["male"] + "gender": ["male"] } get_mock.return_value = AdGroupResponse( @@ -124,7 +125,7 @@ def test_update_ad_group(self, get_mock, update_mock): ad_account_id=self.test_ad_account_id, campaign_id=self.test_campaign_id, name=new_name, - targeting_spec=new_spec + targeting_spec=TargetingSpec(**new_spec) ), exceptions=[] ) @@ -138,7 +139,7 @@ def test_update_ad_group(self, get_mock, update_mock): assert update_response == True assert getattr(ad_group_response, "_name") == new_name - assert getattr(ad_group_response, "_targeting_spec") == new_spec + assert str(getattr(ad_group_response, "_targeting_spec")) == str(new_spec) @patch('pinterest.ads.ad_groups.AdGroupsApi.ad_groups_list') @patch('pinterest.ads.ad_groups.AdGroupsApi.ad_groups_get')