├── pytest.ini
├── samples
├── .gitkeep
├── create_campaign.py
├── create_keyword.py
├── create_adgroup.py
├── create_ad.py
├── create_customer_list.py
└── create_audience.py
├── tests
├── __init__.py
└── src
│ ├── __init__.py
│ └── pinterest
│ ├── __init__.py
│ ├── utils
│ ├── __init__.py
│ ├── test_load_json_config.py
│ └── test_error_handling.py
│ ├── bin
│ └── get_config_test.py
│ ├── organic
│ ├── test_pins.py
│ └── test_boards.py
│ ├── ads
│ ├── test_conversion_events.py
│ ├── test_keywords.py
│ └── test_audiences.py
│ └── client
│ └── client_test.py
├── pinterest
├── ads
│ ├── __init__.py
│ ├── conversion_events.py
│ └── keywords.py
├── bin
│ ├── __init__.py
│ └── get_config.py
├── utils
│ ├── __init__.py
│ ├── sdk_exceptions.py
│ ├── load_json_config.py
│ ├── bookmark.py
│ ├── error_handling.py
│ ├── refresh_access_token.py
│ └── base_model.py
├── organic
│ └── __init__.py
├── version.py
├── __init__.py
├── config.py
└── client
│ └── __init__.py
├── integration_tests
├── __init__.py
├── ads
│ ├── __init__.py
│ ├── test_conversion_events.py
│ ├── test_audiences.py
│ ├── test_keywords.py
│ ├── test_conversion_tags.py
│ ├── test_ads.py
│ ├── test_ad_accounts.py
│ ├── test_ad_groups.py
│ └── test_customer_lists.py
├── config.py
├── clean_organic_data.py
├── client
│ └── test_client.py
├── utils
│ └── organic_utils.py
├── test_pinterest_base_model.py
├── base_test.py
└── organic
│ └── test_pins.py
├── config.json
├── setup.cfg
├── docs
├── pinterest
│ ├── .pages
│ ├── ads.md
│ ├── utils.md
│ ├── organic.md
│ ├── utils.error_handling.md
│ ├── utils.sdk_exceptions.md
│ ├── utils.base_model.md
│ ├── utils.refresh_access_token.md
│ ├── utils.load_json_config.md
│ ├── utils.bookmark.md
│ ├── client.md
│ ├── ads.conversion_events.md
│ ├── ads.keywords.md
│ ├── README.md
│ ├── organic.pins.md
│ ├── ads.conversion_tags.md
│ └── ads.customer_lists.md
└── utils
│ ├── README.md
│ ├── skeleton-spec.yaml
│ └── script.py
├── requirements.txt
├── package_test
├── run.sh
└── main.py
├── dev-requirements.txt
├── .flake8
├── config.json.example
├── .env.example
├── .github
├── workflows
│ ├── build.yml
│ ├── lint.yml
│ ├── unittests.yml
│ ├── integrationtests.yml
│ ├── publish-pypi.yml
│ ├── publish-pypi-test.yml
│ ├── packagetest.yml
│ └── cron_integrationtests.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── CODE_OF_CONDUCT.md
├── .gitignore
├── Makefile
├── CONTRIBUTING.md
└── setup.py
/pytest.ini:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/samples/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/src/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pinterest/ads/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pinterest/bin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pinterest/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/integration_tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/integration_tests/ads/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pinterest/organic/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/src/pinterest/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/src/pinterest/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {"state":"package_test"}
2 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=99
3 |
--------------------------------------------------------------------------------
/pinterest/version.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest SDK Packages Version
3 | """
4 | __version__ = '0.2.6'
5 |
--------------------------------------------------------------------------------
/docs/pinterest/.pages:
--------------------------------------------------------------------------------
1 | title: API Reference
2 | nav:
3 | - Overview: README.md
4 | - ...
5 |
--------------------------------------------------------------------------------
/pinterest/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest SDK for ads
3 | """
4 | __path__ = __import__("pkgutil").extend_path(__path__, __name__)
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Pinterest-Generated-Client==0.1.10
2 | python-dateutil==2.8.2
3 | six==1.16.0
4 | urllib3>=1.26.12
5 | python-dotenv>=0.20.0
--------------------------------------------------------------------------------
/package_test/run.sh:
--------------------------------------------------------------------------------
1 | python -m venv .venv
2 | source .venv/bin/activate
3 | pip install .
4 | echo '{"state":"package_test"}' > config.json
5 | python package_test/main.py
6 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 |
3 | pytest==7.1.2
4 | requests-mock~=1.8.0
5 | black==22.3.0
6 | flake8==3.8.4
7 | isort==5.6.4
8 | pylint==2.15.0
9 | pytest-cov
10 | parameterized==0.8.1
11 | lazydocs==0.4.8
12 | pyyaml==6.0
13 | markdown==3.4.1
--------------------------------------------------------------------------------
/docs/pinterest/ads.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `ads`
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils`
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/pinterest/organic.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `organic`
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/samples/create_campaign.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest campaign."""
2 |
3 | from pinterest.ads.campaigns import Campaign
4 |
5 | campaign = Campaign.create(
6 | ad_account_id="12345",
7 | name="SDK Test Campaign",
8 | objective_type="AWARENESS",
9 | daily_spend_cap=10,
10 | )
11 |
12 | print('New campaign created: ', campaign)
13 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | generated,
4 | __pycache__,
5 | venv,
6 | .venv,
7 | .git,
8 | .tox,
9 | dist,
10 | doc,
11 | *openstack/common/*,
12 | *lib/python*,
13 | *egg,
14 | build,
15 | tools/xenserver*,
16 | releasenotes
17 |
18 | select =
19 | E9,
20 | F63,
21 | F7,
22 | F82
--------------------------------------------------------------------------------
/samples/create_keyword.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest Keyword."""
2 |
3 | from pinterest.ads.keywords import Keyword
4 |
5 | AD_ACCOUNT_ID = "12345"
6 | AD_GROUP_ID = "56789"
7 |
8 | keyword = Keyword.create(
9 | ad_account_id=AD_ACCOUNT_ID,
10 | parent_id=AD_GROUP_ID,
11 | value="baby shoes",
12 | match_type="BROAD",
13 | bid=1000,
14 | )
15 |
16 | print("New keyword: ", keyword)
17 |
--------------------------------------------------------------------------------
/config.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "debug": "true",
3 | "port": "0",
4 | "app_id": "",
5 | "app_secret": "",
6 | "redirect_uri": "localhost",
7 | "response_type": "code",
8 | "scope": "",
9 | "state": "dev",
10 | "access_token_json_path": "./",
11 | "access_token": "",
12 | "refresh_access_token": "",
13 | "api_uri": "https://api.pinterest.com/v5"
14 | }
--------------------------------------------------------------------------------
/samples/create_adgroup.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest ad group."""
2 |
3 | from pinterest.ads.ad_groups import AdGroup
4 |
5 | AD_ACCOUNT_ID = "12345"
6 | CAMPAIGN_ID = "22222"
7 |
8 | ad_group = AdGroup.create(
9 | ad_account_id=AD_ACCOUNT_ID,
10 | campaign_id=CAMPAIGN_ID,
11 | billable_event="IMPRESSION",
12 | name="My first adgroup from SDK",
13 | )
14 |
15 | print('New ad group created: ', ad_group)
16 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PINTEREST_DEBUG=True
2 | PINTEREST_PORT=0
3 | PINTEREST_APP_ID=
4 | PINTEREST_APP_SECRET=
5 | PINTEREST_REDIRECT_URI=localhost
6 | PINTEREST_RESPONSE_TYPE=code
7 | PINTEREST_SCOPE=
8 | PINTEREST_STATE=dev
9 | PINTEREST_ACCESS_TOKEN_JSON_PATH=./
10 | PINTEREST_ACCESS_TOKEN=''
11 | PINTEREST_REFRESH_ACCESS_TOKEN=''
12 | PINTEREST_API_URI=https://api.pinterest.com/v5
--------------------------------------------------------------------------------
/samples/create_ad.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest Ad."""
2 |
3 | from pinterest.ads.ads import Ad
4 |
5 | AD_ACCOUNT_ID = "12345"
6 | AD_GROUP_ID = "333333"
7 | PIN_ID = "44444"
8 |
9 | ad = Ad.create(
10 | ad_account_id=AD_ACCOUNT_ID,
11 | ad_group_id=AD_GROUP_ID,
12 | creative_type="REGULAR",
13 | pin_id=PIN_ID,
14 | name="My SDK AD",
15 | status="ACTIVE",
16 | )
17 |
18 | print('New ad created: ', ad)
19 |
--------------------------------------------------------------------------------
/samples/create_customer_list.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest Customer List."""
2 |
3 | from pinterest.ads.customer_lists import CustomerList
4 |
5 | AD_ACCOUNT_ID = "12345"
6 |
7 | customer_list = CustomerList.create(
8 | ad_account_id=AD_ACCOUNT_ID,
9 | name="SDK Test Customer List",
10 | records="test@pinterest.com,test@example.com",
11 | list_type="EMAIL",
12 | )
13 |
14 | print("New customer list: ", customer_list)
15 |
--------------------------------------------------------------------------------
/samples/create_audience.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest Audience."""
2 |
3 | from pinterest.ads.audiences import Audience
4 |
5 | AD_ACCOUNT_ID = "12345"
6 |
7 | audience = Audience.create(
8 | ad_account_id=AD_ACCOUNT_ID,
9 | name="SDK Test Audience",
10 | rule=dict(
11 | engager_type=1
12 | ),
13 | audience_type="ENGAGEMENT",
14 | description="SDK Test Audience Description",
15 | )
16 |
17 | print("New audience: ", audience)
18 |
--------------------------------------------------------------------------------
/tests/src/pinterest/bin/get_config_test.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 | import unittest
3 |
4 | from pytest import skip
5 | from pinterest.bin import get_config
6 |
7 |
8 | class GetConfigTest(unittest.TestCase):
9 | @skip(allow_module_level=True)
10 | @patch('builtins.print')
11 | def test_output_contains_pinterest_variables(self, mock_stdout):
12 | get_config.main([])
13 | self.assertIn('PINTEREST_', str(mock_stdout.call_args))
14 |
15 |
--------------------------------------------------------------------------------
/integration_tests/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dotenv import load_dotenv
3 |
4 | load_dotenv()
5 |
6 | DEFAULT_BOARD_ID = os.environ.get('DEFAULT_BOARD_ID', "")
7 | DEFAULT_BOARD_NAME = os.environ.get('DEFAULT_BOARD_NAME', "")
8 | DEFAULT_PIN_ID = os.environ.get('DEFAULT_PIN_ID', "")
9 | DEFAULT_BOARD_SECTION_ID = os.environ.get('DEFAULT_BOARD_SECTION_ID', "")
10 | OWNER_USER_ID = os.environ.get('OWNER_USER_ID', "")
11 | DEFAULT_AD_ACCOUNT_ID = os.environ.get('DEFAULT_AD_ACCOUNT_ID', "")
12 |
--------------------------------------------------------------------------------
/docs/utils/README.md:
--------------------------------------------------------------------------------
1 | # Documentation guide
2 |
3 | The documentation for this library has been generated using [lazydocs](https://github.com/ml-tooling/lazydocs). If you make change to this library, please use `script.py` to regenerate the documentation.
4 |
5 | ## Steps to generate documentation
6 |
7 | Make sure you are in virtual environment, and run `script.py`:
8 | ```
9 | python3 docs/utils/script.py
10 | ```
11 |
12 | ## Documentation guide
13 |
14 | When making change that add or remove module, you have to update `MODULES` and `IGNORED_FILES_INDEXING` accordingly and `IGNORE_MODULES` if needed.
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.8"]
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Build package
19 | run: |
20 | python -m pip install --upgrade pip
21 | make install
22 | pip install build
23 | make build
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.8"]
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | python -m pip install --upgrade pip
21 | make install_dev
22 | - name: Lint with flake8
23 | run: |
24 | make lint
--------------------------------------------------------------------------------
/.github/workflows/unittests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.8"]
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Run all unit tests
19 | env:
20 | PINTEREST_ACCESS_TOKEN: "test_token"
21 | run: |
22 | python -m pip install --upgrade pip
23 | make install_dev
24 | make unit_tests
--------------------------------------------------------------------------------
/package_test/main.py:
--------------------------------------------------------------------------------
1 | """Example code to create a Pinterest Ad."""
2 | import os
3 |
4 | from pinterest.config import PINTEREST_API_URI, PINTEREST_STATE
5 | from pinterest.client import PinterestSDKClient
6 | from pinterest.ads.ad_accounts import AdAccount
7 |
8 | DEFAULT_AD_ACCOUNT_ID = os.environ.get('PINTEREST_DEFAULT_AD_ACCOUNT_ID')
9 |
10 |
11 | def main():
12 | client = PinterestSDKClient.create_default_client()
13 | assert client is not None
14 | assert PINTEREST_API_URI is not None
15 | assert PINTEREST_STATE == 'package_test'
16 | campaigns = AdAccount(ad_account_id=DEFAULT_AD_ACCOUNT_ID).list_campaigns()
17 | assert len(campaigns) > 0
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/pinterest/bin/get_config.py:
--------------------------------------------------------------------------------
1 | """
2 | command to get config variables
3 | """
4 | import sys
5 |
6 |
7 | def main(_):
8 | # pylint: disable=import-outside-toplevel
9 | """
10 | function to get config variables
11 | """
12 | try:
13 | from pinterest.client import config as module
14 | except ImportError as exc:
15 | raise exc
16 |
17 | variables = [
18 | (key, value)
19 | for (key, value) in vars(module).items()
20 | if (isinstance(value, (str, int, float)))
21 | and not (key.startswith("_") or key.startswith("__"))
22 | ]
23 |
24 | for (key, value) in variables:
25 | print(key, '=', value)
26 |
27 |
28 | if __name__ == "__main__":
29 | main(sys.argv)
30 |
--------------------------------------------------------------------------------
/.github/workflows/integrationtests.yml:
--------------------------------------------------------------------------------
1 | name: Integration Tests
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | environment: integ
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: ["3.8"]
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Run all integration tests
20 | env:
21 | PINTEREST_JSON_ENV_VARIABLES: ${{ secrets.CI_INTEG_TEST_JSON }}
22 | run: |
23 | python -m pip install --upgrade pip
24 | make install_dev
25 | make integration_tests
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish Pypi
2 |
3 | on: [workflow_dispatch]
4 |
5 | jobs:
6 | build:
7 | environment: integ
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: ["3.9"]
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Run publish pypi
20 | env:
21 | TWINE_USERNAME: ${{ secrets.CI_USERNAME_PYPI }}
22 | TWINE_PASSWORD: ${{ secrets.CI_PASSWORD_PYPI }}
23 | run: |
24 | python -m pip install --upgrade pip
25 | make publish_pypi
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish-pypi-test.yml:
--------------------------------------------------------------------------------
1 | name: Publish Test
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | environment: integ
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: ["3.9"]
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Run publish pypi test
20 | env:
21 | TWINE_USERNAME: ${{ secrets.CI_USERNAME_TEST_PYPI }}
22 | TWINE_PASSWORD: ${{ secrets.CI_PASSWORD_TEST_PYPI }}
23 | run: |
24 | python -m pip install --upgrade pip
25 | make publish_pypi_test
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: Feature Request -
5 | labels: enhancement
6 |
7 | ---
8 |
9 | **What is your feature request? Please describe.**
10 |
11 | A clear and concise description of what the problem is or what new feature you would like us to add. Ex. I'm always frustrated when [...] or I would like to be able to [...]
12 |
13 | **Describe the solution you'd like**
14 |
15 | A clear and concise description of what you want to happen.
16 |
17 | **Describe alternatives you've considered**
18 |
19 | A clear and concise description of any alternative solutions or features you've considered.
20 |
21 | **Additional context**
22 |
23 | Add any other context or screenshots about the feature request here.
24 |
--------------------------------------------------------------------------------
/integration_tests/clean_organic_data.py:
--------------------------------------------------------------------------------
1 | """
2 | Delete all non-essential organic data from test user
3 | """
4 |
5 | from pinterest.organic.boards import Board
6 | from pinterest.organic.pins import Pin
7 | from integration_tests.config import DEFAULT_BOARD_ID, DEFAULT_PIN_ID
8 |
9 | def test_delete_organic_data():
10 | """
11 | Delete organic boards from default client
12 | """
13 | all_boards, _ = Board.get_all()
14 | for board in all_boards:
15 | if board.id == DEFAULT_BOARD_ID:
16 | continue
17 | Board.delete(board_id=board.id)
18 | assert len(Board.get_all()[0]) == 1
19 |
20 | all_pins, _ = Pin.get_all()
21 | for pin in all_pins:
22 | if pin.id == DEFAULT_PIN_ID:
23 | continue
24 | Pin.delete(pin_id=pin.id)
25 | assert len(Pin.get_all()[0]) == 1
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: BUG -
5 | labels: bug
6 |
7 | ---
8 |
9 | **Describe the bug**
10 |
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 |
15 | Steps to reproduce the behavior:
16 | 1. The model/class with an issue is '....'
17 | 2. The function with an issue is '....'
18 | 3. The arguments passed to the model/function are '....'
19 | 4. The error thrown is '....'
20 |
21 | **Expected behavior**
22 |
23 | A clear and concise description of what you expected to happen.
24 |
25 | **Screenshots**
26 |
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Additional Details (please complete the following information):**
30 |
31 | - OS and Version: [e.g. MacOS, Windows]
32 | - SDK Version [e.g. v0.1.0]
33 |
34 | **Additional Context**
35 |
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/docs/utils/skeleton-spec.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.3
2 | info:
3 | version: 0.1.4
4 | title: Pinterest Python SDK
5 | description: Pinterest SDK for Python
6 | contact:
7 | name: Pinterest, Inc.
8 | url: https://github.com/pinterest/pinterest-python-sdk
9 | email: sdk@pinterest.com
10 | license:
11 | name: MIT
12 | url: https://spdx.org/licenses/MIT
13 | termsOfService: https://developers.pinterest.com/terms/
14 | servers:
15 | - url: https://api.pinterest.com/v5
16 | paths:
17 | /test:
18 | get:
19 | summary: Arbitrate summary
20 | description: This is an arbitrate endpoint, please visit https://developers.pinterest.com/docs/api/v5/ for more information
21 | responses:
22 | '200': # status code
23 | description: A JSON array of user names
24 | content:
25 | application/json:
26 | schema:
27 | type: array
28 | items:
29 | type: string
30 |
--------------------------------------------------------------------------------
/.github/workflows/packagetest.yml:
--------------------------------------------------------------------------------
1 | name: Package Install Test
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | environment: integ
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | python-version: ["3.8"]
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python ${{ matrix.python-version }}
16 | uses: actions/setup-python@v4
17 | with:
18 | python-version: ${{ matrix.python-version }}
19 | - name: Run package test
20 | env:
21 | PINTEREST_REFRESH_ACCESS_TOKEN: ${{ secrets.CI_REFRESH_ACCESS_TOKEN }}
22 | PINTEREST_APP_SECRET: ${{ secrets.CI_APP_SECRET }}
23 | PINTEREST_APP_ID: ${{ secrets.CI_APP_ID }}
24 | PINTEREST_API_URI: ${{ secrets.CI_HOST_URI }}
25 | PINTEREST_DEFAULT_AD_ACCOUNT_ID: ${{ secrets.CI_DEFAULT_AD_ACCOUNT_ID }}
26 | run: |
27 | python -m pip install --upgrade pip
28 | make package_test
29 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.error_handling.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.error_handling`
6 | Util error handling function
7 |
8 |
9 | ---
10 |
11 |
12 |
13 | ## function `verify_api_response`
14 |
15 | ```python
16 | verify_api_response(response) → bool
17 | ```
18 |
19 | Verify that there are no errors in `response` received from api
20 |
21 |
22 |
23 | **Args:**
24 |
25 | - `response`: Response received from api request
26 |
27 |
28 |
29 | **Returns:**
30 |
31 | - `bool`: If the `response` is without any exceptions
32 |
33 |
34 |
--------------------------------------------------------------------------------
/pinterest/utils/sdk_exceptions.py:
--------------------------------------------------------------------------------
1 | """
2 | SDK Exceptions for error handling in the models.
3 | """
4 |
5 | class SdkException(Exception):
6 | """Raises an exception for Model's Errors"""
7 | def __init__(self, status=None, reason=None, http_resp=None, body=None):
8 | if http_resp:
9 | self.status = http_resp.status
10 | self.reason = http_resp.reason
11 | self.body = http_resp.data
12 | self.headers = http_resp.getheaders()
13 | else:
14 | self.status = status
15 | self.reason = reason
16 | self.body = body
17 | self.headers = None
18 |
19 | def __str__(self):
20 | """Custom error messages for exception"""
21 | error_message = f"({self.status})\n"\
22 | f"Reason: {self.reason}\n"
23 | if self.headers:
24 | error_message += f"HTTP response headers: {self.headers}\n"
25 |
26 | if self.body:
27 | error_message += f"HTTP response body: {self.body}\n"
28 | return error_message
29 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.sdk_exceptions.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.sdk_exceptions`
6 | SDK Exceptions for error handling in the models.
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `SdkException`
15 | Raises an exception for Model's Errors
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(status=None, reason=None, http_resp=None, body=None)
23 | ```
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 | venv/
48 | .venv/
49 | .python-version
50 | .pytest_cache
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 |
59 | # Sphinx files/dirs
60 | docs/doctrees/
61 |
62 | # PyBuilder
63 | target/
64 |
65 | #Ipython Notebook
66 | .ipynb_checkpoints
67 |
68 | # IDEs
69 | .idea
70 |
71 | # Config files
72 | .env
73 | http_logs.txt
--------------------------------------------------------------------------------
/tests/src/pinterest/organic/test_pins.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Pin Model
3 | """
4 |
5 | from unittest import TestCase
6 | from unittest.mock import patch
7 |
8 | from pinterest.client import PinterestSDKClient
9 |
10 | from openapi_generated.pinterest_client.model.pin import Pin as GeneratedPin
11 |
12 | from pinterest.organic.pins import Pin
13 |
14 | class TestPin(TestCase):
15 | """
16 | Test Pin model and its higher level functions
17 | """
18 | def __init__(self, *args, **kwargs):
19 | super().__init__(*args, **kwargs)
20 | PinterestSDKClient.set_default_access_token("test_token")
21 | self.test_pin_id = "111111111111"
22 |
23 | @patch('pinterest.organic.pins.PinsApi.pins_get')
24 | def test_create_pins_model_using_existing_pin(self, pins_get_mock):
25 | """
26 | Test if a Pin model/object is created successfully with correct pin_id
27 | """
28 | pins_get_mock.return_value = GeneratedPin(
29 | title="SDK Test Pin",
30 | description="SDK Test Pin Description",
31 | )
32 |
33 | pin_response = Pin(
34 | pin_id=self.test_pin_id,
35 | )
36 |
37 | assert pin_response
38 | assert pin_response.title == "SDK Test Pin"
39 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.base_model.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.base_model`
6 | Pinterest Base Model
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `PinterestBaseModel`
15 | Base Model for all other Higher Level Models in the Python Client
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(
23 | _id: str,
24 | generated_api: object,
25 | generated_api_get_fn: str,
26 | generated_api_get_fn_args: dict,
27 | model_attribute_types: dict,
28 | client=None
29 | )
30 | ```
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/src/pinterest/organic/test_boards.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Board Model
3 | """
4 |
5 | from unittest import TestCase
6 | from unittest.mock import patch
7 |
8 | from pinterest.client import PinterestSDKClient
9 | from openapi_generated.pinterest_client.model.board import Board as GeneratedBoard
10 |
11 | from pinterest.organic.boards import Board
12 |
13 | class TestBoard(TestCase):
14 | """
15 | Test Board model and its higher level functions
16 | """
17 | def __init__(self, *args, **kwargs):
18 | super().__init__(*args, **kwargs)
19 | PinterestSDKClient.set_default_access_token("test_token")
20 | self.test_board_id = "111111111111"
21 |
22 | @patch('pinterest.organic.boards.BoardsApi.boards_get')
23 | def test_create_board_model_using_existing_board(self, boards_get_mock):
24 | """
25 | Test if a Board model/object is created successfully with correct board_id
26 | """
27 |
28 | boards_get_mock.return_value = GeneratedBoard(
29 | name="SDK Test Board",
30 | description="SDK Test Board Description",
31 | privacy="PUBLIC"
32 | )
33 |
34 | board_response = Board(
35 | board_id=self.test_board_id,
36 | )
37 |
38 | assert board_response
39 | assert board_response.name == "SDK Test Board"
40 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.refresh_access_token.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.refresh_access_token`
6 | This script has functions for generating a new ACCESSTOKEN using the REFRESHTOKEN
7 |
8 |
9 | ---
10 |
11 |
12 |
13 | ## function `get_new_access_token`
14 |
15 | ```python
16 | get_new_access_token(
17 | app_id: str,
18 | app_secret: str,
19 | refresh_access_token: str,
20 | host: str
21 | ) → str
22 | ```
23 |
24 | Function used to retrieve a new access token for a user using the refresh token.
25 |
26 | **Args:**
27 |
28 | - `app_id` (str): APP_ID or CLIENT_ID
29 | - `app_secret` (str): APP_SECRET
30 | - `refresh_access_token` (str): Refresh access token retrieved from oauth.
31 | - `host` (str): base url of the host
32 |
33 | **Returns:**
34 |
35 | - `str`: New access token
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.load_json_config.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.load_json_config`
6 | module with the function `load_json_config` that parse a config.json file and then load all the variables found as environment variables.
7 |
8 |
9 | ---
10 |
11 |
12 |
13 | ## function `load_json_config`
14 |
15 | ```python
16 | load_json_config()
17 | ```
18 |
19 | Parse a config.json file and then load all the variables found as environment variables.
20 |
21 |
22 | ---
23 |
24 |
25 |
26 | ## function `load_json_config_from_single_env_var`
27 |
28 | ```python
29 | load_json_config_from_single_env_var()
30 | ```
31 |
32 | Parse PINTEREST_JSON_ENV_VARIABLES environment variable to split long JSON string into individual environment variables.
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/cron_integrationtests.yml:
--------------------------------------------------------------------------------
1 | name: Cron Integration Tests
2 |
3 | on:
4 | schedule:
5 | - cron: "0 17 * * *"
6 |
7 | jobs:
8 | build:
9 | environment: integ
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | python-version: ["3.8"]
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | - name: Set up Python ${{ matrix.python-version }}
18 | uses: actions/setup-python@v4
19 | with:
20 | python-version: ${{ matrix.python-version }}
21 | - name: Run all integration tests
22 | env:
23 | PINTEREST_REFRESH_ACCESS_TOKEN: ${{ secrets.CI_REFRESH_ACCESS_TOKEN }}
24 | PINTEREST_APP_SECRET: ${{ secrets.CI_APP_SECRET }}
25 | PINTEREST_APP_ID: ${{ secrets.CI_APP_ID }}
26 | PINTEREST_API_URI: ${{ secrets.CI_HOST_URI }}
27 | CONVERSION_ACCESS_TOKEN: ${{ secrets.CI_CONVERSION_ACCESS_TOKEN }}
28 | DEFAULT_BOARD_ID: ${{ secrets.CI_DEFAULT_BOARD_ID }}
29 | DEFAULT_BOARD_NAME: ${{ secrets.CI_DEFAULT_BOARD_NAME }}
30 | DEFAULT_PIN_ID: ${{ secrets.CI_DEFAULT_PIN_ID }}
31 | DEFAULT_BOARD_SECTION_ID: ${{ secrets.CI_DEFAULT_BOARD_SECTION_ID }}
32 | OWNER_USER_ID: ${{ secrets.CI_OWNER_USER_ID }}
33 | DEFAULT_AD_ACCOUNT_ID: ${{ secrets.CI_DEFAULT_AD_ACCOUNT_ID }}
34 | run: |
35 | python -m pip install --upgrade pip
36 | make install_dev
37 | make integration_tests
38 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | At Pinterest, we work hard to ensure that our work environment is welcoming
4 | and inclusive to as many people as possible. We are committed to creating this
5 | environment for everyone involved in our open source projects as well. We
6 | welcome all participants regardless of ability, age, ethnicity, identified
7 | gender, religion (or lack there of), sexual orientation and socioeconomic
8 | status.
9 |
10 | This code of conduct details our expectations for upholding these values.
11 |
12 | ## Good behavior
13 |
14 | We expect members of our community to exhibit good behavior including (but of
15 | course not limited to):
16 |
17 | - Using intentional and empathetic language.
18 | - Focusing on resolving instead of escalating conflict.
19 | - Providing constructive feedback.
20 |
21 | ## Unacceptable behavior
22 |
23 | Some examples of unacceptable behavior (again, this is not an exhaustive
24 | list):
25 |
26 | - Harassment, publicly or in private.
27 | - Trolling.
28 | - Sexual advances (this isn’t the place for it).
29 | - Publishing other’s personal information.
30 | - Any behavior which would be deemed unacceptable in a professional environment.
31 |
32 | ## Recourse
33 |
34 | If you are witness to or the target of unacceptable behavior, it should be
35 | reported to Pinterest at opensource-policy@pinterest.com. All reporters will
36 | be kept confidential and an appropriate response for each incident will be
37 | evaluated.
38 |
39 | If the maintainers do not uphold and enforce this code of conduct in
40 | good faith, community leadership will hold them accountable.
--------------------------------------------------------------------------------
/tests/src/pinterest/utils/test_load_json_config.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Load Json Config
3 | """
4 | import os
5 | from unittest import TestCase, mock
6 |
7 | import pinterest.utils.load_json_config as load_json_config_module
8 |
9 |
10 | def _get_var_value():
11 | # pylint: disable=import-outside-toplevel
12 | """this import are for testing purpose"""
13 | return os.environ.get('PINTEREST_DUMMY', '1234567890')
14 |
15 |
16 | class TestLoadJsonConfig(TestCase):
17 | """
18 | Test Load Json Config
19 | """
20 |
21 | def setUp(self) -> None:
22 | super().setUp()
23 | # pylint: disable=protected-access
24 | # Mock ._find_config_json_path and _load_json_file to return a dummy path to test
25 | load_json_config_module._find_config_json_path = lambda x: r'\foo\config.json'
26 | load_json_config_module._load_json_file = lambda x: {'state': '1234567890'}
27 |
28 | @mock.patch.dict(os.environ, {"PINTEREST_STATE": "1234567890"})
29 | def test_load_json_config(self):
30 | """
31 | Verify if the function successfully load config.json
32 | """
33 | load_json_config_module.load_json_config()
34 | self.assertEqual(_get_var_value(), '1234567890')
35 |
36 | def test_not_load_json_config(self):
37 | """
38 | Verify if the function not load config.json
39 | """
40 | prev_load_value = _get_var_value()
41 | load_json_config_module.load_json_config()
42 | post_load_value = _get_var_value()
43 | self.assertNotEqual(prev_load_value, '12345678901234')
44 | self.assertEqual(prev_load_value, post_load_value)
45 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: integration_tests lint unit_tests clean_organic_data
2 |
3 | install:
4 | @echo pip install
5 | pip install -r requirements.txt
6 |
7 | install_dev:
8 | @echo pip install dev
9 | pip install -r dev-requirements.txt
10 |
11 | unit_tests:
12 | @echo unit test...
13 | python -m pytest ./tests/src
14 |
15 | package_test:
16 | ./package_test/run.sh
17 |
18 | clean_organic_data:
19 | @echo cleaning organic data...
20 | python -m pytest ./integration_tests/clean_organic_data.py
21 |
22 | integration_tests: clean_organic_data
23 | @echo integration tests...
24 | python -m pytest --ignore=clean_organic_data.py --cov ./pinterest/ --cov-branch ./integration_tests/ --cov-report term-missing
25 |
26 | clean: clean-build clean-pyc ## Clean
27 |
28 | clean-build: ## Clean python build
29 | rm -fr build/
30 | rm -fr dist/
31 | rm -fr *.egg-info
32 | rm -fr .tox
33 |
34 | clean-pyc: ## Clean python binaries
35 | find . -name '*.pyc' -exec rm -f {} +
36 | find . -name '*.pyo' -exec rm -f {} +
37 | find . -name '*~' -exec rm -f {} +
38 |
39 | build: ## Build command
40 | python -m build
41 | ls -l dist
42 |
43 | build_test: ## Build test command
44 | IS_TEST_BUILD=1 python -m build
45 | ls -l dist
46 |
47 | pip_release_install:
48 | pip install twine build
49 |
50 | publish_pypi_test: clean pip_release_install build_test
51 | twine upload -r testpypi dist/*
52 |
53 | publish_pypi: clean pip_release_install build
54 | twine upload -r pypi dist/*
55 |
56 | pylint:
57 | @echo lint
58 | pylint .
59 |
60 | flake:
61 | flake8 . --count --show-source --statistics
62 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
63 |
64 | lint: pylint flake
65 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: contributing
3 | title: Contributing
4 | sidebar_label: Contributing
5 | ---
6 |
7 | First off, thanks for taking the time to contribute! This guide will answer
8 | some common questions about how this project works.
9 |
10 | While this is a Pinterest open source project, we welcome contributions from
11 | everyone.
12 |
13 | ## Code of Conduct
14 |
15 | Please be sure to read and understand our [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
16 | We work hard to ensure that our projects are welcoming and inclusive to as many
17 | people as possible.
18 |
19 | ## Reporting Issues
20 |
21 | If you have a bug report, please provide as much information as possible so that
22 | we can help you out:
23 |
24 | - Version of the project you're using.
25 | - Code (or even better whole projects) which reproduce the issue.
26 | - Steps which reproduce the issue.
27 | - Screenshots, GIFs or videos (if relavent).
28 | - Stack traces for crashes.
29 | - Any logs produced.
30 |
31 | Open an [issue](https://github.com/pinterest/pinterest-python-sdk/issues/new) in this repo.
32 |
33 | ## Make changes
34 |
35 | Right now as BETA release we are not accepting external changes but that is something that we are in our roadmap for the near future.
36 |
37 | If you have any problem feel free to open an [issue](https://github.com/pinterest/pinterest-python-sdk/issues/new) in this repo.
38 |
39 | ## Help
40 |
41 | If you have any problem feel free to open an [issue](https://github.com/pinterest/pinterest-python-sdk/issues/new) in this repo.
42 |
43 | ## Security
44 |
45 | If you've found a security issue in one of our open source projects,
46 | please report it at [Bugcrowd](https://bugcrowd.com/pinterest); you may even
47 | make some money!
--------------------------------------------------------------------------------
/pinterest/utils/load_json_config.py:
--------------------------------------------------------------------------------
1 | """
2 | module with the function `load_json_config` that parse a config.json file and then load all the variables found as
3 | environment variables.
4 | """
5 | import json
6 | import os
7 |
8 | __all__ = ['load_json_config', 'load_json_config_from_single_env_var']
9 |
10 | _PREFIX = 'PINTEREST_'
11 |
12 |
13 | def load_json_config():
14 | """Parse a config.json file and then load all the variables found as environment variables."""
15 | current_dir = _get_current_dir()
16 |
17 | config_json_file_path = _find_config_json_path(current_dir)
18 | if not config_json_file_path:
19 | return
20 |
21 | config_json = _load_json_file(config_json_file_path)
22 |
23 | for attribute, value in config_json.items():
24 | _set_as_environment_variables(f'{_PREFIX}{attribute.upper()}', str(value))
25 |
26 | def load_json_config_from_single_env_var():
27 | """
28 | Parse PINTEREST_JSON_ENV_VARIABLES environment variable to split long JSON string into
29 | individual environment variables.
30 | """
31 | config_json = os.environ.get('PINTEREST_JSON_ENV_VARIABLES')
32 | if not config_json:
33 | return
34 |
35 | config_json = json.loads(config_json)
36 |
37 | for attribute, value in config_json.items():
38 | os.environ[attribute] = str(value)
39 |
40 |
41 | def _get_current_dir():
42 | return os.path.abspath(os.path.join(os.getcwd(), os.path.pardir))
43 |
44 |
45 | def _find_config_json_path(path: str, file_name: str = 'config.json'):
46 | for root, _, files in os.walk(path):
47 | if file_name in files:
48 | return os.path.join(root, file_name)
49 | return None
50 |
51 |
52 | def _load_json_file(file_path: str) -> dict:
53 | with open(file_path, 'r', encoding='UTF-8') as jsonfile:
54 | return json.load(jsonfile)
55 |
56 |
57 | def _set_as_environment_variables(key: str, value: str):
58 | os.environ[key] = value
59 |
--------------------------------------------------------------------------------
/pinterest/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest config
3 | """
4 | import os as _os
5 | from dotenv import load_dotenv as _load_env_vars
6 | from pinterest.version import __version__
7 | from pinterest.utils.load_json_config import load_json_config as _load_json,\
8 | load_json_config_from_single_env_var as _load_json_single_variable
9 |
10 | _load_env_vars()
11 | _load_json()
12 | _load_json_single_variable()
13 |
14 | PINTEREST_DEBUG = _os.environ.get('PINTEREST_DEBUG', "False").lower() == "true"
15 | PINTEREST_PORT = _os.environ.get('PINTEREST_PORT', 0)
16 | PINTEREST_APP_ID = _os.environ.get('PINTEREST_APP_ID', 0)
17 | PINTEREST_APP_SECRET = _os.environ.get('PINTEREST_APP_SECRET', '')
18 | PINTEREST_CLIENT_ID = _os.environ.get('PINTEREST_CLIENT_ID', '')
19 | PINTEREST_REDIRECT_URI = _os.environ.get('PINTEREST_REDIRECT_URI', '')
20 | PINTEREST_RESPONSE_TYPE = _os.environ.get('PINTEREST_RESPONSE_TYPE', '')
21 | PINTEREST_SCOPE = _os.environ.get('PINTEREST_SCOPE', '')
22 | PINTEREST_STATE = _os.environ.get('PINTEREST_STATE', '')
23 | PINTEREST_ACCESS_TOKEN_JSON_PATH = _os.environ.get('PINTEREST_ACCESS_TOKEN_JSON_PATH', '')
24 | PINTEREST_ACCESS_TOKEN = _os.environ.get('PINTEREST_ACCESS_TOKEN')
25 | PINTEREST_REFRESH_ACCESS_TOKEN = _os.environ.get('PINTEREST_REFRESH_ACCESS_TOKEN')
26 | PINTEREST_API_URI = _os.environ.get('PINTEREST_API_URI', 'https://api.pinterest.com/v5')
27 | PINTEREST_LOG_FILE = _os.environ.get('PINTEREST_LOG_FILE', None)
28 | DEFAULT_DISABLE_VALIDATIONS = ",".join([
29 | 'multipleOf', 'maximum', 'exclusiveMaximum',
30 | 'minimum', 'exclusiveMinimum', 'maxLength',
31 | 'minLength', 'pattern', 'maxItems', 'minItems',
32 | ])
33 | PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS = _os.environ.get(
34 | 'PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS',
35 | DEFAULT_DISABLE_VALIDATIONS
36 | )
37 | PINTEREST_LOGGER_FORMAT = _os.environ.get('PINTEREST_LOGGER_FORMAT', '%(asctime)s %(levelname)s %(message)s')
38 | PINTEREST_SDK_VERSION = __version__
39 | PINTEREST_USER_AGENT = f'pins-sdk/python/v{PINTEREST_SDK_VERSION}'
40 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest Client Package Setup
3 | """
4 | import os
5 | from datetime import datetime
6 | from pathlib import Path
7 | from setuptools import setup, find_namespace_packages
8 |
9 |
10 | def _get_test_version():
11 | return datetime.today().strftime('%m%d%Y%H%M%S')
12 |
13 |
14 | def _get_prod_version():
15 | module = {}
16 | with open(os.path.join(package_root, "pinterest/version.py"), encoding='UTF-8') as fp:
17 | exec(fp.read(), module) # pylint: disable=exec-used
18 | return module.get("__version__")
19 |
20 |
21 | _IS_TEST_BUILD = os.environ.get("IS_TEST_BUILD", 0)
22 |
23 | REQUIRES = [
24 | "urllib3>=1.26.12",
25 | "python-dateutil",
26 | "python-dotenv>=0.20.0",
27 | "six==1.16.0",
28 | "Pinterest-Generated-Client==0.1.10"
29 | ]
30 |
31 | long_description = (Path(__file__).parent / "README.md").read_text()
32 | package_root = os.path.abspath(os.path.dirname(__file__))
33 |
34 | __version__ = None
35 |
36 | if _IS_TEST_BUILD:
37 | print("* Test build enable")
38 | __version__ = _get_test_version()
39 | else:
40 | __version__ = _get_prod_version()
41 |
42 | if __version__ is None:
43 | raise ValueError("Version is not defined")
44 |
45 | setup(
46 | name="pinterest-api-sdk",
47 | description="Pinterest API SDK",
48 | version=__version__,
49 | author="Pinterest, Inc.",
50 | author_email="sdk@pinterest.com",
51 | url="https://github.com/pinterest/pinterest-python-sdk",
52 | install_requires=REQUIRES,
53 | include_package_data=True,
54 | packages=find_namespace_packages(
55 | include=['pinterest.*', 'pinterest', 'pinterest.version', 'pinterest.config'],
56 | exclude=[
57 | 'sample',
58 | 'sample.*',
59 | 'tests',
60 | 'tests.*',
61 | 'integration_tests',
62 | 'integration_tests.*',
63 | '.github',
64 | ]
65 | ),
66 | license='Apache License 2.0',
67 | long_description=long_description,
68 | long_description_content_type='text/markdown',
69 | )
70 |
--------------------------------------------------------------------------------
/pinterest/utils/bookmark.py:
--------------------------------------------------------------------------------
1 | """
2 | Bookmark Model
3 | """
4 | from __future__ import annotations
5 |
6 | from pinterest.client import PinterestSDKClient
7 |
8 | class Bookmark:
9 | """
10 | Bookmark Model used as a utilty to improve pagination experience for user.
11 | """
12 | def __init__(
13 | self,
14 | bookmark_token:str,
15 | model:object,
16 | model_fn:str,
17 | model_fn_args:dict,
18 | client:PinterestSDKClient
19 | ):
20 | # pylint: disable=too-many-arguments
21 | """
22 | Initialize a Bookmark object.
23 |
24 | Args:
25 | bookmark_token (str): Bookmark pagination token.
26 | model (PinterestBaseModel): The SDK Model where function was called.
27 | model_fn (str): The model's function which returns a bookmark.
28 | model_fn_args (dict): Arguments passed to the function.
29 | client (PinterestSDKClient): Client used to make the SDK call.
30 | """
31 | self.bookmark_token = bookmark_token
32 | self.model = model
33 | self.model_fn = model_fn
34 | self.model_fn_args = model_fn_args
35 | self.client = client
36 |
37 | def get_next(self) -> tuple[list[object], Bookmark]:
38 | """
39 | Function used to get the next page of items using Bookmark Pagination Token.
40 |
41 | Returns:
42 | list[PinterestBaseModel]: List of SDK Model Objects
43 | Bookmark: Bookmark Object for pagination if present, else None.
44 | """
45 | self.model_fn_args['bookmark'] = self.bookmark_token
46 | if self.client:
47 | self.model_fn_args['client'] = self.client
48 | if 'kwargs' in self.model_fn_args:
49 | kwargs = self.model_fn_args.get('kwargs')
50 | del self.model_fn_args['kwargs']
51 | self.model_fn_args.update(kwargs)
52 | return getattr(self.model, self.model_fn)(**self.model_fn_args)
53 |
54 | def get_bookmark_token(self) -> str:
55 | """
56 | Returns the bookmark pagination token in string format.
57 |
58 | Returns:
59 | str: pagination or continuatiuon token
60 | """
61 | return self.bookmark_token
62 |
--------------------------------------------------------------------------------
/pinterest/utils/error_handling.py:
--------------------------------------------------------------------------------
1 | """
2 | Util error handling function
3 | """
4 | from pinterest.utils.sdk_exceptions import SdkException
5 |
6 | def verify_api_response(response) -> bool:
7 | # pylint: disable=too-many-boolean-expressions
8 | """
9 | Verify that there are no errors in `response` received from api
10 |
11 | Args:
12 | response: Response received from api request
13 |
14 | Returns:
15 | bool: If the `response` is without any exceptions
16 | """
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 | )
49 | return True
50 |
--------------------------------------------------------------------------------
/pinterest/utils/refresh_access_token.py:
--------------------------------------------------------------------------------
1 | """
2 | This script has functions for generating a new ACCESSTOKEN using the REFRESHTOKEN
3 | """
4 |
5 | from base64 import b64encode
6 | import json
7 | import urllib3
8 |
9 | from pinterest.utils.sdk_exceptions import SdkException
10 |
11 |
12 | def get_new_access_token(
13 | app_id: str,
14 | app_secret: str,
15 | refresh_access_token: str,
16 | host: str,
17 | ) -> str:
18 | """
19 | Function used to retrieve a new access token for a user using the refresh token.
20 | Args:
21 | app_id (str): APP_ID or CLIENT_ID
22 | app_secret (str): APP_SECRET
23 | refresh_access_token (str): Refresh access token retrieved from oauth.
24 | host (str): base url of the host
25 | Returns:
26 | str: New access token
27 | """
28 | refresh_auth_token = b64encode(
29 | s=(f"{app_id}:{app_secret}").encode()
30 | ).decode("utf-8")
31 |
32 | headers = {
33 | 'Authorization': f'Basic {refresh_auth_token}',
34 | 'Content-Type': 'application/x-www-form-urlencoded',
35 | }
36 |
37 | data = f'grant_type=refresh_token&refresh_token={refresh_access_token}'
38 |
39 | response = urllib3.PoolManager().request(
40 | method='POST',
41 | url=f'{host}/oauth/token',
42 | headers=headers,
43 | body=data,
44 | timeout=5
45 | )
46 | if response.status == 401:
47 | raise SdkException(
48 | status=response.status,
49 | reason=response.reason,
50 | body="Authentication error. " +
51 | "Kindly check if the following variables are correct: [PINTEREST_ACCESS_TOKEN] or " +
52 | "[PINTEREST_APP_ID, PINTEREST_APP_SECRET, PINTEREST_REFRESH_ACCESS_TOKEN]. " +
53 | f"Response from server: {response.data}",
54 | http_resp=response
55 | )
56 | if response.status != 200:
57 | raise SdkException(http_resp=response)
58 |
59 | data = json.loads(response.data)
60 |
61 | if not data.get('access_token'):
62 | raise KeyError(f"`access_token` not found in response body. response={data}."+
63 | "Kindly check input arguments or update PINTEREST_REFRESH_TOKEN")
64 |
65 | return data.get('access_token')
66 |
--------------------------------------------------------------------------------
/tests/src/pinterest/utils/test_error_handling.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Error Handling
3 | """
4 |
5 | from unittest import TestCase
6 |
7 | from openapi_generated.pinterest_client.model.campaign_create_response import CampaignCreateResponse
8 | from openapi_generated.pinterest_client.model.campaign_create_response_item import CampaignCreateResponseItem
9 | from openapi_generated.pinterest_client.model.campaign_create_response_data import CampaignCreateResponseData
10 | from openapi_generated.pinterest_client.model.exception import Exception as GeneratedException
11 |
12 | from pinterest.utils.sdk_exceptions import SdkException
13 | from pinterest.utils.error_handling import verify_api_response
14 |
15 | class TestErrorHandling(TestCase):
16 | """
17 | Test Error Handling utility functions
18 | """
19 |
20 | def test_verify_api_response_without_exceptions(self):
21 | """
22 | Verify if the function successfully returns True when there are no errors in api response
23 | """
24 | test_api_response = CampaignCreateResponse(
25 | items=[
26 | CampaignCreateResponseItem(
27 | data=CampaignCreateResponseData(
28 | id="123123123123",
29 | ad_account_id="456456456456",
30 | name='SDK_TEST_CLIENT',
31 | ),
32 | exceptions=[]
33 | )
34 | ],
35 | )
36 | assert verify_api_response(test_api_response)
37 |
38 | def test_verify_api_response_with_exceptions(self):
39 | """
40 | Verify if the function throws `SdkException` when there are exceptions in api response
41 | """
42 | test_api_response = CampaignCreateResponse(
43 | items=[
44 | CampaignCreateResponseItem(
45 | data=CampaignCreateResponseData(),
46 | exceptions=[
47 | GeneratedException(
48 | code=1234,
49 | message="Test exception caught."
50 | )
51 | ]
52 | )
53 | ],
54 | )
55 | self.assertRaises(SdkException, verify_api_response, response=test_api_response)
56 |
--------------------------------------------------------------------------------
/docs/pinterest/utils.bookmark.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `utils.bookmark`
6 | Bookmark Model
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `Bookmark`
15 | Bookmark Model used as a utilty to improve pagination experience for user.
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(
23 | bookmark_token: 'str',
24 | model: 'object',
25 | model_fn: 'str',
26 | model_fn_args: 'dict',
27 | client: 'PinterestSDKClient'
28 | )
29 | ```
30 |
31 | Initialize a Bookmark object.
32 |
33 |
34 |
35 | **Args:**
36 |
37 | - `bookmark_token` (str): Bookmark pagination token.
38 | - `model` (PinterestBaseModel): The SDK Model where function was called.
39 | - `model_fn` (str): The model's function which returns a bookmark.
40 | - `model_fn_args` (dict): Arguments passed to the function.
41 | - `client` (PinterestSDKClient): Client used to make the SDK call.
42 |
43 |
44 |
45 |
46 | ---
47 |
48 |
49 |
50 | ### method `get_bookmark_token`
51 |
52 | ```python
53 | get_bookmark_token() → str
54 | ```
55 |
56 | Returns the bookmark pagination token in string format.
57 |
58 |
59 |
60 | **Returns:**
61 |
62 | - `str`: pagination or continuatiuon token
63 |
64 | ---
65 |
66 |
67 |
68 | ### method `get_next`
69 |
70 | ```python
71 | get_next() → tuple[list[object], Bookmark]
72 | ```
73 |
74 | Function used to get the next page of items using Bookmark Pagination Token.
75 |
76 |
77 |
78 | **Returns:**
79 |
80 | - `list[PinterestBaseModel]`: List of SDK Model Objects
81 | - `Bookmark`: Bookmark Object for pagination if present, else None.
82 |
83 |
84 |
--------------------------------------------------------------------------------
/tests/src/pinterest/ads/test_conversion_events.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from unittest.mock import patch
3 |
4 | from openapi_generated.pinterest_client.model.conversion_api_response import ConversionApiResponse
5 | from openapi_generated.pinterest_client.model.conversion_api_response_events import ConversionApiResponseEvents
6 |
7 | from pinterest.ads.conversion_events import Conversion
8 |
9 | class TestConversionEvent(TestCase):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | self.test_ad_account_id = "777777777777"
13 |
14 | @patch('pinterest.ads.conversion_events.ConversionEventsApi.events_create')
15 | def test_send_conversion_event_success(self, create_mock):
16 | """
17 | Test if ConversionEvent can be sent to Pinterest API
18 | """
19 | create_mock.return_value = ConversionApiResponse(
20 | num_events_received = 2,
21 | num_events_processed = 2,
22 | events = [
23 | ConversionApiResponseEvents(
24 | status="processed",
25 | error_message = "",
26 | warning_message = "",
27 | ),
28 | ConversionApiResponseEvents(
29 | status="processed",
30 | error_message = "",
31 | warning_message = "",
32 | )
33 | ]
34 | )
35 |
36 | NUMBER_OF_CONVERSION_EVENTS = 2
37 | raw_user_data = dict(
38 | em = ["964bbaf162703657e787eb4455197c8b35c18940c75980b0285619fe9b8acec8"] #random hash256
39 | )
40 | conversion_events = [
41 | Conversion.create_conversion_event(
42 | event_name = "add_to_cart",
43 | action_source = "app_ios",
44 | event_time = 1670026573,
45 | event_id = "eventId0001",
46 | user_data = raw_user_data,
47 | custom_data = dict(),
48 | )
49 | for _ in range(NUMBER_OF_CONVERSION_EVENTS)
50 | ]
51 |
52 | response = Conversion.send_conversion_events(
53 | ad_account_id = self.test_ad_account_id,
54 | conversion_events = conversion_events,
55 | test = True,
56 | )
57 |
58 | assert response
59 | assert response.num_events_received == 2
60 | assert response.num_events_processed == 2
61 | assert len(response.events) == 2
62 |
63 | assert response.events[0].status == "processed"
64 | assert response.events[0].error_message == ""
65 | assert response.events[0].warning_message == ""
66 |
67 | assert response.events[1].status == "processed"
68 | assert response.events[1].error_message == ""
69 | assert response.events[1].warning_message == ""
70 |
--------------------------------------------------------------------------------
/integration_tests/client/test_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from unittest import mock
4 | from unittest.mock import patch
5 |
6 | from pinterest.utils.refresh_access_token import get_new_access_token
7 |
8 | from pinterest import config
9 |
10 | from openapi_generated.pinterest_client.exceptions import UnauthorizedException
11 |
12 | from pinterest.organic.boards import Board
13 | from pinterest.client import PinterestSDKClient
14 | from integration_tests.base_test import BaseTestCase
15 | from pinterest.utils.error_handling import SdkException
16 |
17 |
18 | class ClientTest(BaseTestCase):
19 |
20 | def test_bad_setup_default_access_token(self):
21 | sample_access_token = 'test_token'
22 | PinterestSDKClient.set_default_access_token(sample_access_token)
23 | with self.assertRaises(UnauthorizedException):
24 | Board.get_all()
25 |
26 | def test_create_custom_client_with_refresh_token(self):
27 | client = PinterestSDKClient.create_client_with_refresh_token(
28 | refresh_token=config.PINTEREST_REFRESH_ACCESS_TOKEN,
29 | app_id=config.PINTEREST_APP_ID,
30 | app_secret=config.PINTEREST_APP_SECRET
31 | )
32 | self.assertIsNotNone(Board.get_all(client=client))
33 |
34 | def test_client_custom_client_with_access_token(self):
35 | access_token = get_new_access_token(
36 | app_id=config.PINTEREST_APP_ID,
37 | app_secret=config.PINTEREST_APP_SECRET,
38 | refresh_access_token=config.PINTEREST_REFRESH_ACCESS_TOKEN,
39 | host=config.PINTEREST_API_URI,
40 | )
41 | client = PinterestSDKClient.create_client_with_token(access_token=access_token)
42 | self.assertIsNotNone(Board.get_all(client=client))
43 |
44 | def test_good_setup_default_access_token(self):
45 |
46 | bad_access_token = 'test_token'
47 | PinterestSDKClient.set_default_access_token(bad_access_token)
48 | with self.assertRaises(UnauthorizedException):
49 | Board.get_all()
50 |
51 | good_access_token = get_new_access_token(
52 | app_id=config.PINTEREST_APP_ID,
53 | app_secret=config.PINTEREST_APP_SECRET,
54 | refresh_access_token=config.PINTEREST_REFRESH_ACCESS_TOKEN,
55 | host=config.PINTEREST_API_URI,
56 | )
57 | PinterestSDKClient.set_default_access_token(access_token=good_access_token)
58 | self.assertIsNotNone(Board.get_all())
59 |
60 | def test_bad_refresh_token(self):
61 | refresh_token = 'refresh_token'
62 | app_id = '12345'
63 | app_secret = '123456asdfg'
64 | with self.assertRaises(SdkException):
65 | PinterestSDKClient._get_access_token(
66 | refresh_token=refresh_token,
67 | app_id=app_id,
68 | app_secret=app_secret
69 | )
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/integration_tests/utils/organic_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Provide helper and utility functions for Organic Endpoints Integration Testing
3 | """
4 | import random
5 |
6 | from pinterest.client import PinterestSDKClient
7 |
8 | from pinterest.organic.boards import Board
9 | from pinterest.organic.pins import Pin
10 |
11 | from integration_tests.config import DEFAULT_BOARD_ID, DEFAULT_PIN_ID
12 |
13 |
14 | def _merge_default_params_with_params(default_params, params):
15 | if not params:
16 | return default_params
17 |
18 | for field, new_value in params.items():
19 | default_params[field] = new_value
20 |
21 | return default_params
22 |
23 |
24 | class BoardUtils:
25 | def __init__(self, client=None):
26 | self.test_client = client or PinterestSDKClient.create_default_client()
27 | self.board = Board(board_id=DEFAULT_BOARD_ID, client=client)
28 |
29 | def get_random_board_name(self):
30 | return "SDK Test Create Board {}".format(random.randint(0, 1000))
31 |
32 | def get_board(self):
33 | return self.board
34 |
35 | def get_board_id(self):
36 | return self.board.id
37 |
38 | def get_default_params(self):
39 | return dict(
40 | name="SDK Utils Test Board",
41 | description="SDK Test Board Description",
42 | privacy="PUBLIC",
43 | client=self.test_client
44 | )
45 |
46 | def create_new_board(self, **kwargs):
47 | return Board.create(**_merge_default_params_with_params(self.get_default_params(), kwargs))
48 |
49 | def delete_board(self, board_id):
50 | if board_id == DEFAULT_BOARD_ID:
51 | return
52 | return Board.delete(board_id=board_id, client=self.test_client)
53 |
54 |
55 | class PinUtils:
56 | def __init__(self, client=None):
57 | self.test_client = client or PinterestSDKClient.create_default_client()
58 | self.pin = Pin(pin_id=DEFAULT_PIN_ID, client=client)
59 |
60 | def get_pin(self):
61 | return self.pin
62 |
63 | def get_pin_id(self):
64 | return self.pin.id
65 |
66 | def get_default_params(self):
67 | return dict(
68 | board_id=DEFAULT_BOARD_ID,
69 | title="SDK Test Pin",
70 | description="SDK Test Pin Description",
71 | media_source={
72 | "source_type": "image_url",
73 | "content_type": "image/jpeg",
74 | "data": "string",
75 | 'url':'https://i.pinimg.com/564x/28/75/e9/2875e94f8055227e72d514b837adb271.jpg'
76 | },
77 | client=self.test_client
78 | )
79 |
80 | def create_new_pin(self, **kwargs):
81 | return Pin.create(**_merge_default_params_with_params(self.get_default_params(), kwargs))
82 |
83 | def delete_pin(self, pin_id):
84 | if pin_id == DEFAULT_PIN_ID:
85 | return
86 | return Pin.delete(pin_id=pin_id, client=self.test_client)
87 |
--------------------------------------------------------------------------------
/tests/src/pinterest/client/client_test.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import mock
3 | from unittest.mock import patch
4 | import unittest
5 | import subprocess
6 | from pinterest.utils.sdk_exceptions import SdkException
7 |
8 | from pinterest.client import PinterestSDKClient
9 |
10 |
11 | class ClientTest(unittest.TestCase):
12 |
13 | def test_set_default_access_token(self):
14 | os.unsetenv("PINTEREST_ACCESS_TOKEN")
15 | sample_access_token = 'test_token'
16 | PinterestSDKClient.set_default_access_token(sample_access_token)
17 | client = PinterestSDKClient.create_default_client()
18 | self.assertEqual(sample_access_token, client.configuration.access_token)
19 |
20 | @mock.patch.dict(
21 | os.environ,
22 | {
23 | "PINTEREST_REFRESH_ACCESS_TOKEN": "test_refresh_token",
24 | "PINTEREST_APP_ID": "test_app_id",
25 | "PINTEREST_APP_SECRET": "test_app_secret",
26 | },
27 | clear=True
28 | )
29 | @patch('dotenv.load_dotenv')
30 | def test_set_default_refresh_token(self, load_dotenv_mock):
31 | load_dotenv_mock.return_value = None
32 |
33 | refresh_token = 'refresh_token'
34 | app_id = '12345'
35 | app_secret = '123456asdfg'
36 |
37 | from pinterest.config import PINTEREST_REFRESH_ACCESS_TOKEN
38 | from pinterest.config import PINTEREST_APP_ID
39 | from pinterest.config import PINTEREST_APP_SECRET
40 | self.assertNotEqual(refresh_token, PINTEREST_REFRESH_ACCESS_TOKEN)
41 | self.assertNotEqual(app_id, PINTEREST_APP_ID)
42 | self.assertNotEqual(app_secret, PINTEREST_APP_SECRET)
43 |
44 | PinterestSDKClient.set_default_refresh_token(
45 | refresh_token=refresh_token,
46 | app_id=app_id,
47 | app_secret=app_secret
48 | )
49 |
50 | from pinterest.config import PINTEREST_REFRESH_ACCESS_TOKEN
51 | from pinterest.config import PINTEREST_APP_ID
52 | from pinterest.config import PINTEREST_APP_SECRET
53 | self.assertEqual(refresh_token, PINTEREST_REFRESH_ACCESS_TOKEN)
54 | self.assertEqual(app_id, PINTEREST_APP_ID)
55 | self.assertEqual(app_secret, PINTEREST_APP_SECRET)
56 |
57 | @mock.patch.dict(
58 | os.environ,
59 | {
60 | "PINTEREST_APP_ID": "test_app_id",
61 | "PINTEREST_APP_SECRET": "test_app_secret",
62 | },
63 | clear=True
64 | )
65 | @patch('dotenv.load_dotenv')
66 | def test_set_bad_refresh_token(self, load_dotenv_mock):
67 | load_dotenv_mock.return_value = None
68 | refresh_token = 'refresh_token'
69 | app_id = '12345'
70 | app_secret = '123456asdfg'
71 |
72 | from pinterest.config import PINTEREST_REFRESH_ACCESS_TOKEN
73 | from pinterest.config import PINTEREST_APP_ID
74 | from pinterest.config import PINTEREST_APP_SECRET
75 | self.assertNotEqual(refresh_token, PINTEREST_REFRESH_ACCESS_TOKEN)
76 | self.assertNotEqual(app_id, PINTEREST_APP_ID)
77 | self.assertNotEqual(app_secret, PINTEREST_APP_SECRET)
78 | with self.assertRaises(SdkException):
79 | PinterestSDKClient._get_access_token(
80 | refresh_token=refresh_token,
81 | app_id=app_id,
82 | app_secret=app_secret
83 | )
84 |
85 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_conversion_events.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Conversion Model
3 | """
4 | import os as _os
5 | from datetime import datetime
6 |
7 | from integration_tests.base_test import BaseTestCase
8 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
9 | from openapi_generated.pinterest_client import ApiException
10 |
11 | from pinterest.client import PinterestSDKClient
12 | from pinterest.ads.conversion_events import Conversion
13 |
14 | _get_event_time = lambda: int(datetime.now().timestamp())
15 |
16 | class TestSendConversionEvent(BaseTestCase):
17 | """
18 | Test send Conversion Event
19 | """
20 |
21 | def test_send_conversion_success(self):
22 | """
23 | Test send ConversionEvent successfully
24 | """
25 | client = PinterestSDKClient.create_client_with_token(_os.environ.get('CONVERSION_ACCESS_TOKEN'))
26 |
27 | NUMBER_OF_CONVERSION_EVENTS = 2
28 | raw_user_data = dict(
29 | em = ["f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a"] #random hash256
30 | )
31 | raw_custom_data = dict()
32 |
33 | conversion_events = [
34 | Conversion.create_conversion_event(
35 | event_name = "add_to_cart",
36 | action_source = "app_ios",
37 | event_time = _get_event_time(),
38 | event_id = "eventId0001",
39 | user_data = raw_user_data,
40 | custom_data= raw_custom_data,
41 | )
42 | for _ in range(NUMBER_OF_CONVERSION_EVENTS)
43 | ]
44 |
45 | response = Conversion.send_conversion_events(
46 | client = client,
47 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
48 | conversion_events = conversion_events,
49 | test = True,
50 | )
51 |
52 | assert response
53 | assert response.num_events_received == 2
54 | assert response.num_events_processed == 2
55 | assert len(response.events) == 2
56 |
57 | assert response.events[0].status == "processed"
58 | assert response.events[0].error_message == ""
59 |
60 | assert response.events[1].status == "processed"
61 | assert response.events[1].error_message == ""
62 |
63 | def test_send_conversion_fail(self):
64 | """
65 | Test send ConversionEvent fail with non-hashed email
66 | """
67 | client = PinterestSDKClient.create_client_with_token(_os.environ.get('CONVERSION_ACCESS_TOKEN'))
68 |
69 | NUMBER_OF_CONVERSION_EVENTS = 2
70 | raw_user_data = dict(
71 | em = ["test_non_hashed_email@pinterest.com"]
72 | )
73 | raw_custom_data = dict()
74 |
75 | conversion_events = [
76 | Conversion.create_conversion_event(
77 | event_name = "add_to_cart",
78 | action_source = "app_ios",
79 | event_time = _get_event_time(),
80 | event_id = "eventId0001",
81 | user_data = raw_user_data,
82 | custom_data = raw_custom_data,
83 | )
84 | for _ in range(NUMBER_OF_CONVERSION_EVENTS)
85 | ]
86 |
87 | try:
88 | Conversion.send_conversion_events(
89 | client = client,
90 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
91 | conversion_events = conversion_events,
92 | test = True,
93 | )
94 | except ApiException as e:
95 | assert e.status == 422
96 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_audiences.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Audiences Model
3 | """
4 |
5 | from unittest.mock import patch
6 |
7 | from pinterest.ads.audiences import Audience
8 |
9 | from integration_tests.base_test import BaseTestCase
10 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
11 |
12 |
13 | class TestAudience(BaseTestCase):
14 | """
15 | Test Audience model and its higher level functions
16 | """
17 |
18 | def test_create_audience(self):
19 | """
20 | Test creating a new audience
21 | """
22 | audience = Audience.create(
23 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
24 | name="SDK Test Audience",
25 | rule=dict(
26 | engager_type=1
27 | ),
28 | audience_type="ENGAGEMENT",
29 | description="SDK Test Audience Description",
30 | client=self.test_client
31 | )
32 |
33 | assert audience
34 | assert getattr(audience, '_id')
35 | assert getattr(audience, '_name') == "SDK Test Audience"
36 | assert getattr(audience, '_created_timestamp')
37 |
38 | def test_get_existing_audience(self):
39 | """
40 | Test if a Audience model/object is created successfully with correct audience_id
41 | """
42 | audience = Audience(
43 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
44 | audience_id=self.audience_utils.get_audience_id(),
45 | client=self.test_client
46 | )
47 | assert audience
48 | assert getattr(audience, '_id') == self.audience_utils.get_audience_id()
49 |
50 | def test_update_audience_with_kwargs(self):
51 | """
52 | Test if a given audience is updated successfully with passed in keyword arguments
53 | """
54 | new_name = "SDK Test Audience UPDATED"
55 |
56 | audience_response = Audience(
57 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
58 | audience_id=self.audience_utils.get_audience_id(),
59 | client=self.test_client
60 | )
61 | update_response = audience_response.update_fields(
62 | name=new_name,
63 | description="SDK Test Audience Description UPDATED",
64 | )
65 |
66 | assert update_response
67 | assert getattr(audience_response, '_name') == "SDK Test Audience UPDATED"
68 | assert getattr(audience_response, '_description') == "SDK Test Audience Description UPDATED"
69 |
70 |
71 | class TestGetAllAudiences(BaseTestCase):
72 | """
73 | Test Get All Audiences class method
74 | """
75 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
76 | def test_get_all_audiences(self, audiences_get_mock):
77 | """
78 | Test if all audiences are returned for a given Ad Account ID
79 | """
80 | NUMBER_OF_AUDIENCES_TO_CREATE = 3
81 | created_audience_ids = set(
82 | getattr(self.audience_utils.create_new_audience(), '_id') for _ in range(NUMBER_OF_AUDIENCES_TO_CREATE)
83 | )
84 |
85 | assert len(created_audience_ids) == NUMBER_OF_AUDIENCES_TO_CREATE
86 |
87 | audiences_list, _ = Audience.get_all(
88 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
89 | order="DESCENDING",
90 | page_size=NUMBER_OF_AUDIENCES_TO_CREATE,
91 | )
92 |
93 | assert audiences_get_mock.call_count - 1 == NUMBER_OF_AUDIENCES_TO_CREATE
94 | assert len(created_audience_ids) == len(audiences_list)
95 |
96 | get_all_audiences_ids = set()
97 | for audience in audiences_list:
98 | get_all_audiences_ids.add(getattr(audience, '_id'))
99 | assert isinstance(audience, Audience)
100 |
101 | assert created_audience_ids == get_all_audiences_ids
102 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_keywords.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Keyword Model
3 | """
4 |
5 |
6 | from integration_tests.base_test import BaseTestCase
7 |
8 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
9 |
10 | from pinterest.ads.keywords import Keyword
11 | from pinterest.utils.sdk_exceptions import SdkException
12 |
13 | from openapi_generated.pinterest_client.model.match_type_response import MatchTypeResponse
14 |
15 |
16 | class TestCreateKeyword(BaseTestCase):
17 | """
18 | Test Keyword create
19 | """
20 | def test_create_keyword_success(self):
21 | """
22 | Test creating a new keyword
23 | """
24 | keyword = Keyword.create(
25 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
26 | parent_id=self.ad_group_utils.get_ad_group_id(),
27 | value="string",
28 | match_type="BROAD",
29 | bid=1000,
30 | )
31 |
32 | assert keyword
33 | assert getattr(keyword, '_id')
34 | assert getattr(keyword, '_parent_id') == self.ad_group_utils.get_ad_group_id()
35 | assert getattr(keyword, '_match_type') == MatchTypeResponse("BROAD")
36 |
37 | def test_create_fail_without_matchtype(self):
38 | """
39 | Test creating a new keyword
40 | """
41 | keyword_arguments = dict(
42 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
43 | parent_id=self.ad_group_utils.get_ad_group_id(),
44 | value="string",
45 | )
46 |
47 | with self.assertRaises(SdkException):
48 | Keyword.create(**keyword_arguments)
49 |
50 |
51 | class TestGetKeywords(BaseTestCase):
52 | """
53 | Test get keywords
54 | """
55 | def test_get_keywords_success(self):
56 | """
57 | Test get keywords success
58 | """
59 | keyword1 = Keyword.create(
60 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
61 | parent_id=self.ad_group_utils.get_ad_group_id(),
62 | value="Keyword_SDK_1",
63 | match_type="BROAD",
64 | bid=1000)
65 | keyword2 = Keyword.create(
66 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
67 | parent_id=self.ad_group_utils.get_ad_group_id(),
68 | value="Keyword_SDK_2",
69 | match_type="BROAD",
70 | bid=1000)
71 | keywords = set([getattr(keyword1, "_id"), getattr(keyword2, "_id")])
72 |
73 | keywords_from_get, _ = Keyword.get_all(
74 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
75 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
76 | page_size=25,
77 | )
78 |
79 | assert len(keywords) == len(keywords_from_get)
80 | for kw in keywords_from_get:
81 | assert getattr(kw, "_id") in keywords
82 |
83 |
84 | class TestUpdateKeyword(BaseTestCase):
85 | """
86 | Test get keywords
87 | """
88 | def test_update_keyword_success(self):
89 | """
90 | Test update Keyword success
91 | """
92 | Keyword.create(
93 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
94 | parent_id=self.ad_group_utils.get_ad_group_id(),
95 | value="Keyword_SDK_1",
96 | match_type="BROAD",
97 | bid=1000)
98 |
99 | keywords, _ = Keyword.get_all(
100 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
101 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
102 | page_size=25,
103 | )
104 | keyword = keywords[0]
105 |
106 | new_archived = not getattr(keyword, "_archived")
107 | keyword.update_fields(
108 | archived=new_archived
109 | )
110 |
111 | assert keyword
112 | assert getattr(keyword, "_archived") == new_archived
113 |
--------------------------------------------------------------------------------
/integration_tests/test_pinterest_base_model.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Pinterest Base Model
3 | """
4 | from parameterized import parameterized
5 |
6 | from pinterest.ads.campaigns import Campaign
7 | from pinterest.organic.boards import Board
8 |
9 | from integration_tests.base_test import BaseTestCase
10 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
11 | from integration_tests.config import DEFAULT_BOARD_ID
12 |
13 | class TestPinterestBaseModel(BaseTestCase):
14 | def test_error_message_for_accessing_non_existant_attribute(self):
15 | """
16 | Test getting a Campaign and accessing an attribute that does not exist
17 | """
18 | existing_campaign_id = self.campaign_utils.get_campaign_id()
19 | campaign = Campaign(
20 | client=self.test_client,
21 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
22 | campaign_id=existing_campaign_id,
23 | )
24 |
25 | assert campaign
26 | with self.assertRaises(AttributeError):
27 | campaign.non_existing_property
28 |
29 | def test_equality_of_campaign_models(self):
30 | """
31 | Test if two campaign model instances of same campaign id are equal to each other
32 | """
33 | existing_campaign_id = self.campaign_utils.get_campaign_id()
34 | campaign_v1_0 = Campaign(
35 | client=self.test_client,
36 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
37 | campaign_id=existing_campaign_id,
38 | )
39 | campaign_v1_1 = Campaign(
40 | client=self.test_client,
41 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
42 | campaign_id=existing_campaign_id,
43 | )
44 | campaign_v2_0 = self.campaign_utils.create_new_campaign()
45 |
46 | assert campaign_v1_0 == campaign_v1_1
47 | assert campaign_v1_1 != campaign_v2_0
48 |
49 | def test_set_board_attributes_failure(self):
50 | """
51 | Test setting a Board attribute failure
52 | """
53 | board = Board(
54 | board_id=DEFAULT_BOARD_ID,
55 | client=self.test_client,
56 | )
57 |
58 | assert board
59 | with self.assertRaises(AttributeError):
60 | board.name = "FAILURE_BOARD_NAME"
61 |
62 | class TestModelAttributes(BaseTestCase):
63 | @parameterized.expand(
64 | [
65 | # Ads Models
66 | ("ad_account",),
67 | ("campaign"),
68 | ("ad_group"),
69 | ("ad"),
70 | ("customer_list"),
71 | ("audience"),
72 | ]
73 | )
74 | def test_ads_model_attributes_match_properties(self, model_name):
75 | model_creation_util = f"{model_name}_utils"
76 | model_creation_util_fn = f"create_new_{model_name}"
77 | model = getattr(getattr(self, model_creation_util), model_creation_util_fn)()
78 | api_spec_attribute_set = set(model._model_attribute_types.keys())
79 | sdk_model_property_set = set(model._property_dict.keys())
80 |
81 | assert api_spec_attribute_set == sdk_model_property_set
82 |
83 | @parameterized.expand(
84 | [
85 | # Organic Models
86 | ("board"),
87 | ("pin"),
88 | ]
89 | )
90 | def test_organic_model_attributes_match_properties(self, model_name):
91 | model_creation_util = f"{model_name}_utils"
92 | model_creation_util_fn = f"create_new_{model_name}"
93 | model_deletion_util_fn = f"delete_{model_name}"
94 |
95 | model = getattr(getattr(self, model_creation_util), model_creation_util_fn)()
96 |
97 | getattr(getattr(self, model_creation_util), model_deletion_util_fn)(model.id)
98 |
99 | api_spec_attribute_set = set(model._model_attribute_types.keys())
100 | sdk_model_property_set = set(model._property_dict.keys())
101 |
102 | assert api_spec_attribute_set == sdk_model_property_set
103 |
--------------------------------------------------------------------------------
/integration_tests/base_test.py:
--------------------------------------------------------------------------------
1 | """
2 | Base test case for centralized the common code between different integration tests
3 | """
4 |
5 | from unittest import TestCase
6 | from pinterest.client import PinterestSDKClient
7 |
8 | from integration_tests.utils.ads_utils import AdAccountUtils
9 | from integration_tests.utils.ads_utils import CampaignUtils
10 | from integration_tests.utils.ads_utils import AudienceUtils
11 | from integration_tests.utils.ads_utils import CustomerListUtils
12 | from integration_tests.utils.ads_utils import AdGroupUtils
13 | from integration_tests.utils.ads_utils import ConversionTagUtils
14 | from integration_tests.utils.ads_utils import AdUtils
15 |
16 | from integration_tests.utils.organic_utils import BoardUtils
17 | from integration_tests.utils.organic_utils import PinUtils
18 |
19 |
20 | class BaseTestCase(TestCase):
21 | """
22 | Base test case for centralized the common code between different integration tests
23 | """
24 | # pylint: disable=too-many-instance-attributes
25 | def __init__(self, *args, **kwargs):
26 | super().__init__(*args, **kwargs)
27 | self._test_client = None
28 | self._ad_account_utils = None
29 | self._campaign_utils = None
30 | self._customer_list_utils = None
31 | self._audience_utils = None
32 | self._ad_group_utils = None
33 | self._ads_utils = None
34 | self._conversion_utils = None
35 | self._ad_utils = None
36 | self._test_customer_list_utils = None
37 | self._board_utils = None
38 | self._pin_utils = None
39 |
40 | @property
41 | def test_client(self):
42 | """Default pinterest sdk client"""
43 | if not self._test_client:
44 | self._test_client = PinterestSDKClient.create_default_client()
45 | return self._test_client
46 |
47 | @property
48 | def ad_account_utils(self):
49 | """Ad account util object"""
50 | if not self._ad_account_utils:
51 | self._ad_account_utils = AdAccountUtils()
52 | return self._ad_account_utils
53 |
54 | @property
55 | def campaign_utils(self):
56 | """Campaign utils util object"""
57 | if not self._campaign_utils:
58 | self._campaign_utils = CampaignUtils()
59 | return self._campaign_utils
60 |
61 | @property
62 | def customer_list_utils(self):
63 | """Customer list utils util object"""
64 | if not self._customer_list_utils:
65 | self._customer_list_utils = CustomerListUtils()
66 | return self._customer_list_utils
67 |
68 | @property
69 | def audience_utils(self):
70 | """Audience utils util object"""
71 | if not self._audience_utils:
72 | self._audience_utils = AudienceUtils()
73 | return self._audience_utils
74 |
75 | @property
76 | def ad_group_utils(self):
77 | """Ad group utils util object"""
78 | if not self._ad_group_utils:
79 | self._ad_group_utils = AdGroupUtils()
80 | return self._ad_group_utils
81 |
82 | @property
83 | def ad_utils(self):
84 | """Ads utils util object"""
85 | if not self._ad_utils:
86 | self._ad_utils = AdUtils()
87 | return self._ad_utils
88 |
89 | @property
90 | def conversion_tag_utils(self):
91 | """Conversion Tag util object"""
92 | if not self._conversion_utils:
93 | self._conversion_utils = ConversionTagUtils()
94 | return self._conversion_utils
95 |
96 | @property
97 | def board_utils(self):
98 | """Board utils util object"""
99 | if not self._board_utils:
100 | self._board_utils = BoardUtils()
101 | return self._board_utils
102 |
103 | @property
104 | def pin_utils(self):
105 | """Pin utils util object"""
106 | if not self._pin_utils:
107 | self._pin_utils = PinUtils()
108 | return self._pin_utils
109 |
--------------------------------------------------------------------------------
/integration_tests/organic/test_pins.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Pin Model
3 |
4 | *NOTE*: Do not forget to delete pin after the test.
5 | """
6 | from parameterized import parameterized
7 |
8 | from openapi_generated.pinterest_client.exceptions import NotFoundException
9 |
10 | from pinterest.organic.pins import Pin
11 |
12 | from integration_tests.base_test import BaseTestCase
13 | from integration_tests.config import DEFAULT_PIN_ID
14 | from integration_tests.config import DEFAULT_BOARD_ID
15 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
16 | from integration_tests.config import DEFAULT_BOARD_SECTION_ID
17 |
18 | class TestGetPin(BaseTestCase):
19 | """
20 | Test class for getting Pin
21 | """
22 | @parameterized.expand(
23 | [
24 | ({
25 | 'description': 'PIN is PUBLIC',
26 | 'pin_id': DEFAULT_PIN_ID,
27 | 'ad_account_id': DEFAULT_AD_ACCOUNT_ID,
28 | },),
29 | ({
30 | 'description': 'PIN in PROTECTED BOARD',
31 | 'pin_id': DEFAULT_PIN_ID,
32 | 'ad_account_id': DEFAULT_AD_ACCOUNT_ID,
33 | },)
34 | ]
35 | )
36 | def test_get_public_and_private_pin_success(self, pin_info_dict):
37 | """
38 | Test getting and initializing a PUBLIC pin using pin_id
39 | and
40 | Test getting and initializing a pin inside a PROTECTED board using pin_id and ad_account_id
41 | """
42 | pin = Pin(pin_id=pin_info_dict.get('pin_id'), ad_acount_id=pin_info_dict.get('ad_account_id'))
43 |
44 | assert pin
45 | assert pin.id == pin_info_dict.get('pin_id')
46 |
47 |
48 | class TestCreateAndDeletePin(BaseTestCase):
49 | """
50 | Test creating and deleting Pin Model
51 | """
52 | def test_create_and_delete_pin_success(self):
53 | """
54 | Test creating a new Pin and deleting the Pin successfully
55 | """
56 | pin = Pin.create(
57 | board_id=DEFAULT_BOARD_ID,
58 | title="SDK Test Pin",
59 | description="SDK Test Pin Description",
60 | media_source={
61 | "source_type": "image_url",
62 | "content_type": "image/jpeg",
63 | "data": "string",
64 | 'url':'https://i.pinimg.com/564x/28/75/e9/2875e94f8055227e72d514b837adb271.jpg'
65 | },
66 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
67 | client=self.test_client
68 | )
69 |
70 | assert pin
71 | assert pin.title == "SDK Test Pin"
72 |
73 | self.pin_utils.delete_pin(pin_id=pin.id)
74 |
75 | with self.assertRaises(NotFoundException):
76 | Pin(
77 | pin_id=pin.id,
78 | client=self.test_client
79 | )
80 |
81 |
82 | class TestSavePin(BaseTestCase):
83 | """
84 | Test saving a pin
85 | """
86 | @parameterized.expand(
87 | [
88 | ({
89 | # Save to board
90 | 'board_id': DEFAULT_BOARD_ID,
91 | },),
92 | ({
93 | # Save to board section
94 | 'board_id': DEFAULT_BOARD_ID,
95 | 'board_section_id': DEFAULT_BOARD_SECTION_ID,
96 | },)
97 | ]
98 | )
99 | def test_save_pin_success(self, pin_save_kwargs):
100 | """
101 | Test saving pin with default args, board_id and board_section_id
102 | """
103 | pin = self.pin_utils.create_new_pin(title="Test Saving Pin")
104 | assert pin
105 |
106 | pin.save(**pin_save_kwargs)
107 |
108 | if pin_save_kwargs.get('board_id'):
109 | assert pin.board_id == pin_save_kwargs.get('board_id')
110 |
111 | if pin_save_kwargs.get('board_section_id'):
112 | assert pin.board_section_id == pin_save_kwargs.get('board_section_id')
113 |
114 | self.pin_utils.delete_pin(pin_id=pin.id)
115 | with self.assertRaises(NotFoundException):
116 | Pin(
117 | pin_id=pin.id,
118 | client=self.test_client
119 | )
120 |
--------------------------------------------------------------------------------
/docs/pinterest/client.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `client`
6 | Pinterest Client
7 |
8 | **Global Variables**
9 | ---------------
10 | - **default_sdk_client**
11 |
12 |
13 | ---
14 |
15 |
16 |
17 | ## class `PinterestSDKClient`
18 | Wrapper API client for SDK high level models
19 |
20 | NOTE: This class is base in a generated by OpenAPI Generator. Ref: https://openapi-generator.tech
21 |
22 |
23 |
24 | ### method `__init__`
25 |
26 | ```python
27 | __init__(
28 | access_token=None,
29 | refresh_token=None,
30 | app_id=None,
31 | app_secret=None,
32 | configuration=None,
33 | header_name=None,
34 | header_value=None,
35 | cookie=None,
36 | pool_threads=1
37 | )
38 | ```
39 |
40 |
41 |
42 |
43 |
44 |
45 | ---
46 |
47 | #### property pool
48 |
49 | Create thread pool on first request avoids instantiating unused threadpool for blocking clients.
50 |
51 | ---
52 |
53 | #### property user_agent
54 |
55 | User agent for this API client
56 |
57 |
58 |
59 | ---
60 |
61 |
62 |
63 | ### classmethod `create_client_with_refresh_token`
64 |
65 | ```python
66 | create_client_with_refresh_token(
67 | refresh_token: str,
68 | app_id: str,
69 | app_secret: str
70 | )
71 | ```
72 |
73 | Get a new SDK client with the given refresh token, app id and app secret.
74 |
75 | ---
76 |
77 |
78 |
79 | ### classmethod `create_client_with_token`
80 |
81 | ```python
82 | create_client_with_token(access_token: str)
83 | ```
84 |
85 | Get a new SDK client with the given access token.
86 |
87 | ---
88 |
89 |
90 |
91 | ### classmethod `create_default_client`
92 |
93 | ```python
94 | create_default_client()
95 | ```
96 |
97 | Returns the default SDK client.
98 |
99 | If client is not explicitly initialized, a new client will be initialized from environment variables.
100 |
101 | ---
102 |
103 |
104 |
105 | ### classmethod `set_default_access_token`
106 |
107 | ```python
108 | set_default_access_token(access_token: str)
109 | ```
110 |
111 | Replace the default access_token with the given ones.
112 |
113 | ---
114 |
115 |
116 |
117 | ### classmethod `set_default_client`
118 |
119 | ```python
120 | set_default_client(client)
121 | ```
122 |
123 | Replace the default client with the given client.
124 |
125 | ---
126 |
127 |
128 |
129 | ### classmethod `set_default_refresh_token`
130 |
131 | ```python
132 | set_default_refresh_token(refresh_token: str, app_id: str, app_secret: str)
133 | ```
134 |
135 | Replace the default refresh_token, app_id, app_secret with the given ones.
136 |
137 |
138 |
--------------------------------------------------------------------------------
/docs/pinterest/ads.conversion_events.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `ads.conversion_events`
6 | Conversion Event Class for Pinterest Python SDK
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `Conversion`
15 | Conversion Event Model used to send conversion events to Pinterest API
16 |
17 |
18 |
19 |
20 | ---
21 |
22 |
23 |
24 | ### classmethod `create_conversion_event`
25 |
26 | ```python
27 | create_conversion_event(
28 | event_name: 'str',
29 | action_source: 'str',
30 | event_time: 'int',
31 | event_id: 'str',
32 | user_data: 'dict',
33 | custom_data: 'dict',
34 | event_source_url: 'str' = None,
35 | partner_name: 'str' = None,
36 | app_id: 'str' = None,
37 | app_name: 'str' = None,
38 | app_version: 'str' = None,
39 | device_brand: 'str' = None,
40 | device_carrier: 'str' = None,
41 | device_model: 'str' = None,
42 | device_type: 'str' = None,
43 | os_version: 'str' = None,
44 | language: 'str' = None,
45 | **kwargs
46 | ) → ConversionEventsData
47 | ```
48 |
49 | Create Conversion Event Data to be sent.
50 |
51 |
52 |
53 | **Args:**
54 |
55 | - `event_name` (str): The type of the user event, Enum: "add_to_cart", "checkout", "custom", "lead", "page_visit", "search", "signup", "view_category", "watch_video"
56 | - `action_source` (str): The source indicating where the conversion event occurred, Enum: "app_adroid", "app_ios", "web", "offline"
57 | - `event_time` (int): The time when the event happened. Unix timestamp in seconds
58 | - `event_id` (str): The unique id string that identifies this event and can be used for deduping between events ingested via both the conversion API and Pinterest tracking
59 | - `user_data` (dict): Object containing customer information data. Note, it is required at least one of 1) em, 2) hashed_maids or 3) pair client_ip_address + client_user_agent.
60 | - `custom_data` (dict): Object containing other custom data.
61 | - `event_source_url` (str, optional): URL of the web conversion event
62 | - `partner_name` (str, optional): The third party partner name responsible to send the event to Conversion API on behalf of the adverstiser. Only send this field if Pinterest has worked directly with you to define a value for partner_name.
63 | - `app_id` (str, optional): The app store app ID.
64 | - `app_name` (str, optional): Name of the app.
65 | - `app_version` (str, optional): Version of the app.
66 | - `device_brand` (str, optional): Brand of the user device.
67 | - `device_carrier` (str, optional): User device's model carrier.
68 | - `device_model` (str, optional): Model of the user device.
69 | - `device_type` (str, optional): Type of the user device.
70 | - `os_version` (str, optional): Version of the device operating system.
71 | - `language` (str, optional): Two-character ISO-639-1 language code indicating the user's language.
72 |
73 |
74 |
75 | **Returns:**
76 |
77 | - `ConversionEventsData`: ConversionEventData to be sent
78 |
79 | ---
80 |
81 |
82 |
83 | ### classmethod `send_conversion_events`
84 |
85 | ```python
86 | send_conversion_events(
87 | ad_account_id: 'str',
88 | conversion_events: 'list[ConversionEventsData]',
89 | test: 'bool' = False,
90 | client: 'PinterestSDKClient' = None,
91 | **kwargs
92 | ) → tuple(int, int, list[ConversionApiResponseEvents])
93 | ```
94 |
95 | Send conversion events to Pinterest API for Conversions.
96 |
97 | Note: Highly recommend to use create_client_with_token (with Conversion Access Token) to create different client for this functionality.
98 |
99 |
100 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_conversion_tags.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Conversion Tag Model
3 | """
4 |
5 | from integration_tests.base_test import BaseTestCase
6 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
7 |
8 | from pinterest.ads.conversion_tags import ConversionTag
9 |
10 | class TestCreateConversionTag(BaseTestCase):
11 | """
12 | Test creating Conversion Tag
13 | """
14 |
15 | def test_create_conversion_tag_success(self):
16 | """
17 | Test creating a new Conversion Tag successfully
18 | """
19 | conversion_tag = ConversionTag.create(
20 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
21 | name = "Test Conversion Tag",
22 | aem_enabled = None,
23 | md_frequency = None,
24 | aem_fnln_enabled = None,
25 | aem_ph_enabled = None,
26 | aem_ge_enabled = None,
27 | aem_db_enabled = None,
28 | aem_loc_enabled = None,
29 | )
30 |
31 | assert conversion_tag
32 | assert getattr(conversion_tag, "_id")
33 | assert getattr(conversion_tag, "_name") == "Test Conversion Tag"
34 | assert getattr(conversion_tag.configs, "aem_enabled") == False
35 |
36 | def test_create_conversion_tag_with_configs_success(self):
37 | """
38 | Test creating a new Conversion Tag successfully
39 | """
40 | conversion_tag = ConversionTag.create(
41 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
42 | name = "Test Conversion Tag",
43 | aem_enabled = True,
44 | md_frequency = 1.2,
45 | aem_fnln_enabled = None,
46 | aem_ph_enabled = None,
47 | aem_ge_enabled = None,
48 | aem_db_enabled = None,
49 | aem_loc_enabled = None,
50 | )
51 |
52 | assert conversion_tag
53 | assert getattr(conversion_tag, "_id")
54 | assert getattr(conversion_tag, "_name") == "Test Conversion Tag"
55 | assert getattr(conversion_tag.configs, "aem_enabled") == True
56 | assert getattr(conversion_tag.configs, "md_frequency") == 1.2
57 |
58 | class TestGetConversionTag(BaseTestCase):
59 | """
60 | Test get Conversion Tag
61 | """
62 |
63 | def test_get_conversion_tag_success(self):
64 | """
65 | Test get conversion tag from existing conversion tag
66 | """
67 | exiting_conversion_tag_id = self.conversion_tag_utils.get_conversion_tag_id()
68 | conversion_tag = ConversionTag(
69 | client = self.test_client,
70 | ad_account_id = DEFAULT_AD_ACCOUNT_ID,
71 | conversion_tag_id = exiting_conversion_tag_id,
72 | )
73 |
74 | assert conversion_tag
75 | assert getattr(conversion_tag, "_id")
76 | assert getattr(conversion_tag, "_name") == getattr(self.conversion_tag_utils.get_conversion_tag(), "_name")
77 |
78 | class TestGetListConversionTag(BaseTestCase):
79 | """
80 | Test get list of ConversionTags
81 | """
82 | def test_get_list_success(self):
83 | """
84 | Test get list successfully
85 | """
86 | # Create new account so the integration test does not get slow as number of conversion
87 | # tags increasing while testing
88 | ad_account_id = getattr(self.ad_account_utils.create_new_ad_account(), "_id")
89 |
90 | NUMBER_OF_NEW_CONVERSION_TAG = 3
91 | for _ in range(NUMBER_OF_NEW_CONVERSION_TAG):
92 | self.conversion_tag_utils.create_new_conversion_tag(
93 | name = "SDK_TEST_CONVERSION_TAG",
94 | ad_account_id = ad_account_id,
95 | )
96 |
97 | conversion_tags = ConversionTag.get_all(ad_account_id = ad_account_id)
98 |
99 | assert len(conversion_tags) == NUMBER_OF_NEW_CONVERSION_TAG
100 | for conversion_tag in conversion_tags:
101 | assert conversion_tag.name == "SDK_TEST_CONVERSION_TAG"
102 | assert conversion_tag.ad_account_id == ad_account_id
103 |
104 |
105 | class TestGetPageVsitConversionTag(BaseTestCase):
106 | """
107 | Test get page visit conversion tag events
108 | """
109 | def test_get_page_visit_success(self):
110 | """
111 | Test get page visit converion tag events for an Ad Account
112 | """
113 | conversion_tag_events, bookmark = ConversionTag.get_page_visit_conversion_tag_events(
114 | ad_account_id = DEFAULT_AD_ACCOUNT_ID
115 | )
116 |
117 | assert not conversion_tag_events
118 | assert not bookmark
119 |
120 | class TestGetOcpmEligibleConversionTag(BaseTestCase):
121 | """
122 | Test get ocpm eligible conversion tag events
123 | """
124 | def test_get_ocpm_eligible_conversion_tags(self):
125 | """
126 | Test get ocpm eligible conversion tag events for an Ad Account
127 | """
128 | property, conversion_tag_events = ConversionTag.get_ocpm_eligible_conversion_tag_events(
129 | ad_account_id = DEFAULT_AD_ACCOUNT_ID
130 | )
131 |
132 | assert not property
133 | assert not conversion_tag_events
134 |
--------------------------------------------------------------------------------
/docs/pinterest/ads.keywords.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `ads.keywords`
6 | High level module class for Keyword object
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `Keyword`
15 | High level model class to manage keywords
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(ad_account_id, keyword_id, client=None, **kwargs)
23 | ```
24 |
25 |
26 |
27 |
28 |
29 |
30 | ---
31 |
32 | #### property archived
33 |
34 |
35 |
36 |
37 |
38 | ---
39 |
40 | #### property bid
41 |
42 |
43 |
44 |
45 |
46 | ---
47 |
48 | #### property id
49 |
50 |
51 |
52 |
53 |
54 | ---
55 |
56 | #### property match_type
57 |
58 |
59 |
60 |
61 |
62 | ---
63 |
64 | #### property parent_id
65 |
66 |
67 |
68 |
69 |
70 | ---
71 |
72 | #### property parent_type
73 |
74 |
75 |
76 |
77 |
78 | ---
79 |
80 | #### property type
81 |
82 |
83 |
84 |
85 |
86 | ---
87 |
88 | #### property value
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | ---
97 |
98 |
99 |
100 | ### classmethod `create`
101 |
102 | ```python
103 | create(
104 | ad_account_id: 'str',
105 | parent_id: 'str',
106 | value: 'str',
107 | bid: 'int' = None,
108 | match_type: 'str' = None,
109 | client: 'PinterestSDKClient' = None,
110 | **kwargs
111 | ) → Keyword
112 | ```
113 |
114 | Create keywords for follow entity types (advertiser, campaign, ad group or ad).
115 |
116 |
117 |
118 | **NOTE:**
119 |
120 | > - Advertisers campaigns can only be assigned keywords with excluding ('_NEGATIVE'). - All keyword match types are available for ad groups.
121 | >
122 |
123 | **Args:**
124 |
125 | - `ad_account_id` (str): Ad Account ID
126 | - `parent_id` (str): Keyword parent entity ID (advertiser, campaign, ad group)
127 | - `bid` (float): Keyword custom bid
128 | - `match_type` (str): Keyword match type, ENUM: "BOARD", "PHRASE", "EXACT", "EXACT_NEGATIVE", "PHRASE_NEGATIVE", null
129 | - `value` (str): Keyword value(120 chars max)
130 |
131 |
132 |
133 | **Returns:**
134 |
135 | - `Keyword`: Keyword Object
136 |
137 | ---
138 |
139 |
140 |
141 | ### classmethod `get_all`
142 |
143 | ```python
144 | get_all(
145 | ad_account_id: 'str',
146 | page_size: 'int' = None,
147 | bookmark: 'str' = None,
148 | client: 'PinterestSDKClient' = None,
149 | **kwargs
150 | ) → tuple[list[Keyword], str]
151 | ```
152 |
153 | Get a list of keywords bases on the filters provided.
154 |
155 |
156 |
157 | **NOTE:**
158 |
159 | > - Advertisers campaigns can only be assigned keywords with excluding ('_NEGATIVE'). - All keyword match types are available for ad groups.
160 | >
161 |
162 | **Args:**
163 |
164 | - `ad_account_id` (str): Ad Account ID.
165 | - `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 all campaigns.
166 | - `bookmark` (str, optional): Cursor used to fetch the next page of items. Defaults to None.
167 | - `client` (PinterestSDKClient, optional): Defaults to the default api client.
168 |
169 |
170 |
171 | **Returns:**
172 |
173 | - `list[Keyword]`: List of Keyword Objects
174 | - `str`: Bookmark for paginations if present, else None.
175 |
176 | ---
177 |
178 |
179 |
180 | ### method `update_fields`
181 |
182 | ```python
183 | update_fields(**kwargs) → bool
184 | ```
185 |
186 | Update keyword fields using any attributes
187 |
188 | Keyword Args: Any valid keyword fields with valid values
189 |
190 |
191 |
192 | **Returns:**
193 |
194 | - `bool`: if keyword fields were successfully updated
195 |
196 |
197 |
--------------------------------------------------------------------------------
/docs/pinterest/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # API Overview
4 |
5 | ## Modules
6 |
7 | - [`ads`](./ads.md#module-ads)
8 | - [`ads.ad_accounts`](./ads.ad_accounts.md#module-adsad_accounts): AdAccount Class for Pinterest Python SDK
9 | - [`ads.ad_groups`](./ads.ad_groups.md#module-adsad_groups): High level module class for AdGroup object
10 | - [`ads.ads`](./ads.ads.md#module-adsads): Ads high level model
11 | - [`ads.audiences`](./ads.audiences.md#module-adsaudiences): High level module class for Audience object
12 | - [`ads.campaigns`](./ads.campaigns.md#module-adscampaigns): Campaign Class for Pinterest Python SDK
13 | - [`ads.conversion_events`](./ads.conversion_events.md#module-adsconversion_events): Conversion Event Class for Pinterest Python SDK
14 | - [`ads.conversion_tags`](./ads.conversion_tags.md#module-adsconversion_tags): Conversion Class for Pinterest Python SDK
15 | - [`ads.customer_lists`](./ads.customer_lists.md#module-adscustomer_lists): High level module class for Customer List object
16 | - [`ads.keywords`](./ads.keywords.md#module-adskeywords): High level module class for Keyword object
17 | - [`client`](./client.md#module-client): Pinterest Client
18 | - [`organic`](./organic.md#module-organic)
19 | - [`organic.boards`](./organic.boards.md#module-organicboards): Board Class for Pinterest Python SDK
20 | - [`organic.pins`](./organic.pins.md#module-organicpins): Pin Class for Pinterest Python SDK
21 | - [`utils`](./utils.md#module-utils)
22 | - [`utils.base_model`](./utils.base_model.md#module-utilsbase_model): Pinterest Base Model
23 | - [`utils.bookmark`](./utils.bookmark.md#module-utilsbookmark): Bookmark Model
24 | - [`utils.error_handling`](./utils.error_handling.md#module-utilserror_handling): Util error handling function
25 | - [`utils.load_json_config`](./utils.load_json_config.md#module-utilsload_json_config): module with the function `load_json_config` that parse a config.json file and then load all the variables found as
26 | - [`utils.refresh_access_token`](./utils.refresh_access_token.md#module-utilsrefresh_access_token): This script has functions for generating a new ACCESSTOKEN using the REFRESHTOKEN
27 | - [`utils.sdk_exceptions`](./utils.sdk_exceptions.md#module-utilssdk_exceptions): SDK Exceptions for error handling in the models.
28 |
29 | ## Classes
30 |
31 | - [`ad_accounts.AdAccount`](./ads.ad_accounts.md#class-adaccount): Ad Account model used to create, update its attributes and list its different entities.
32 | - [`ad_groups.AdGroup`](./ads.ad_groups.md#class-adgroup): AdGroup model used to view, create, update its attributes and list its different entities.
33 | - [`ads.Ad`](./ads.ads.md#class-ad): Ad model used to view, create, update its attributes
34 | - [`audiences.Audience`](./ads.audiences.md#class-audience): High level model class to manage audiences for an AdAccount
35 | - [`campaigns.Campaign`](./ads.campaigns.md#class-campaign): Campaign model used to view, create, update its attributes and list its different entities.
36 | - [`conversion_events.Conversion`](./ads.conversion_events.md#class-conversion): Conversion Event Model used to send conversion events to Pinterest API
37 | - [`conversion_tags.ConversionTag`](./ads.conversion_tags.md#class-conversiontag): Conversion Tag model used to view, create, update its attributes and list its different entities
38 | - [`customer_lists.CustomerList`](./ads.customer_lists.md#class-customerlist): High level model class to manage customer_lists for an CustomerList
39 | - [`keywords.Keyword`](./ads.keywords.md#class-keyword): High level model class to manage keywords
40 | - [`client.PinterestSDKClient`](./client.md#class-pinterestsdkclient): Wrapper API client for SDK high level models
41 | - [`boards.Board`](./organic.boards.md#class-board): Board model used to view, create, update its attributes and list its different entities.
42 | - [`boards.BoardSection`](./organic.boards.md#class-boardsection): Board Section model used as a helper model for `BOARD`
43 | - [`pins.Pin`](./organic.pins.md#class-pin): Pin model used to view, create, update its attributes and list its different entities.
44 | - [`base_model.PinterestBaseModel`](./utils.base_model.md#class-pinterestbasemodel): Base Model for all other Higher Level Models in the Python Client
45 | - [`bookmark.Bookmark`](./utils.bookmark.md#class-bookmark): Bookmark Model used as a utilty to improve pagination experience for user.
46 | - [`sdk_exceptions.SdkException`](./utils.sdk_exceptions.md#class-sdkexception): Raises an exception for Model's Errors
47 |
48 | ## Functions
49 |
50 | - [`error_handling.verify_api_response`](./utils.error_handling.md#function-verify_api_response): Verify that there are no errors in `response` received from api
51 | - [`load_json_config.load_json_config`](./utils.load_json_config.md#function-load_json_config): Parse a config.json file and then load all the variables found as environment variables.
52 | - [`load_json_config.load_json_config_from_single_env_var`](./utils.load_json_config.md#function-load_json_config_from_single_env_var): Parse PINTEREST_JSON_ENV_VARIABLES environment variable to split long JSON string into
53 | - [`refresh_access_token.get_new_access_token`](./utils.refresh_access_token.md#function-get_new_access_token): Function used to retrieve a new access token for a user using the refresh token.
54 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_ads.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Ad Model
3 | """
4 |
5 | from pinterest.ads.ads import Ad
6 |
7 | from openapi_generated.pinterest_client.exceptions import ApiValueError
8 | from openapi_generated.pinterest_client.exceptions import NotFoundException
9 | from openapi_generated.pinterest_client.model.entity_status import EntityStatus
10 |
11 | from integration_tests.base_test import BaseTestCase
12 | from integration_tests.config import DEFAULT_PIN_ID, DEFAULT_AD_ACCOUNT_ID
13 |
14 |
15 | class TestCreateAd(BaseTestCase):
16 | """
17 | Test creating Ad model
18 | """
19 |
20 | def test_create_ad_success(self):
21 | """
22 | Test create ad success
23 | """
24 | ad = Ad.create(
25 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
26 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
27 | creative_type="REGULAR",
28 | pin_id=DEFAULT_PIN_ID,
29 | name="Test_create_ad",
30 | status="ACTIVE",
31 | is_pin_deleted=False,
32 | is_removable=False,
33 | client=self.test_client,
34 | )
35 |
36 | assert ad
37 | assert getattr(ad, "_id")
38 | assert getattr(ad, "_name") == "Test_create_ad"
39 |
40 | def test_create_ad_failure_without_creative_type(self):
41 | """
42 | Test create ad fail without creative type
43 | """
44 | ad_arguments = dict(
45 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
46 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
47 | pin_id=DEFAULT_PIN_ID,
48 | name="Test_create_ad",
49 | status="ACTIVE",
50 | client=self.test_client,
51 | )
52 |
53 | with self.assertRaises(TypeError):
54 | Ad.create(**ad_arguments)
55 |
56 | def test_create_ad_failure_with_incorrect_creative_type(self):
57 | """
58 | Test create ad fail with incorrect creative type
59 | """
60 | ad_arguments = dict(
61 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
62 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
63 | creative_type="NOT",
64 | pin_id=DEFAULT_PIN_ID,
65 | name="Test_create_ad",
66 | status="ACTIVE",
67 | client=self.test_client,
68 | )
69 |
70 | with self.assertRaises(ApiValueError):
71 | Ad.create(**ad_arguments)
72 |
73 | class TestGetAd(BaseTestCase):
74 | """
75 | Test getting Ad model
76 | """
77 | def test_get_ad_success(self):
78 | """
79 | Test get ad success
80 | """
81 | ad = Ad(
82 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
83 | ad_id=self.ad_utils.get_ad_id(),
84 | client=self.test_client,
85 | )
86 |
87 | assert ad
88 | assert getattr(ad, "_id") == self.ad_utils.get_ad_id()
89 |
90 | def test_get_ad_fail_with_invalid_id(self):
91 | """
92 | Test get ad with invalid id
93 | """
94 | non_existant_ad_id="123321"
95 | ad_arguments = dict(
96 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
97 | ad_id=non_existant_ad_id,
98 | client=self.test_client,
99 | )
100 |
101 | with self.assertRaises(NotFoundException):
102 | Ad(**ad_arguments)
103 |
104 |
105 | class TestGetListAd(BaseTestCase):
106 | """
107 | Test get all Ad model
108 | """
109 | def test_get_all_success(self):
110 | """
111 | Test get all ads successfully
112 | """
113 | NUMBER_OF_NEW_ADS = 3
114 | new_ad_ids = list(
115 | getattr(self.ad_utils.create_new_ad(),"_id") for _ in range(NUMBER_OF_NEW_ADS)
116 | )
117 |
118 | ads, _ = Ad.get_all(
119 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
120 | ad_ids=new_ad_ids,
121 | )
122 | assert len(ads) == NUMBER_OF_NEW_ADS
123 |
124 | def test_get_all_with_invalid_ids_fail(self):
125 | """
126 | Test get all with invalid IDs fail
127 | """
128 | INVALID_AD_IDS = ["11111111", "222222222"]
129 | get_list_dict = dict(
130 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
131 | ad_ids=INVALID_AD_IDS
132 | )
133 |
134 | with self.assertRaises(TypeError):
135 | Ad.get_all(get_list_dict)
136 |
137 |
138 | class TestUpdateAds(BaseTestCase):
139 | """
140 | Test update Ad model
141 | """
142 | def test_update_ad_success(self):
143 | """
144 | Test update Ads successfully
145 | """
146 | ad = Ad(
147 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
148 | ad_id=self.ad_utils.get_ad_id(),
149 | client=self.test_client,
150 | )
151 |
152 | new_name = "NEW_AD_NAME"
153 | new_status = "PAUSED"
154 |
155 | ad.update_fields(
156 | name=new_name,
157 | status=new_status
158 | )
159 |
160 | assert ad
161 | assert getattr(ad, "_name") == new_name
162 | assert getattr(ad, "_status") == EntityStatus(new_status)
163 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_ad_accounts.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Ad Account Model
3 | """
4 |
5 | from unittest.mock import patch
6 |
7 | from pinterest.ads.ad_accounts import AdAccount
8 | from pinterest.ads.campaigns import Campaign
9 | from pinterest.ads.audiences import Audience
10 |
11 | from integration_tests.base_test import BaseTestCase
12 | from integration_tests.config import OWNER_USER_ID, DEFAULT_AD_ACCOUNT_ID
13 |
14 |
15 | class TestAdAccount(BaseTestCase):
16 | """
17 | Test Ad Account model and its higher level functions
18 | """
19 | def test_create_ad_account(self):
20 | """
21 | Test creating a new ad_account
22 | """
23 | ad_account = AdAccount.create(
24 | client=self.test_client,
25 | country="US",
26 | name="SDK Test Ad Account",
27 | owner_user_id=OWNER_USER_ID,
28 | )
29 |
30 | assert ad_account
31 | assert getattr(ad_account, '_id')
32 | assert getattr(ad_account, '_name') == "SDK Test Ad Account"
33 |
34 | def test_get_existing_ad_account(self):
35 | """
36 | Test if a AdAccount model/object is created successfully with correct account_id
37 | """
38 | ad_account = AdAccount(
39 | ad_account_id=self.ad_account_utils.get_default_ad_account_id(),
40 | client=self.test_client
41 | )
42 | assert ad_account
43 | assert getattr(ad_account, '_id') == DEFAULT_AD_ACCOUNT_ID
44 |
45 |
46 | class TestListCampaigns(BaseTestCase):
47 | """
48 | Test List Campaigns method for a given Ad Account
49 | """
50 | # pylint: disable=duplicate-code
51 | @patch('pinterest.ads.campaigns.CampaignsApi.campaigns_get')
52 | def test_get_all_campaigns(self, campaigns_get_mock):
53 | """
54 | Test if all campaigns are returned for a given Ad Account ID
55 | """
56 | NUMBER_OF_CAMPAIGNS_TO_CREATE = 3
57 | created_campaign_ids = set(
58 | getattr(self.campaign_utils.create_new_campaign(), '_id') for _ in range(NUMBER_OF_CAMPAIGNS_TO_CREATE)
59 | )
60 |
61 | assert len(created_campaign_ids) == NUMBER_OF_CAMPAIGNS_TO_CREATE
62 |
63 | ad_account = self.ad_account_utils.get_default_ad_account()
64 | campaigns_list, _ = ad_account.list_campaigns(
65 | campaign_ids=list(created_campaign_ids),
66 | )
67 |
68 | assert campaigns_get_mock.call_count - 1 == NUMBER_OF_CAMPAIGNS_TO_CREATE
69 | assert len(created_campaign_ids) == len(campaigns_list)
70 |
71 | get_all_campaigns_ids = set()
72 | for campaign in campaigns_list:
73 | get_all_campaigns_ids.add(getattr(campaign, '_id'))
74 | assert isinstance(campaign, Campaign)
75 |
76 | assert created_campaign_ids == get_all_campaigns_ids
77 |
78 |
79 | class TestListAudiences(BaseTestCase):
80 | """
81 | Test List Audiences method for a given Ad Account
82 | """
83 | # pylint: disable=duplicate-code
84 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
85 | def test_get_all_audiences(self, audiences_get_mock):
86 | """
87 | Test if all audiences are returned for a given Ad Account ID
88 | """
89 | NUMBER_OF_AUDIENCES_TO_CREATE = 3
90 | created_audience_ids = set(
91 | getattr(self.audience_utils.create_new_audience(), '_id') for _ in range(NUMBER_OF_AUDIENCES_TO_CREATE)
92 | )
93 |
94 | assert len(created_audience_ids) == NUMBER_OF_AUDIENCES_TO_CREATE
95 |
96 | audiences_list, _ = Audience.get_all(
97 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
98 | order="DESCENDING",
99 | page_size=NUMBER_OF_AUDIENCES_TO_CREATE,
100 | )
101 |
102 | assert audiences_get_mock.call_count - 1 == NUMBER_OF_AUDIENCES_TO_CREATE
103 | assert len(created_audience_ids) == len(audiences_list)
104 |
105 | get_all_audiences_ids = set()
106 | for audience in audiences_list:
107 | get_all_audiences_ids.add(getattr(audience, '_id'))
108 | assert isinstance(audience, Audience)
109 |
110 | assert created_audience_ids == get_all_audiences_ids
111 |
112 |
113 | class TestListCustomerLists(BaseTestCase):
114 | """
115 | Test List customer lists from Ad Account
116 | """
117 | def test_list_customer_list_success(self):
118 | """
119 | Test if Ad Account can lists customer lists
120 | """
121 | NUMBER_OF_NEW_CUSTOMER_LISTS = 3
122 | new_customer_list_ids = set()
123 | for _ in range(NUMBER_OF_NEW_CUSTOMER_LISTS):
124 | new_customer_list_ids.add(getattr(self.customer_list_utils.create_new_customer_list(), "_id"))
125 |
126 | assert len(new_customer_list_ids) == NUMBER_OF_NEW_CUSTOMER_LISTS
127 |
128 | ad_account = self.ad_account_utils.get_default_ad_account()
129 | new_customer_lists, _ = ad_account.list_customer_lists(
130 | order="DESCENDING",
131 | page_size=NUMBER_OF_NEW_CUSTOMER_LISTS,
132 | )
133 |
134 | get_all_customer_list_ids = set()
135 | for customer_list in new_customer_lists:
136 | get_all_customer_list_ids.add(getattr(customer_list, "_id"))
137 |
138 | assert new_customer_list_ids == get_all_customer_list_ids
139 |
--------------------------------------------------------------------------------
/pinterest/ads/conversion_events.py:
--------------------------------------------------------------------------------
1 | """
2 | Conversion Event Class for Pinterest Python SDK
3 | """
4 | from __future__ import annotations
5 |
6 | from openapi_generated.pinterest_client.model.conversion_events import ConversionEvents
7 | from openapi_generated.pinterest_client.api.conversion_events_api import ConversionEventsApi
8 | from openapi_generated.pinterest_client.model.conversion_events_data import ConversionEventsData
9 | from openapi_generated.pinterest_client.model.conversion_events_user_data import ConversionEventsUserData
10 | from openapi_generated.pinterest_client.model.conversion_events_custom_data import ConversionEventsCustomData
11 | from openapi_generated.pinterest_client.model.conversion_api_response_events import ConversionApiResponseEvents
12 |
13 | from pinterest.client import PinterestSDKClient
14 | from pinterest.utils.base_model import PinterestBaseModel
15 |
16 | class Conversion(PinterestBaseModel):
17 | # pylint: disable=too-many-locals
18 | """
19 | Conversion Event Model used to send conversion events to Pinterest API
20 | """
21 |
22 | @classmethod
23 | def create_conversion_event(
24 | cls,
25 | event_name : str,
26 | action_source : str,
27 | event_time : int,
28 | event_id : str,
29 | user_data : dict,
30 | custom_data : dict,
31 | event_source_url : str = None,
32 | partner_name : str = None,
33 | app_id : str = None,
34 | app_name : str = None,
35 | app_version : str = None,
36 | device_brand : str = None,
37 | device_carrier : str = None,
38 | device_model : str = None,
39 | device_type : str = None,
40 | os_version : str = None,
41 | language : str = None,
42 | **kwargs
43 | ) -> ConversionEventsData:
44 | """ Create Conversion Event Data to be sent.
45 |
46 | Args:
47 | event_name (str): The type of the user event, Enum: "add_to_cart", "checkout", "custom",
48 | "lead", "page_visit", "search", "signup", "view_category", "watch_video"
49 | action_source (str): The source indicating where the conversion event occurred, Enum:
50 | "app_adroid", "app_ios", "web", "offline"
51 | event_time (int): The time when the event happened. Unix timestamp in seconds
52 | event_id (str): The unique id string that identifies this event and can be used for deduping
53 | between events ingested via both the conversion API and Pinterest tracking
54 | user_data (dict): Object containing customer information data. Note, it is required at least
55 | one of 1) em, 2) hashed_maids or 3) pair client_ip_address + client_user_agent.
56 | custom_data (dict): Object containing other custom data.
57 | event_source_url (str, optional): URL of the web conversion event
58 | partner_name (str, optional): The third party partner name responsible to send the event to
59 | Conversion API on behalf of the adverstiser. Only send this field if Pinterest has worked
60 | directly with you to define a value for partner_name.
61 | app_id (str, optional): The app store app ID.
62 | app_name (str, optional): Name of the app.
63 | app_version (str, optional): Version of the app.
64 | device_brand (str, optional): Brand of the user device.
65 | device_carrier (str, optional): User device's model carrier.
66 | device_model (str, optional): Model of the user device.
67 | device_type (str, optional): Type of the user device.
68 | os_version (str, optional): Version of the device operating system.
69 | language (str, optional): Two-character ISO-639-1 language code indicating the user's language.
70 |
71 | Returns:
72 | ConversionEventsData: ConversionEventData to be sent
73 | """
74 | return ConversionEventsData(
75 | event_name = event_name,
76 | action_source = action_source,
77 | event_time = event_time,
78 | event_id = event_id,
79 | user_data = ConversionEventsUserData(**user_data),
80 | custom_data = ConversionEventsCustomData(**custom_data),
81 | event_source_url = event_source_url,
82 | partner_name = partner_name,
83 | app_id = app_id,
84 | app_name = app_name,
85 | app_version = app_version,
86 | device_brand = device_brand,
87 | device_carrier = device_carrier,
88 | device_model = device_model,
89 | device_type = device_type,
90 | os_version = os_version,
91 | language = language,
92 | **kwargs,
93 | )
94 |
95 | @classmethod
96 | def send_conversion_events(
97 | cls,
98 | ad_account_id : str,
99 | conversion_events : list[ConversionEventsData],
100 | test : bool = False,
101 | client : PinterestSDKClient = None,
102 | **kwargs,
103 | )-> tuple(int, int, list[ConversionApiResponseEvents]):
104 | """
105 | Send conversion events to Pinterest API for Conversions.
106 |
107 | Note: Highly recommend to use create_client_with_token (with Conversion Access Token) to create different
108 | client for this functionality.
109 | """
110 | response = ConversionEventsApi(api_client=cls._get_client(client)).events_create(
111 | ad_account_id = str(ad_account_id),
112 | conversion_events = ConversionEvents(
113 | data = conversion_events
114 | ),
115 | test = test,
116 | **kwargs,
117 | )
118 |
119 | return response
120 |
--------------------------------------------------------------------------------
/pinterest/client/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest Client
3 | """
4 | import os
5 | from importlib import reload
6 |
7 | from openapi_generated.pinterest_client.configuration import Configuration
8 | from openapi_generated.pinterest_client.api_client import ApiClient
9 | from pinterest import config
10 | from pinterest.utils.refresh_access_token import get_new_access_token
11 | from pinterest.utils.sdk_exceptions import SdkException
12 |
13 |
14 | __all__ = ['default_sdk_client', 'PinterestSDKClient']
15 |
16 | default_sdk_client = None
17 |
18 |
19 | class PinterestSDKClient(ApiClient):
20 | # pylint: disable=too-many-arguments
21 | """
22 | Wrapper API client for SDK high level models
23 |
24 | NOTE: This class is base in a generated by OpenAPI Generator.
25 | Ref: https://openapi-generator.tech
26 | """
27 |
28 | def __init__(self, access_token=None, refresh_token=None, app_id=None, app_secret=None, configuration=None,
29 | header_name=None, header_value=None, cookie=None, pool_threads=1):
30 | _configuration = None
31 |
32 | if access_token:
33 | _configuration = PinterestSDKClient._get_config(access_token)
34 |
35 | if (refresh_token and app_id and app_secret) and not access_token:
36 | _access_token = PinterestSDKClient._get_access_token(
37 | refresh_token,
38 | app_id,
39 | app_secret
40 | )
41 | _configuration = PinterestSDKClient._get_config(_access_token)
42 |
43 | if configuration:
44 | _configuration = configuration
45 |
46 | super().__init__(configuration=_configuration,
47 | header_name=header_name,
48 | header_value=header_value,
49 | cookie=cookie,
50 | pool_threads=pool_threads,
51 | user_agent=config.PINTEREST_USER_AGENT)
52 |
53 | @classmethod
54 | def create_client_with_refresh_token(cls, refresh_token: str, app_id: str, app_secret: str):
55 | """Get a new SDK client with the given refresh token, app id and app secret."""
56 | access_token = cls._get_access_token(
57 | refresh_token=refresh_token,
58 | app_id=app_id,
59 | app_secret=app_secret
60 | )
61 | return PinterestSDKClient(
62 | configuration=cls._get_config(
63 | access_token=access_token
64 | )
65 | )
66 |
67 | @classmethod
68 | def create_client_with_token(cls, access_token: str):
69 | """Get a new SDK client with the given access token."""
70 | return PinterestSDKClient(
71 | configuration=cls._get_config(
72 | access_token=access_token
73 | )
74 | )
75 |
76 | @classmethod
77 | def set_default_client(cls, client):
78 | """Replace the default client with the given client."""
79 | global default_sdk_client # pylint: disable=global-statement
80 | default_sdk_client = client
81 |
82 | @classmethod
83 | def set_default_access_token(cls, access_token: str):
84 | """Replace the default access_token with the given ones."""
85 | os.environ['PINTEREST_ACCESS_TOKEN'] = access_token
86 | cls._reset_default_client()
87 | reload(config)
88 |
89 | @classmethod
90 | def set_default_refresh_token(cls, refresh_token: str, app_id: str, app_secret: str):
91 | """Replace the default refresh_token, app_id, app_secret with the given ones."""
92 | os.environ['PINTEREST_REFRESH_ACCESS_TOKEN'] = refresh_token
93 | os.environ['PINTEREST_APP_ID'] = app_id
94 | os.environ['PINTEREST_APP_SECRET'] = app_secret
95 | cls._reset_default_client()
96 | reload(config)
97 |
98 | @classmethod
99 | def create_default_client(cls):
100 | """
101 | Returns the default SDK client.
102 |
103 | If client is not explicitly initialized, a new client will be initialized from environment variables.
104 | """
105 | if not default_sdk_client:
106 | cls._init_default_sdk_client_from_env()
107 | return default_sdk_client
108 |
109 | @classmethod
110 | def _init_default_sdk_client_from_env(cls):
111 | """Loads a new SDK client from environment variables."""
112 | if not config.PINTEREST_ACCESS_TOKEN and not config.PINTEREST_REFRESH_ACCESS_TOKEN:
113 | raise SdkException("Environment variables not present. \
114 | Kindly initialize required environment variables: [PINTEREST_ACCESS_TOKEN] or \
115 | [PINTEREST_APP_ID, PINTEREST_APP_SECRET, PINTEREST_REFRESH_ACCESS_TOKEN]")
116 |
117 | access_token = config.PINTEREST_ACCESS_TOKEN
118 | if not access_token:
119 | access_token = cls._get_access_token()
120 |
121 | configuration = cls._get_config(access_token)
122 | cls._set_default_client(client=cls(configuration=configuration))
123 |
124 |
125 | @classmethod
126 | def _get_access_token(
127 | cls, app_id: str = config.PINTEREST_APP_ID,
128 | app_secret: str = config.PINTEREST_APP_SECRET,
129 | refresh_token: str = config.PINTEREST_REFRESH_ACCESS_TOKEN,
130 | api_uri: str = config.PINTEREST_API_URI
131 | ):
132 | return get_new_access_token(
133 | app_id=app_id,
134 | app_secret=app_secret,
135 | refresh_access_token=refresh_token,
136 | host=api_uri,
137 | )
138 |
139 | @classmethod
140 | def _get_config(cls, access_token: str,
141 | api_uri: str = config.PINTEREST_API_URI,
142 | debug: str = config.PINTEREST_DEBUG,
143 | log_file: str = config.PINTEREST_LOG_FILE,
144 | logger_format: str = config.PINTEREST_LOGGER_FORMAT,
145 | disabled_client_side_validations: str = config.PINTEREST_DISABLED_CLIENT_SIDE_VALIDATIONS):
146 | _config = Configuration(
147 | access_token=access_token,
148 | host=api_uri,
149 | )
150 |
151 | _config.debug = debug
152 | _config.logger_file = log_file
153 | _config.logger_format = logger_format
154 | _config.disabled_client_side_validations = disabled_client_side_validations
155 | return _config
156 |
157 | @classmethod
158 | def _set_default_client(cls, client):
159 | global default_sdk_client # pylint: disable=global-statement
160 | default_sdk_client = client
161 |
162 | @classmethod
163 | def _reset_default_client(cls):
164 | global default_sdk_client # pylint: disable=global-statement
165 | default_sdk_client = None
166 |
--------------------------------------------------------------------------------
/docs/utils/script.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import yaml
4 | import markdown
5 |
6 | MODULES = [
7 | "ads",
8 | "bin",
9 | "client",
10 | "organic",
11 | "utils",
12 | ]
13 |
14 | IGNORED_FILES_INDEXING = {
15 | "ads.md", #Ignore this file since ads/__init__.py is converted into ads.md
16 | "organic.md",
17 | "utils.md",
18 | "README.md",
19 | ".pages",
20 | }
21 |
22 | IGNORE_MODULES = [
23 | "bin",
24 | "config",
25 | "version",
26 | ]
27 |
28 | PROJECT_PATH = os.path.abspath(os.getcwd())
29 |
30 |
31 | def set_up_python_path():
32 | """
33 | Set up python path
34 | """
35 | sys.path.append(PROJECT_PATH)
36 |
37 |
38 | def remove_old_doc():
39 | """
40 | Remove old doc in docs/pinterest
41 | """
42 | print("----------------- " + "Cleaning up old docs")
43 |
44 | files = next(os.walk("docs/pinterest"), (None, None, []))[2]
45 | for file in files: os.remove(f"docs/pinterest/{file}")
46 |
47 |
48 | def generate_new_doc():
49 | """
50 | Use lazydoc to generate new doc at: docs/pinterest
51 | """
52 | print("----------------- " + "Generating md docs")
53 |
54 | from lazydocs import generate_docs
55 | generate_docs(
56 | ['pinterest'],
57 | ignored_modules=IGNORE_MODULES,
58 | watermark=False,
59 | remove_package_prefix=True,
60 | overview_file='README.md',
61 | output_path='docs/pinterest/',
62 | src_base_url='https://github.com/pinterest/pinterest-python-sdk/blob/main/')
63 |
64 |
65 | def sort_index(index: dict) -> dict:
66 | """Sort index by key and value
67 |
68 | Args:
69 | index (dict): unsorted index
70 |
71 | Returns:
72 | dict: sorted index
73 | """
74 | return_index = {}
75 | for k,v in index.items():
76 | return_index[k] = sorted(v)
77 |
78 | return_index = dict(sorted(return_index.items(), key=lambda item: item[0]))
79 | return return_index
80 |
81 |
82 | def remove_module_prefix_from_file(file: str, module: str) -> str:
83 | """Remove module prefix from file name
84 | By default, lazydocs add module as prefix
85 | ex: ads.ad_groups.md -> ad_groups.md
86 |
87 | Args:
88 | file (str): file to rename
89 | module (str): module of file
90 |
91 | Returns:
92 | str: new file name
93 | """
94 | new_name = file
95 | if file != (module + ".md") and file.find(module) == 0:
96 | new_name = file[len(module)+1:]
97 | return new_name
98 |
99 |
100 | def check_index(num_file: int, index: dict):
101 | """Check if we index corrent number of files
102 |
103 | Args:
104 | num_file (int): number of file to be indexed
105 | index (dict): the index
106 |
107 | Raises:
108 | Exception: Couldn't index all file
109 | """
110 | print("Files to index: " + str(num_file))
111 | num_file_indexed = sum([len(value) for value in index.values()])
112 | print("Total file indexed: " + str(num_file_indexed))
113 |
114 | if num_file != num_file_indexed:
115 | raise Exception("Cound't index all file, please double check")
116 |
117 |
118 | def create_file_index() -> dict:
119 | """
120 | Create file index: index[module_name] = list[file_name]
121 |
122 | This function also get rid of the module prefix that lazydocs added in every file
123 |
124 | Note: all the file without module, such as configs.py, version.py is mapped
125 | to index["extra"]
126 |
127 | Returns:
128 | dict: file index
129 | """
130 | print("----------------- " + "Indexing docs")
131 |
132 | index = {}
133 | extra_mapping = []
134 | files = next(os.walk("docs/pinterest"), (None, None, []))[2]
135 |
136 | for file in files:
137 | if file in IGNORED_FILES_INDEXING:
138 | continue
139 |
140 | found_matching_module = False
141 | for module in MODULES:
142 | if len(file) < len(module):
143 | continue
144 |
145 | if file.find(module) == 0:
146 | if module not in index:
147 | index[module] = []
148 | index[module].append(file)
149 |
150 | found_matching_module = True
151 | break
152 |
153 | if not found_matching_module:
154 | extra_mapping.append(file)
155 |
156 | return_index = sort_index(index)
157 |
158 | if extra_mapping:
159 | return_index["extra"] = extra_mapping
160 |
161 | check_index(len(files)-len(IGNORED_FILES_INDEXING), return_index)
162 |
163 | return return_index
164 |
165 |
166 | def truncate_md_extension(file: str) -> str:
167 | """Truncate .md file extension
168 |
169 | Args:
170 | file (str): file name
171 |
172 | Returns:
173 | str: origin file name
174 | """
175 | return file[:-3]
176 |
177 |
178 | def append_doc_to_spec_file(index: dict):
179 | """
180 | Accord to index file, append docs to spec skeleton spec and overwrite it to python-sdk-doc.yaml
181 |
182 | Args:
183 | index (dict): file index
184 | """
185 | print("----------------- " + "Appending doc to spec file")
186 |
187 | # Get skeleton spec
188 | spec_path = PROJECT_PATH + '/docs/utils/skeleton-spec.yaml'
189 | spec = yaml.safe_load(open(spec_path, 'r'))
190 |
191 | # Update version
192 | from pinterest.version import __version__
193 | spec['info']['version'] = __version__
194 |
195 | # Appending md doc into skeleton spec
196 | spec['tags'] = []
197 | spec['x-tagGroups'] = []
198 | for module, md_files in index.items():
199 | tag_names = []
200 | for md_file in md_files:
201 | name = truncate_md_extension(remove_module_prefix_from_file(md_file, module))
202 | tag_names.append(name)
203 | spec['tags'].append(
204 | {
205 | "name": name,
206 | "description": markdown.markdown(open(PROJECT_PATH + '/docs/pinterest/' + md_file, 'r').read()),
207 | "x-traitTag": False,
208 | }
209 | )
210 | spec['x-tagGroups'].append(
211 | {
212 | "name": 'pinterest.' + module,
213 | "tags": tag_names,
214 | }
215 | )
216 |
217 | # Overwrite new spec to python-sdk-doc.yaml
218 | with open(PROJECT_PATH + '/docs/utils/python-sdk-doc.yaml', 'w') as file:
219 | yaml.dump(spec, file)
220 |
221 |
222 | def start_doc():
223 | """
224 | Use this function to generate documentations
225 | """
226 | set_up_python_path()
227 |
228 | remove_old_doc()
229 |
230 | generate_new_doc()
231 |
232 | index = create_file_index()
233 |
234 | append_doc_to_spec_file(index)
235 |
236 | start_doc()
237 |
--------------------------------------------------------------------------------
/tests/src/pinterest/ads/test_keywords.py:
--------------------------------------------------------------------------------
1 | '''
2 | Test Keyword Model
3 | '''
4 | from unittest import TestCase
5 | from unittest.mock import patch
6 |
7 | from openapi_generated.pinterest_client.model.paginated import Paginated
8 | from openapi_generated.pinterest_client.model.keyword import Keyword as GeneratedKeyword
9 | from openapi_generated.pinterest_client.model.keywords_response import KeywordsResponse
10 | from openapi_generated.pinterest_client.model.match_type_response import MatchTypeResponse
11 |
12 |
13 | from pinterest.ads.keywords import Keyword
14 |
15 | from openapi_generated.pinterest_client.model.match_type_response import MatchTypeResponse
16 |
17 | class TestKeyword(TestCase):
18 | '''
19 | Test Keyword Model and its higher level functions
20 | '''
21 | def __init__(self, *args, **kwargs):
22 | super().__init__(*args, **kwargs)
23 | self.test_ad_account_id = "777777777777"
24 | self.test_keyword_id = "888888888888"
25 |
26 | @patch('pinterest.ads.keywords.KeywordsApi.keywords_get')
27 | def test_create_keyword_model_using_existing_keyword(self, keyword_get_mock):
28 | '''
29 | Test if Keyword model/object is created successfully with correct account_id, keyword_id
30 | '''
31 | keyword_get_mock.__name__ = "keywords_get"
32 | keyword_get_mock.return_value = Paginated(
33 | items=[
34 | {
35 | "archived": False,
36 | "id": self.test_keyword_id,
37 | "parent_id": "383791336903426391",
38 | "parent_type": "campaign",
39 | "type": "keyword",
40 | "bid": 200000,
41 | "match_type": "BROAD",
42 | "value": "string",
43 | },
44 | ],
45 | bookmark="test_keyword",
46 | )
47 |
48 | keyword_response, _ = Keyword.get_all(
49 | ad_account_id=self.test_ad_account_id,
50 | campaign_id="383791336903426391",
51 | page_size=1,
52 | )
53 | keyword = keyword_response[0]
54 |
55 |
56 | assert getattr(keyword, '_id') == self.test_keyword_id
57 | assert getattr(keyword, '_ad_account_id') == self.test_ad_account_id
58 | assert getattr(keyword, '_parent_id') == "383791336903426391"
59 | assert getattr(keyword, '_parent_type') == "campaign"
60 |
61 | @patch('pinterest.ads.keywords.KeywordsApi.keywords_create')
62 | @patch('pinterest.ads.keywords.KeywordsApi.keywords_get')
63 | def test_create_keyword_model_new_keyword(self, keyword_get_mock, keyword_create_mock):
64 | """
65 | Test if a Keyword model/object is created successfully with correct account_id and keyword
66 | """
67 | keyword_create_mock.__name__ = "keywords_create"
68 | keyword_get_mock.return_value = Paginated(
69 | items=[
70 | {
71 | "archived": False,
72 | "id": self.test_keyword_id,
73 | "parent_id": "383791336903426391",
74 | "parent_type": "campaign",
75 | "type": "keyword",
76 | "bid": 200000,
77 | "match_type": "BROAD",
78 | "value": "string",
79 | },
80 | ],
81 | bookmark="string",
82 | )
83 |
84 | keyword_create_mock.return_value = KeywordsResponse(
85 | keywords=[GeneratedKeyword(
86 | archived=False,
87 | id=self.test_keyword_id,
88 | parent_id="383791336903426391",
89 | parent_type="campaign",
90 | type="keyword",
91 | bid=200000,
92 | match_type=MatchTypeResponse("BROAD"),
93 | value="string",
94 | )
95 | ],
96 | errors=[],
97 | )
98 |
99 | created_keyword = Keyword.create(
100 | ad_account_id=self.test_ad_account_id,
101 | parent_id="383791336903426391",
102 | value="string",
103 | match_type="BROAD",
104 | )
105 |
106 | assert created_keyword
107 | assert getattr(created_keyword, '_id')
108 | assert getattr(created_keyword, '_ad_account_id') == self.test_ad_account_id
109 | assert getattr(created_keyword, '_value') == "string"
110 | assert getattr(created_keyword, '_match_type') == MatchTypeResponse("BROAD")
111 |
112 |
113 | @patch('pinterest.ads.keywords.KeywordsApi.keywords_update')
114 | @patch('pinterest.ads.keywords.KeywordsApi.keywords_get')
115 | def test_update_keyword_success(self, get_mock, update_mock):
116 | """
117 | Test if Keyword Model can be updated
118 | """
119 | get_mock.__name__ = "keywords_get"
120 | update_mock.__name__ = "keywords_update"
121 | new_bid = 10
122 |
123 | get_mock.return_value = Paginated(
124 | items=[
125 | {
126 | "archived": False,
127 | "id": self.test_keyword_id,
128 | "parent_id": "383791336903426391",
129 | "parent_type": "campaign",
130 | "type": "keyword",
131 | "bid": 200000,
132 | "match_type": "BROAD",
133 | "value": "string",
134 | },
135 | ],
136 | bookmark="test_keyword",
137 | )
138 | update_mock.return_value = KeywordsResponse(
139 | errors=[
140 | ],
141 | keywords=[
142 | GeneratedKeyword(
143 | archived=False,
144 | id=self.test_keyword_id,
145 | parent_id="383791336903426391",
146 | parent_type="campaign",
147 | type="keyword",
148 | bid=new_bid,
149 | match_type=MatchTypeResponse("BROAD"),
150 | value="string",
151 | )
152 | ]
153 | )
154 |
155 | keywords, _ = Keyword.get_all(
156 | ad_account_id=self.test_ad_account_id,
157 | campaign_id="383791336903426391",
158 | page_size=1,
159 | )
160 | keyword = keywords[0]
161 | update_response = keyword.update_fields(
162 | bid=new_bid
163 | )
164 |
165 | assert update_response == True
166 | assert getattr(keyword, "_id") == self.test_keyword_id
167 | assert getattr(keyword, "_bid") == new_bid
168 |
--------------------------------------------------------------------------------
/tests/src/pinterest/ads/test_audiences.py:
--------------------------------------------------------------------------------
1 | """
2 | Test Audience Model
3 | """
4 |
5 | from unittest import TestCase
6 | from unittest.mock import patch
7 |
8 | from openapi_generated.pinterest_client.model.audience import Audience as GeneratedAudience
9 | from openapi_generated.pinterest_client.model.audience_rule import AudienceRule
10 |
11 | from pinterest.ads.audiences import Audience
12 |
13 |
14 | class TestAudience(TestCase):
15 | """
16 | Test Audience model and its higher level functions
17 | """
18 | def __init__(self, *args, **kwargs):
19 | super().__init__(*args, **kwargs)
20 | self.test_ad_account_id = "777777777777"
21 | self.test_audience_id = "111111111111"
22 |
23 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
24 | def test_create_audience_model_using_existing_audience(self, audiences_get_mock):
25 | """
26 | Test if a Audience model/object is created successfully with correct audience_id
27 | """
28 |
29 | audiences_get_mock.return_value = GeneratedAudience(
30 | ad_account_id=self.test_ad_account_id,
31 | id=self.test_audience_id,
32 | name="Test Audience",
33 | audience_type="VISITOR",
34 | description="Test Description",
35 | rule=AudienceRule(
36 | prefill = True,
37 | retention_days = 7,
38 | visitor_source_id = "123123123123",
39 | ),
40 | size=100000,
41 | status="READY",
42 | created_timestamp=1499361351,
43 | updated_timestamp=1499361351,
44 | )
45 |
46 | audience_response = Audience(ad_account_id=self.test_ad_account_id, audience_id=self.test_audience_id)
47 |
48 | assert getattr(audience_response, '_id') == self.test_audience_id
49 | assert getattr(audience_response, '_audience_type') == 'VISITOR'
50 |
51 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_create')
52 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
53 | def test_create_new_audience(self, audiences_get_mock, audiences_create_mock):
54 | """
55 | Test if a Audience model/object is created successfully with correct information
56 | """
57 | audiences_create_mock.__name__ = "audiences_create"
58 | audiences_create_mock.return_value = GeneratedAudience(
59 | ad_account_id=self.test_ad_account_id,
60 | id=self.test_audience_id,
61 | name="Test Audience",
62 | audience_type="VISITOR",
63 | description="Test Description",
64 | rule=AudienceRule(
65 | prefill = True,
66 | retention_days = 7,
67 | visitor_source_id = "123123123123",
68 | ),
69 | size=100000,
70 | status="READY",
71 | created_timestamp=1499361351,
72 | updated_timestamp=1499361351,
73 | )
74 | audiences_get_mock.return_value = audiences_create_mock.return_value
75 |
76 | created_audience = Audience.create(
77 | ad_account_id=self.test_ad_account_id,
78 | name="Test Audience",
79 | rule=dict(
80 | prefill = True,
81 | retention_days = 7,
82 | visitor_source_id = "123123123123",
83 | ),
84 | audience_type="VISITOR",
85 | description="TEST DESCRIPTION",
86 | )
87 |
88 | assert created_audience
89 | assert getattr(created_audience, '_id') == self.test_audience_id
90 | assert getattr(created_audience, '_name') == "Test Audience"
91 |
92 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_update')
93 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
94 | def test_update_audience_with_kwargs(self, audiences_get_mock, audiences_update_mock):
95 | """
96 | Test if a given audience is updated successfully with passed in keyword arguments
97 | """
98 | audiences_update_mock.__name__ = "audiences_update"
99 |
100 | old_name, new_name = "Test Old Audience Name", "Test New Audience Name"
101 | old_audience_type, new_audience_type = "VISITOR", "ENGAGEMENT"
102 |
103 | audiences_get_mock.return_value = GeneratedAudience(
104 | ad_account_id=self.test_ad_account_id,
105 | id=self.test_audience_id,
106 | name=old_name,
107 | audience_type=old_audience_type,
108 | description="Test Description",
109 | rule=AudienceRule(
110 | prefill = True,
111 | retention_days = 7,
112 | visitor_source_id = "123123123123",
113 | ),
114 | size=100000,
115 | status="READY",
116 | created_timestamp=1499361351,
117 | updated_timestamp=1499361351,
118 | )
119 |
120 | audiences_update_mock.return_value = GeneratedAudience(
121 | ad_account_id=self.test_ad_account_id,
122 | id=self.test_audience_id,
123 | name=new_name,
124 | audience_type=new_audience_type,
125 | description="Test Description",
126 | rule=AudienceRule(
127 | prefill = True,
128 | retention_days = 7,
129 | visitor_source_id = "123123123123",
130 | ),
131 | size=100000,
132 | status="READY",
133 | created_timestamp=1499361351,
134 | updated_timestamp=1499361351,
135 | )
136 |
137 | audience_response = Audience(ad_account_id=self.test_ad_account_id, audience_id=self.test_audience_id)
138 | audiences_get_mock.return_value = audiences_update_mock.return_value
139 | update_response = audience_response.update_fields(name=new_name, audience_type=new_audience_type)
140 |
141 | assert update_response
142 | assert getattr(audience_response, '_name') == new_name
143 | assert getattr(audience_response, '_audience_type') == new_audience_type
144 |
145 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_get')
146 | @patch('pinterest.ads.audiences.AudiencesApi.audiences_list')
147 | def test_get_all_audiences_in_ad_account(self, audiences_list_mock, audiences_get_mock):
148 | """
149 | Test class method returns all Audiences in a given ad_account in a list
150 | """
151 | audiences_list_mock.__name__ = "audiences_list"
152 | audiences_get_mock.__name__ = "audiences_get"
153 | audiences_list_mock.return_value = {
154 | "items": [
155 | {
156 | "id": "123123123123",
157 | "ad_account_id": self.test_ad_account_id,
158 | "name": 'SDK_TEST_CLIENT',
159 | "audience_type": "ENGAGEMENT",
160 | "type": "audience",
161 | }
162 | ],
163 | "bookmark": None
164 | }
165 |
166 | audience_list, bookmark = Audience.get_all(
167 | ad_account_id=self.test_ad_account_id,
168 | page_size=1
169 | )
170 |
171 | audiences_get_mock.assert_not_called()
172 |
173 | assert audience_list
174 | assert not bookmark
175 | assert isinstance(audience_list[0], Audience)
176 | assert getattr(audience_list[0], '_audience_type') == "ENGAGEMENT"
177 |
--------------------------------------------------------------------------------
/pinterest/utils/base_model.py:
--------------------------------------------------------------------------------
1 | """
2 | Pinterest Base Model
3 | """
4 | from typing import Callable
5 |
6 | from openapi_generated.pinterest_client.exceptions import ApiTypeError
7 |
8 | from pinterest.client import PinterestSDKClient
9 |
10 | from pinterest.utils.error_handling import verify_api_response
11 | from pinterest.utils.bookmark import Bookmark
12 |
13 |
14 |
15 | class PinterestBaseModel:
16 | """
17 | Base Model for all other Higher Level Models in the Python Client
18 | """
19 |
20 | def __init__(
21 | self,
22 | _id: str,
23 | generated_api: object,
24 | generated_api_get_fn: str,
25 | generated_api_get_fn_args: dict,
26 | model_attribute_types: dict,
27 | client=None,
28 | ):
29 | # pylint: disable=too-many-arguments
30 | self._id = _id
31 | self._client = client
32 | if self._client is None:
33 | self._client = PinterestBaseModel._get_client()
34 | self._generated_api = generated_api(self._client)
35 | self._generated_api_get_fn = generated_api_get_fn
36 | self._generated_api_get_fn_args = generated_api_get_fn_args
37 | self._model_attribute_types = model_attribute_types
38 |
39 | self._property_dict = dict((k, getattr(self, k))
40 | for k, v in self.__class__.__dict__.items()
41 | if isinstance(v, property))
42 |
43 | def __str__(self):
44 | return f"{self.__class__.__name__} <{self._id}> Model: {self._property_dict}"
45 |
46 | def __repr__(self):
47 | args_string = ""
48 | for arg, arg_val in self._generated_api_get_fn_args.items():
49 | args_string += f"{arg}={arg_val}"
50 | return f"{self.__class__.__name__}({args_string})"
51 |
52 | def _populate_fields(self, **kwargs):
53 | """
54 | Populate all fields as model attributes
55 | """
56 | _model_data = kwargs.get('_model_data')
57 | if not _model_data:
58 | _model_data = getattr(
59 | self._generated_api,
60 | self._generated_api_get_fn
61 | )(**self._generated_api_get_fn_args).to_dict()
62 |
63 | for field in _model_data:
64 | if not self._model_attribute_types.get(field):
65 | continue
66 |
67 | if _model_data.get(field) is None:
68 | attribute_value = None
69 | else:
70 | try:
71 | attribute_value = (self._model_attribute_types.get(field)[0])(_model_data.get(field))
72 | except ApiTypeError:
73 | attribute_value = (self._model_attribute_types.get(field)[0])(**_model_data.get(field))
74 | except TypeError:
75 | attribute_value = type(self._model_attribute_types.get(field)[0])(_model_data.get(field))
76 |
77 | setattr(
78 | self,
79 | f"_{field}",
80 | attribute_value
81 | )
82 |
83 | def __eq__(self, obj):
84 | return isinstance(self, type(obj)) and getattr(self, "id") == getattr(obj, "id")
85 |
86 | @classmethod
87 | def _get_client(cls, client = None):
88 | if not client:
89 | client = PinterestSDKClient.create_default_client()
90 | return client
91 |
92 | @classmethod
93 | def _get_api_instance(
94 | cls,
95 | api,
96 | client: PinterestSDKClient = None
97 | ):
98 | return api(api_client=cls._get_client(client))
99 |
100 | @classmethod
101 | def _call_method(
102 | cls,
103 | instance,
104 | function,
105 | params,
106 | **kwargs
107 | ):
108 | return getattr(instance, function)(**params, **kwargs)
109 |
110 | @classmethod
111 | def _list(
112 | cls,
113 | params: [any] = None,
114 | page_size: int = None,
115 | order: str = None,
116 | bookmark: str = None,
117 | api: type = None,
118 | list_fn: Callable = None,
119 | map_fn: Callable = None,
120 | bookmark_model_cls: object = None,
121 | bookmark_model_fn: Callable = None,
122 | client: PinterestSDKClient = None,
123 | **kwargs
124 | ):
125 | # pylint: disable=too-many-arguments
126 |
127 | if page_size:
128 | kwargs["page_size"] = page_size
129 | if order:
130 | kwargs["order"] = order
131 | if bookmark:
132 | kwargs["bookmark"] = bookmark
133 |
134 | items = []
135 | bookmark = None
136 |
137 | http_response = cls._call_method(
138 | cls._get_api_instance(api, client),
139 | list_fn.__name__,
140 | params,
141 | **kwargs
142 | )
143 |
144 | verify_api_response(http_response)
145 |
146 | items = http_response.get('items', [])
147 | bookmark = http_response.get('bookmark', None)
148 |
149 | # Set the new bookmark
150 | if bookmark is not None:
151 | kwargs["bookmark"] = bookmark
152 |
153 | if not bookmark_model_cls:
154 | kwargs.update(params)
155 | bookmark_model = Bookmark(
156 | bookmark_token=bookmark,
157 | model=cls if not bookmark_model_cls else bookmark_model_cls,
158 | model_fn='get_all' if not bookmark_model_fn else bookmark_model_fn.__name__,
159 | model_fn_args=kwargs,
160 | client=client if not bookmark_model_cls else None,
161 | ) if bookmark else None
162 |
163 | return [map_fn(item) for item in items], bookmark_model
164 |
165 | @classmethod
166 | def _create(
167 | cls,
168 | params: [any] = None,
169 | api: type = None,
170 | create_fn: Callable = None,
171 | map_fn: Callable = lambda x: x,
172 | client: PinterestSDKClient = None,
173 | **kwargs
174 | ):
175 | # pylint: disable=too-many-arguments, unused-argument
176 | http_response = cls._call_method(
177 | cls._get_api_instance(api, client),
178 | create_fn.__name__,
179 | params,
180 | **kwargs
181 | )
182 | verify_api_response(http_response)
183 | return map_fn(http_response)
184 |
185 | def _update(
186 | self,
187 | params: [any] = None,
188 | api: type = None,
189 | update_fn: Callable = None,
190 | map_fn: Callable = None,
191 | client: PinterestSDKClient = None,
192 | **kwargs
193 | ):
194 | # pylint: disable=too-many-arguments, protected-access
195 | http_response = self.__class__._call_method(
196 | self.__class__._get_api_instance(api, client),
197 | update_fn.__name__,
198 | params,
199 | )
200 |
201 | verify_api_response(http_response)
202 |
203 | if map_fn:
204 | self._populate_fields(_model_data=map_fn(http_response))
205 | else:
206 | self._populate_fields()
207 |
208 | for arg, value in kwargs.items():
209 | if getattr(self, f'_{arg}') != value:
210 | raise AssertionError(f"Expected {arg} is {value}"
211 | + f" Actual value is {getattr(self, f'_{arg}')}")
212 |
213 | return True
214 |
--------------------------------------------------------------------------------
/docs/pinterest/organic.pins.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `organic.pins`
6 | Pin Class for Pinterest Python SDK
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `Pin`
15 | Pin model used to view, create, update its attributes and list its different entities.
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(
23 | pin_id: 'str',
24 | ad_account_id: 'str' = None,
25 | client: 'PinterestSDKClient' = None,
26 | **kwargs
27 | ) → None
28 | ```
29 |
30 | Initialize or get a Pin owned by the "operation user_account" - or on a group board that has been shared with this account.
31 |
32 | By default, the "operation user_account" is the token user_account. Optional: Business Access: Specify an ad_account_id (obtained via List ad accounts) to use the owner of that ad_account as the "operation user_account". In order to do this, the token user_account must have one of the following Business Access roles on the ad_account:
33 |
34 |
35 | - For Pins on public or protected boards: Owner, Admin, Analyst, Campaign Manager.
36 | - For Pins on secret boards: Owner, Admin.
37 |
38 |
39 |
40 | **Args:**
41 |
42 | - `pin_id` (str): Unique identifier of a Pin.
43 | - `ad_account_id` (str, optional): Unique identifier of an ad account. Defaults to None.
44 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided.
45 |
46 |
47 | ---
48 |
49 | #### property alt_text
50 |
51 |
52 |
53 |
54 |
55 | ---
56 |
57 | #### property board_id
58 |
59 |
60 |
61 |
62 |
63 | ---
64 |
65 | #### property board_owner
66 |
67 |
68 |
69 |
70 |
71 | ---
72 |
73 | #### property board_section_id
74 |
75 |
76 |
77 |
78 |
79 | ---
80 |
81 | #### property created_at
82 |
83 |
84 |
85 |
86 |
87 | ---
88 |
89 | #### property description
90 |
91 |
92 |
93 |
94 |
95 | ---
96 |
97 | #### property dominant_color
98 |
99 |
100 |
101 |
102 |
103 | ---
104 |
105 | #### property id
106 |
107 |
108 |
109 |
110 |
111 | ---
112 |
113 | #### property link
114 |
115 |
116 |
117 |
118 |
119 | ---
120 |
121 | #### property media
122 |
123 |
124 |
125 |
126 |
127 | ---
128 |
129 | #### property media_source
130 |
131 |
132 |
133 |
134 |
135 | ---
136 |
137 | #### property parent_pin_id
138 |
139 |
140 |
141 |
142 |
143 | ---
144 |
145 | #### property title
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | ---
154 |
155 |
156 |
157 | ### classmethod `create`
158 |
159 | ```python
160 | create(
161 | board_id: 'str',
162 | media_source: 'dict',
163 | link: 'str' = None,
164 | title: 'str' = None,
165 | description: 'str' = None,
166 | dominant_color: 'str' = None,
167 | alt_text: 'str' = None,
168 | board_section_id: 'str' = None,
169 | parent_pin_id: 'str' = None,
170 | client: 'PinterestSDKClient' = None,
171 | **kwargs
172 | ) → Pin
173 | ```
174 |
175 | Create a Pin on a board or board section owned by the "operation user_account".
176 |
177 | Note: If the current "operation user_account" (defined by the access token) has access to another user's Ad Accounts via Pinterest Business Access, you can modify your request to make use of the current operation_user_account's permissions to those Ad Accounts by including the ad_account_id in the path parameters for the request (e.g. .../?ad_account_id=12345&...).
178 |
179 | This function is intended solely for publishing new content created by the user. If you are interested in saving content created by others to your Pinterest boards, sometimes called 'curated content', please use our Save button (https://developers.pinterest.com/docs/add-ons/save-button) instead. For more tips on creating fresh content for Pinterest, review our Content App Solutions Guide (https://developers.pinterest.com/docs/solutions/content-apps).
180 |
181 | Learn more (https://developers.pinterest.com/docs/solutions/content-apps/#creatingvideopins) about video Pin creation
182 |
183 |
184 |
185 | **Args:**
186 |
187 | - `board_id` (str): The board to which this Pin belongs.
188 | - `media_source` (dict): Pin media source. In format: {
189 | - `'source_type'`: (str),
190 | - `'content_type'`: (str),
191 | - `'data'`: (str),
192 | - `'url'`: (str),
193 | - `'cover_image_url'`: (str),
194 | - `'media_id'`: (str), }
195 | - `link` (str, optional): Redirect link of <= 2048 characters. Defaults to None.
196 | - `title` (str, optional): Title of the pin <= 100 characters. Defaults to None.
197 | - `description` (str, optional): Description of the pin <= 500 characters. Defaults to None.
198 | - `dominant_color` (str, optional): Dominant pin color. Hex number, e.g. "#6E7874". Defaults to None.
199 | - `alt_text` (str, optional): <= 500 characters. Defaults to None.
200 | - `board_section_id` (str, optional): The board section to which this Pin belongs. Defaults to None.
201 | - `parent_pin_id` (str, optional): The source pin id if this pin was saved from another pin.
202 | - `Learn more (https`: //help.pinterest.com/article/save-pins-on-pinterest). Defaults to None.
203 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object, uses the default client, if not provided.
204 |
205 | Keyword Args: Any valid keyword arguments or query parameters for endpoint.
206 |
207 |
208 |
209 | **Returns:**
210 |
211 | - `Pin`: Pin object
212 |
213 | ---
214 |
215 |
216 |
217 | ### classmethod `delete`
218 |
219 | ```python
220 | delete(pin_id: 'str', client: 'PinterestSDKClient' = None) → bool
221 | ```
222 |
223 | Delete a Pin owned by the "operation user_account".
224 |
225 |
226 | - By default, the "operation user_account" is the token user_account.
227 |
228 |
229 |
230 | **Args:**
231 |
232 | - `pin_id` (str): Unique identifier of a pin.
233 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided.
234 |
235 |
236 |
237 | **Returns:**
238 |
239 | - `bool`: If the pin was deleted successfully.
240 |
241 | ---
242 |
243 |
244 |
245 | ### method `save`
246 |
247 | ```python
248 | save(board_id: 'str', board_section_id: 'str' = None) → None
249 | ```
250 |
251 | Save a pin on a board or board section owned by the "operation user_account".
252 |
253 |
254 | - By default, the "operation user_account" is the token user_account.
255 |
256 |
257 |
258 | **Args:**
259 |
260 | - `board_id` (str): Unique identifier of the board to which the pin will be saved. Defaults to None.
261 | - `board_section_id` (str, optional): Unique identifier of the board section to which the pin will be saved. Defaults to None.
262 |
263 |
264 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_ad_groups.py:
--------------------------------------------------------------------------------
1 | '''
2 | Test AdGroup Model
3 | '''
4 |
5 | from integration_tests.base_test import BaseTestCase
6 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
7 |
8 | from pinterest.ads.ad_groups import AdGroup
9 |
10 |
11 | class TestCreateAdGroup(BaseTestCase):
12 | '''
13 | Test creating AdGroup Model
14 | '''
15 |
16 | def test_create_ad_group_success(self):
17 | '''
18 | Test creating AdGroup successfully
19 | '''
20 | ad_group = AdGroup.create(
21 | client=self.test_client,
22 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
23 | campaign_id=getattr(self.ad_group_utils.campaign, "_id"),
24 | billable_event="IMPRESSION",
25 | name="SDK_INTEGRATION_TEST_ADGROUP",
26 | auto_targeting_enabled=False,
27 | bid_in_micro_currency=10000000,
28 | targeting_spec=dict(
29 | age_bucket=["35-44"],
30 | location=["US"],
31 | )
32 | )
33 |
34 | assert ad_group
35 | assert getattr(ad_group, "_id")
36 | assert getattr(ad_group, "_name") == "SDK_INTEGRATION_TEST_ADGROUP"
37 | assert getattr(ad_group, "_ad_account_id") == DEFAULT_AD_ACCOUNT_ID
38 | assert getattr(ad_group, "_campaign_id") == getattr(self.ad_group_utils.campaign, "_id")
39 |
40 | def test_get_existing_ad_group(self):
41 | '''
42 | Test if a AdGroup model/object is created successfully with correct audience_id
43 | '''
44 | ad_group = AdGroup(
45 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
46 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
47 | client=self.test_client,
48 | )
49 |
50 | assert ad_group
51 | assert getattr(ad_group, "_id") == self.ad_group_utils.get_ad_group_id()
52 | assert getattr(ad_group, "_campaign_id") == getattr(self.ad_group_utils.campaign, "_id")
53 |
54 |
55 | class TestUpdateAdGroup(BaseTestCase):
56 | """
57 | Test update on AdGroup Model
58 | """
59 | def test_update_success(self):
60 | """
61 | Test update successfully
62 | """
63 | ad_group = AdGroup(
64 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
65 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
66 | client=self.test_client,
67 | )
68 |
69 | new_name = "SDK_AD_GROUP_NEW_NAME"
70 | new_spec = {
71 | "age_bucket":["35-44"],
72 | "location": ["US"],
73 | }
74 |
75 | ad_group.update_fields(
76 | name=new_name,
77 | targeting_spec=new_spec
78 | )
79 |
80 | assert ad_group
81 | assert getattr(ad_group, "_name") == new_name
82 | assert str(getattr(ad_group,"_targeting_spec")).lower() == str(new_spec).lower()
83 |
84 | def test_update_fail_with_invalid_tracking_urls(self):
85 | """
86 | Test update with invalid tracking url
87 | """
88 | ad_group = AdGroup(
89 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
90 | ad_group_id=self.ad_group_utils.get_ad_group_id(),
91 | client=self.test_client,
92 | )
93 |
94 | new_tracking_url = {
95 | "impression": ["URL1","URL2"],
96 | "click": ["URL1","URL2"],
97 | "engagement": ["URL1","URL2"],
98 | "buyable_button": ["URL1","URL2"],
99 | "audience_verification": ["URL1","URL2"],
100 | }
101 |
102 | update_argument = dict(
103 | tracking_urls=new_tracking_url
104 | )
105 |
106 | with self.assertRaises(AssertionError):
107 | ad_group.update_fields(**update_argument)
108 |
109 |
110 | class TestGetListAdGroup(BaseTestCase):
111 | """
112 | Test get list on AdGroup Model
113 | """
114 | def test_get_list_success(self):
115 | """
116 | Test get list successfully
117 | """
118 | NUMBER_OF_NEW_AD_GROUP = 5
119 |
120 | new_ad_group_ids = list(
121 | getattr(self.ad_group_utils.create_new_ad_group(), "_id") for _ in range(NUMBER_OF_NEW_AD_GROUP)
122 | )
123 | ad_groups, _ = AdGroup.get_all(
124 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
125 | ad_group_ids=new_ad_group_ids,
126 | )
127 |
128 | assert len(ad_groups) == NUMBER_OF_NEW_AD_GROUP
129 |
130 | def test_get_list_with_campaign_ids_success(self):
131 | """
132 | Test get list with campaign id
133 | """
134 | old_ad_groups, _ = AdGroup.get_all(
135 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
136 | campaign_ids=[getattr(self.ad_group_utils.campaign, "_id")]
137 | )
138 |
139 | new_campaign = self.campaign_utils.create_new_campaign()
140 | AdGroup.create(
141 | client=self.test_client,
142 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
143 | campaign_id=getattr(new_campaign, "_id"),
144 | billable_event="IMPRESSION",
145 | name="SDK_INTEGRATION_TEST_ADGROUP",
146 | auto_targeting_enabled=False,
147 | bid_in_micro_currency=10000000,
148 | targeting_spec=dict(
149 | age_bucket=["35-44"],
150 | location=["US"],
151 | )
152 | )
153 |
154 | new_ad_groups = AdGroup.get_all(
155 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
156 | campaign_ids=[getattr(self.ad_group_utils.campaign, "_id"), getattr(new_campaign, "_id")]
157 | )
158 |
159 | assert len(new_ad_groups) == len(old_ad_groups) + 1
160 |
161 | def test_get_list_invalid_id_fail(self):
162 | """
163 | Test get list with invalid id
164 | """
165 | INVALID_AD_GROUP_ID = "1111111"
166 | get_list_dict = dict(
167 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
168 | ad_group_ids=[INVALID_AD_GROUP_ID],
169 | )
170 |
171 | with self.assertRaises(TypeError):
172 | AdGroup.get_all(get_list_dict)
173 |
174 |
175 | class TestListAds(BaseTestCase):
176 | """
177 | Test get list on AdGroup Model
178 | """
179 | def test_list_ads_success(self):
180 | """
181 | Test if all ads can be listed from an Ad Group
182 | """
183 | NUMBER_OF_NEW_ADS = 4
184 | new_ad_ids = []
185 | for _ in range(NUMBER_OF_NEW_ADS):
186 | new_ad_ids.append(getattr(self.ad_utils.create_new_ad(),"_id"))
187 |
188 | ad_group = AdGroup(
189 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
190 | ad_group_id=getattr(self.ad_utils.ad_group, "_id"),
191 | client=self.test_client
192 | )
193 | ads, _ = ad_group.list_ads(
194 | ad_ids=new_ad_ids,
195 | )
196 |
197 | assert ads
198 | assert len(ads) == len(new_ad_ids)
199 | for ad in ads:
200 | assert getattr(ad, "_id") in new_ad_ids
201 |
202 | def test_enable_auto_targeting(self):
203 | """
204 | Test enable auto targeting
205 | """
206 | ad_group_0 = self.ad_group_utils.create_new_ad_group(
207 | auto_targeting_enabled=False,
208 | )
209 | self.assertFalse(getattr(ad_group_0, "_auto_targeting_enabled"))
210 | ad_group_0.enable_auto_targeting()
211 |
212 | ad_group_1 = AdGroup(
213 | ad_account_id=getattr(ad_group_0, "_ad_account_id"),
214 | ad_group_id=getattr(ad_group_0, "_id")
215 | )
216 | self.assertTrue(getattr(ad_group_1, "_auto_targeting_enabled"))
217 |
218 | def test_disable_auto_targeting(self):
219 | """
220 | Test disable auto targeting
221 | """
222 | ad_group_0 = self.ad_group_utils.create_new_ad_group(
223 | auto_targeting_enabled=True,
224 | )
225 | self.assertTrue(getattr(ad_group_0, "_auto_targeting_enabled"))
226 | ad_group_0.disable_auto_targeting()
227 |
228 | ad_group_1 = AdGroup(
229 | ad_account_id=getattr(ad_group_0, "_ad_account_id"),
230 | ad_group_id=getattr(ad_group_0, "_id")
231 | )
232 | self.assertFalse(getattr(ad_group_1, "_auto_targeting_enabled"))
233 |
--------------------------------------------------------------------------------
/integration_tests/ads/test_customer_lists.py:
--------------------------------------------------------------------------------
1 | """
2 | Test CustomerList Model
3 | """
4 | from unittest.mock import patch
5 |
6 | from openapi_generated.pinterest_client.exceptions import ApiValueError
7 |
8 | from pinterest.ads.customer_lists import CustomerList
9 |
10 | from integration_tests.base_test import BaseTestCase
11 | from integration_tests.config import DEFAULT_AD_ACCOUNT_ID
12 |
13 |
14 | class TestCreateCustomerList(BaseTestCase):
15 | """
16 | Test creating CustomerList model
17 | """
18 | def test_create_customer_list_success(self):
19 | """
20 | Test creating a new CustomerList successfully
21 | """
22 | customer_list = CustomerList.create(
23 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
24 | name="SDK Test Customer List",
25 | records="test@pinterest.com",
26 | list_type="EMAIL",
27 | )
28 |
29 | assert customer_list
30 | assert getattr(customer_list, '_id')
31 | assert getattr(customer_list, '_name') == "SDK Test Customer List"
32 |
33 | def test_create_customer_list_failure_incorrect_list_type(self):
34 | """
35 | Verify a new CustomerList response failure and catching exceptions
36 | """
37 | customer_list_arguments = dict(
38 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
39 | name="SDK Test Customer List",
40 | records="test@pinterest.com",
41 | list_type="INCORRECT_LIST_TYPE"
42 | )
43 | with self.assertRaisesRegex(ApiValueError, "Invalid"):
44 | CustomerList.create(**customer_list_arguments)
45 |
46 |
47 | class TestGetCustomerList(BaseTestCase):
48 | """
49 | Test retrieving CustomerList model
50 | """
51 | def test_get_existing_customer_list_success(self):
52 | """
53 | Test if a Customer model/object is created successfully with correct customer_list_id
54 | """
55 | customer_list = CustomerList(
56 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
57 | customer_list_id=self.customer_list_utils.get_customer_list_id(),
58 | client=self.test_client
59 | )
60 | assert customer_list
61 | assert getattr(customer_list, '_id') == self.customer_list_utils.get_customer_list_id()
62 |
63 |
64 | class TestUpdateCustomerList(BaseTestCase):
65 | """
66 | Test updating fields in CustomerList model
67 | """
68 | def test_update_field_with_append_operation_success(self):
69 | """
70 | Test upating field successfully with APPEND operation
71 | """
72 | customer_list = CustomerList(
73 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
74 | customer_list_id=self.customer_list_utils.get_customer_list_id(),
75 | client=self.test_client
76 | )
77 |
78 | old_add_counter = getattr(customer_list, "_num_uploaded_user_records")
79 | old_batches_counter = getattr(customer_list, "_num_batches")
80 |
81 | customer_list.update_fields(
82 | records="test_add@pinterest.com",
83 | operation_type="ADD",
84 | )
85 |
86 | assert customer_list
87 | assert getattr(customer_list, "_num_uploaded_user_records") == old_add_counter+1
88 | assert getattr(customer_list, "_num_batches") == old_batches_counter+1
89 |
90 | def test_update_field_with_remove_operation_success(self):
91 | """
92 | Test upating field successfully with REMOVE operation
93 | """
94 | customer_list = CustomerList(
95 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
96 | customer_list_id=self.customer_list_utils.get_customer_list_id(),
97 | client=self.test_client
98 | )
99 |
100 | old_remove_counter = getattr(customer_list, "_num_removed_user_records")
101 | old_batches_counter = getattr(customer_list, "_num_batches")
102 |
103 | customer_list.update_fields(
104 | records="test_remove@pinterest.com",
105 | operation_type="REMOVE",
106 | )
107 |
108 | assert customer_list
109 | assert getattr(customer_list, "_num_removed_user_records") == old_remove_counter+1
110 | assert getattr(customer_list, "_num_batches") == old_batches_counter+1
111 |
112 | def test_update_missing_require_field(self):
113 | """
114 | Test missing required field: records
115 | """
116 | customer_list = CustomerList(
117 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
118 | customer_list_id=self.customer_list_utils.get_customer_list_id(),
119 | client=self.test_client
120 | )
121 |
122 | customer_list_arguments = dict(
123 | operation_type="ADD",
124 | )
125 |
126 | with self.assertRaises(TypeError):
127 | customer_list.update_fields(**customer_list_arguments)
128 |
129 | class TestGetListCustomerList(BaseTestCase):
130 | """
131 | Test get list in CustomerList model
132 | """
133 | @patch('pinterest.ads.customer_lists.CustomerListsApi.customer_lists_get')
134 | def test_get_all(self, customer_lists_get_mock):
135 | # pylint: disable=consider-using-set-comprehension
136 | """
137 | Test if all customer list can be retrived from Ad account
138 | """
139 | NUMBER_OF_NEW_CUSTOMER_LIST = 3
140 | created_customer_list_ids = set(
141 | getattr(self.customer_list_utils.create_new_customer_list(), '_id') \
142 | for _ in range(NUMBER_OF_NEW_CUSTOMER_LIST)
143 | )
144 |
145 | assert len(created_customer_list_ids) == NUMBER_OF_NEW_CUSTOMER_LIST
146 |
147 | customer_lists_list, _ = CustomerList.get_all(
148 | ad_account_id=DEFAULT_AD_ACCOUNT_ID,
149 | order="DESCENDING",
150 | page_size=NUMBER_OF_NEW_CUSTOMER_LIST,
151 | )
152 |
153 | assert customer_lists_get_mock.call_count - 1 == NUMBER_OF_NEW_CUSTOMER_LIST
154 | assert len(created_customer_list_ids) == len(customer_lists_list)
155 |
156 | get_all_customer_lists_ids = set()
157 | for customer_list in customer_lists_list:
158 | get_all_customer_lists_ids.add(getattr(customer_list, '_id'))
159 | assert isinstance(customer_list, CustomerList)
160 |
161 | assert created_customer_list_ids == get_all_customer_lists_ids
162 |
163 |
164 | class TestAddRemoveCustomerList(BaseTestCase):
165 | """
166 | Test add/remove list in CustomerList model
167 | """
168 | def setUp(self) -> None:
169 | super().setUp()
170 | self.record_default = 'EA7583CD-B890-48BC-B542-42ECB2B48606'
171 | self.records_aux = [
172 | 'EA7583CD-B890-48BC-B816-42ECB2B48606',
173 | 'EA7583CD-P667-48BC-B856-42ECB2B48606',
174 | 'EA7583CD-P667-48BC-B866-42ECB2B48606',
175 | ]
176 | self.all_records = self.records_aux + [self.record_default]
177 |
178 | def test_add_customer_list(self):
179 | """
180 | Test add list to CustomerList model
181 | """
182 | customer_list = self.customer_list_utils.create_new_customer_list(
183 | records=self.record_default
184 | )
185 | self.assertEqual(getattr(customer_list, '_num_uploaded_user_records'), 1)
186 |
187 | for record in self.records_aux:
188 | customer_list.add_record(record)
189 |
190 | self.assertEqual(getattr(customer_list, '_num_uploaded_user_records'), len(self.all_records))
191 |
192 | def test_remove_customer_list(self):
193 | """
194 | Test remove list to CustomerList model
195 | """
196 | customer_list = self.customer_list_utils.create_new_customer_list(
197 | records=self.record_default
198 | )
199 | for record in self.records_aux:
200 | customer_list.add_record(record)
201 |
202 | prev_count = getattr(customer_list, '_num_uploaded_user_records')
203 |
204 | for record in self.records_aux:
205 | customer_list.remove_record(record)
206 |
207 | next_count = getattr(customer_list, '_num_removed_user_records')
208 |
209 | self.assertEqual(prev_count, len(self.all_records))
210 | self.assertEqual(next_count, len(self.all_records)-1)
211 |
--------------------------------------------------------------------------------
/docs/pinterest/ads.conversion_tags.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `ads.conversion_tags`
6 | Conversion Class for Pinterest Python SDK
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `ConversionTag`
15 | Conversion Tag model used to view, create, update its attributes and list its different entities
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(
23 | ad_account_id: 'str',
24 | conversion_tag_id: 'str',
25 | client: 'PinterestSDKClient' = None,
26 | **kwargs
27 | ) → None
28 | ```
29 |
30 | Initialize Conversion Tag Object.
31 |
32 |
33 |
34 | **Args:**
35 |
36 | - `ad_account_id` (str): ConversionTag's Ad Account ID
37 | - `conversion_tag_id` (str): ConversionTag ID, must be associated with Ad Account ID provided
38 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided.
39 |
40 |
41 | ---
42 |
43 | #### property ad_account_id
44 |
45 |
46 |
47 |
48 |
49 | ---
50 |
51 | #### property code_snippet
52 |
53 |
54 |
55 |
56 |
57 | ---
58 |
59 | #### property configs
60 |
61 |
62 |
63 |
64 |
65 | ---
66 |
67 | #### property enhanced_match_status
68 |
69 |
70 |
71 |
72 |
73 | ---
74 |
75 | #### property id
76 |
77 |
78 |
79 |
80 |
81 | ---
82 |
83 | #### property last_fired_time_ms
84 |
85 |
86 |
87 |
88 |
89 | ---
90 |
91 | #### property name
92 |
93 |
94 |
95 |
96 |
97 | ---
98 |
99 | #### property status
100 |
101 |
102 |
103 |
104 |
105 | ---
106 |
107 | #### property version
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | ---
116 |
117 |
118 |
119 | ### classmethod `create`
120 |
121 | ```python
122 | create(
123 | ad_account_id: 'str',
124 | name: 'str',
125 | aem_enabled: 'bool' = None,
126 | md_frequency: 'float' = None,
127 | aem_fnln_enabled: 'bool' = None,
128 | aem_ph_enabled: 'bool' = None,
129 | aem_ge_enabled: 'bool' = None,
130 | aem_db_enabled: 'bool' = None,
131 | aem_loc_enabled: 'bool' = None,
132 | client: 'PinterestSDKClient' = None,
133 | **kwargs
134 | ) → ConversionTag
135 | ```
136 |
137 | Create a conversion tag, also known as Pinterest tag with the option to enable enhance match.
138 |
139 | The Pinterest Tag tracks actions people take on the ad account’ s website after they view the ad account's ad on Pinterest. The advertiser needs to customize this tag to track conversions.
140 |
141 | For more information, see:
142 |
143 | Set up the Pinterest tag
144 |
145 | Pinterest Tag
146 |
147 | Enhanced match"
148 |
149 |
150 |
151 | **Args:**
152 |
153 | - `ad_account_id` (str): ConversionTag's Ad Account ID
154 | - `name` (str): ConversionTag name
155 | - `aem_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match email is enabled. See Enhanced match for more information.
156 |
157 |
158 | - `md_frequency` (float=1.0, Nullable): Metadata ingestion frequency.
159 | - `aem_fnln_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match name is enabled. See Enhanced match for more information.
160 |
161 |
162 | - `aem_ph_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match phone is enabled. See Enhanced match for more information.
163 |
164 |
165 | - `aem_ge_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match gender is enabled. See Enhanced match for more information.
166 |
167 |
168 | - `aem_db_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match birthdate is enabled. See Enhanced match for more information.
169 |
170 |
171 | - `aem_loc_enabled` (bool=False, Nullable): Whether Automatic Enhanced Match location is enabled. See Enhanced match for more information.
172 |
173 |
174 |
175 | **Returns:**
176 |
177 | - `ConversionTag`: ConversionTag Object
178 |
179 | ---
180 |
181 |
182 |
183 | ### classmethod `get_all`
184 |
185 | ```python
186 | get_all(
187 | ad_account_id: 'str',
188 | filter_deleted: 'bool' = False,
189 | client: 'PinterestSDKClient' = None,
190 | **kwargs
191 | ) → list[ConversionTag]
192 | ```
193 |
194 | Get a list of ConversionTag, filter by specified arguments
195 |
196 |
197 |
198 | **Args:**
199 |
200 | - `ad_account_id` (str): Unique identifier of an ad account.
201 | - `filter_deleted` (bool=False, optional): Filter out deleted tags.
202 | - `client` (_type_, optional): PinterestSDKClient Object. Uses the default client, if not provided.
203 |
204 |
205 |
206 | **Returns:**
207 |
208 | - `list[ConversionTag]`: List of ConversionTags
209 |
210 | ---
211 |
212 |
213 |
214 | ### classmethod `get_ocpm_eligible_conversion_tag_events`
215 |
216 | ```python
217 | get_ocpm_eligible_conversion_tag_events(
218 | ad_account_id: 'str',
219 | client: 'PinterestSDKClient' = None,
220 | **kwargs
221 | ) → tuple[str, list[ConversionEventResponse]]
222 | ```
223 |
224 | Get OCPM eligible conversion tag events for an Ad Account.
225 |
226 |
227 |
228 | **Args:**
229 |
230 | - `ad_account_id` (str): Ad Account ID
231 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided.
232 |
233 |
234 |
235 | **Returns:**
236 |
237 | - `list[ConversionEventResponse]`: List of ConversionTagEvent
238 |
239 | ---
240 |
241 |
242 |
243 | ### classmethod `get_page_visit_conversion_tag_events`
244 |
245 | ```python
246 | get_page_visit_conversion_tag_events(
247 | ad_account_id: 'str',
248 | page_size: 'int' = None,
249 | order: 'str' = 'ASCENDING',
250 | bookmark: 'str' = None,
251 | client: 'PinterestSDKClient' = None,
252 | **kwargs
253 | ) → tuple[list[ConversionEventResponse], Bookmark]
254 | ```
255 |
256 | Get page visit conversion tag events for an ad account.
257 |
258 |
259 |
260 | **Args:**
261 |
262 | - `ad_account` (str): Ad Account ID
263 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object. Uses the default client, if not provided.
264 |
265 |
266 |
267 | **Returns:**
268 |
269 | - `list[ConversionEventResponse]`: List of ConversionTagEvent
270 |
271 |
272 |
--------------------------------------------------------------------------------
/pinterest/ads/keywords.py:
--------------------------------------------------------------------------------
1 | """
2 | High level module class for Keyword object
3 | """
4 | from __future__ import annotations
5 |
6 | from openapi_generated.pinterest_client.api.keywords_api import KeywordsApi
7 | from openapi_generated.pinterest_client.model.match_type import MatchType
8 | from openapi_generated.pinterest_client.model.keywords_common import KeywordsCommon
9 | from openapi_generated.pinterest_client.model.keywords_request import KeywordsRequest
10 | from openapi_generated.pinterest_client.model.keyword import Keyword as GeneratedKeyword
11 | from openapi_generated.pinterest_client.model.match_type_response import MatchTypeResponse
12 | from openapi_generated.pinterest_client.model.keyword_update_body import KeywordUpdateBody
13 | from openapi_generated.pinterest_client.model.keyword_update import KeywordUpdate
14 |
15 | from pinterest.client import PinterestSDKClient
16 | from pinterest.utils.sdk_exceptions import SdkException
17 | from pinterest.utils.base_model import PinterestBaseModel
18 |
19 |
20 | class Keyword(PinterestBaseModel):
21 | # pylint: disable=too-few-public-methods,too-many-locals,too-many-arguments
22 | """
23 | High level model class to manage keywords
24 | """
25 | def __init__(self, ad_account_id, keyword_id, client=None, **kwargs):
26 |
27 | self._match_type = None
28 | self._value = None
29 | self._archived = None
30 | self._id = None
31 | self._parent_id = None
32 | self._parent_type = None
33 | self._type = None
34 | self._bid = None
35 |
36 | PinterestBaseModel.__init__(
37 | self,
38 | _id=str(keyword_id),
39 | generated_api=KeywordsApi,
40 | generated_api_get_fn="",
41 | generated_api_get_fn_args={},
42 | model_attribute_types=GeneratedKeyword.openapi_types,
43 | client=client,
44 | )
45 | self._ad_account_id = str(ad_account_id)
46 | self._populate_fields(**kwargs)
47 |
48 | @property
49 | def match_type(self) -> MatchTypeResponse:
50 | # pylint: disable=missing-function-docstring
51 | return self._match_type
52 |
53 | @property
54 | def value(self) -> str:
55 | # pylint: disable=missing-function-docstring
56 | return self._value
57 |
58 | @property
59 | def archived(self) -> bool:
60 | # pylint: disable=missing-function-docstring
61 | return self._archived
62 |
63 | @property
64 | def id(self) -> str:
65 | # pylint: disable=missing-function-docstring
66 | return self._id
67 |
68 | @property
69 | def parent_id(self) -> str:
70 | # pylint: disable=missing-function-docstring
71 | return self._parent_id
72 |
73 | @property
74 | def parent_type(self) -> str:
75 | # pylint: disable=missing-function-docstring
76 | return self._parent_type
77 |
78 | @property
79 | def type(self) -> str:
80 | # pylint: disable=missing-function-docstring
81 | return self._type
82 |
83 | @property
84 | def bid(self) -> int:
85 | # pylint: disable=missing-function-docstring
86 | return self._bid
87 |
88 | @classmethod
89 | def create(
90 | cls,
91 | ad_account_id : str,
92 | parent_id : str,
93 | value : str,
94 | bid: int = None,
95 | match_type : str = None,
96 | client: PinterestSDKClient = None,
97 | **kwargs
98 | ) -> Keyword:
99 | # pylint: disable=too-many-locals,too-many-arguments
100 | """
101 | Create keywords for follow entity types (advertiser, campaign, ad group or ad).
102 |
103 | NOTE:
104 | - Advertisers campaigns can only be assigned keywords with excluding ('_NEGATIVE').
105 | - All keyword match types are available for ad groups.
106 |
107 | Args:
108 | ad_account_id (str): Ad Account ID
109 | parent_id (str): Keyword parent entity ID (advertiser, campaign, ad group)
110 | bid (float): Keyword custom bid
111 | match_type (str): Keyword match type, ENUM: "BOARD", "PHRASE", "EXACT", "EXACT_NEGATIVE",
112 | "PHRASE_NEGATIVE", null
113 | value (str): Keyword value(120 chars max)
114 |
115 | Returns:
116 | Keyword: Keyword Object
117 | """
118 | # pylint: disable=line-too-long
119 |
120 | def map_fn(obj):
121 | if len(obj.keywords) == 0:
122 | raise SdkException(
123 | status=f"Fail with error: {obj.errors[0].error_messages}",
124 | )
125 | return obj.keywords[0]
126 |
127 | keyword = KeywordsCommon(
128 | match_type=MatchTypeResponse(match_type),
129 | value=value,
130 | bid=bid,
131 | **kwargs
132 | )
133 |
134 | response = cls._create(
135 | params={
136 | "ad_account_id": str(ad_account_id),
137 | "keywords_request": KeywordsRequest(
138 | keywords=[keyword],
139 | parent_id=str(parent_id),
140 | ),
141 | },
142 | api=KeywordsApi,
143 | create_fn=KeywordsApi.keywords_create,
144 | map_fn=map_fn,
145 | client=cls._get_client(client),
146 | )
147 |
148 | return cls(
149 | ad_account_id=ad_account_id,
150 | keyword_id=response.id,
151 | client=cls._get_client(client),
152 | _model_data=response.to_dict()
153 | )
154 |
155 | @classmethod
156 | def get_all(
157 | cls,
158 | ad_account_id: str,
159 | page_size: int = None,
160 | bookmark : str = None,
161 | client: PinterestSDKClient = None,
162 | **kwargs
163 | ) -> tuple[list[Keyword], str]:
164 | """
165 | Get a list of keywords bases on the filters provided.
166 |
167 | NOTE:
168 | - Advertisers campaigns can only be assigned keywords with excluding ('_NEGATIVE').
169 | - All keyword match types are available for ad groups.
170 |
171 | Args:
172 | ad_account_id (str): Ad Account ID.
173 | page_size (int[1..100], optional): Maximum number of items to include in a single page of the response.
174 | See documentation on Pagination for more information. Defaults to None which will
175 | return all campaigns.
176 | bookmark (str, optional): Cursor used to fetch the next page of items. Defaults to None.
177 | client (PinterestSDKClient, optional): Defaults to the default api client.
178 |
179 | Returns:
180 | list[Keyword]: List of Keyword Objects
181 | str: Bookmark for paginations if present, else None.
182 | """
183 | params = {"ad_account_id": ad_account_id}
184 |
185 | if "match_type" in kwargs:
186 | kwargs["match_type"] = MatchType(kwargs["match_type"])
187 |
188 | def _map_function(obj):
189 | return Keyword(
190 | ad_account_id=ad_account_id,
191 | keyword_id=obj.get('id'),
192 | client=client,
193 | _model_data=obj,
194 | )
195 |
196 | return cls._list(
197 | params=params,
198 | page_size=page_size,
199 | bookmark=bookmark,
200 | api=KeywordsApi,
201 | list_fn=KeywordsApi.keywords_get,
202 | map_fn=_map_function,
203 | client=client,
204 | **kwargs
205 | )
206 |
207 | def update_fields(self, **kwargs) -> bool:
208 | """Update keyword fields using any attributes
209 |
210 | Keyword Args:
211 | Any valid keyword fields with valid values
212 |
213 | Returns:
214 | bool: if keyword fields were successfully updated
215 | """
216 | # TODO(dfana@): replace this method logic with PinterestBaseModel._update, right now is not supported this logic
217 | api_response = self._generated_api.keywords_update(
218 | ad_account_id=self._ad_account_id,
219 | keyword_update_body=KeywordUpdateBody(
220 | keywords=[KeywordUpdate(
221 | id=self._id,
222 | **kwargs
223 | )]
224 | )
225 | )
226 |
227 | assert isinstance(api_response.keywords[0], GeneratedKeyword)
228 | self._populate_fields(_model_data=api_response.keywords[0].to_dict())
229 |
230 | return True
231 |
--------------------------------------------------------------------------------
/docs/pinterest/ads.customer_lists.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # module `ads.customer_lists`
6 | High level module class for Customer List object
7 |
8 |
9 |
10 | ---
11 |
12 |
13 |
14 | ## class `CustomerList`
15 | High level model class to manage customer_lists for an CustomerList
16 |
17 |
18 |
19 | ### method `__init__`
20 |
21 | ```python
22 | __init__(ad_account_id, customer_list_id, client=None, **kwargs)
23 | ```
24 |
25 |
26 |
27 |
28 |
29 |
30 | ---
31 |
32 | #### property ad_account_id
33 |
34 |
35 |
36 |
37 |
38 | ---
39 |
40 | #### property created_time
41 |
42 |
43 |
44 |
45 |
46 | ---
47 |
48 | #### property exceptions
49 |
50 |
51 |
52 |
53 |
54 | ---
55 |
56 | #### property id
57 |
58 |
59 |
60 |
61 |
62 | ---
63 |
64 | #### property name
65 |
66 |
67 |
68 |
69 |
70 | ---
71 |
72 | #### property num_batches
73 |
74 |
75 |
76 |
77 |
78 | ---
79 |
80 | #### property num_removed_user_records
81 |
82 |
83 |
84 |
85 |
86 | ---
87 |
88 | #### property num_uploaded_user_records
89 |
90 |
91 |
92 |
93 |
94 | ---
95 |
96 | #### property status
97 |
98 |
99 |
100 |
101 |
102 | ---
103 |
104 | #### property type
105 |
106 |
107 |
108 |
109 |
110 | ---
111 |
112 | #### property updated_time
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ---
121 |
122 |
123 |
124 | ### method `add_record`
125 |
126 | ```python
127 | add_record(record)
128 | ```
129 |
130 | Add records to an existing customer list, the system scans the additions for existing Pinterest accounts; those are the records that will be added to your “CUSTOMER_LIST” audience.
131 |
132 | Your original list of records to add will be deleted when the matching process is complete.
133 |
134 |
135 |
136 | **Args:**
137 |
138 | - `record` (str): Records list. Can be any combination of emails, MAIDs, or IDFAs. Emails must be lowercase and can be plain text or hashed using SHA1, SHA256, or MD5. MAIDs and IDFAs must be hashed with SHA1, SHA256, or MD5.
139 |
140 |
141 |
142 | **Returns:**
143 |
144 | - `bool`: If record was added to the customer list fields were successfully updated
145 |
146 | ---
147 |
148 |
149 |
150 | ### classmethod `create`
151 |
152 | ```python
153 | create(
154 | ad_account_id: 'str',
155 | name: 'str',
156 | records: 'str',
157 | list_type: 'str' = 'EMAIL',
158 | client: 'PinterestSDKClient' = None,
159 | **kwargs
160 | )
161 | ```
162 |
163 | Create a customer list from your records(hashed or plain-text email addresses, or hashed MAIDs or IDFAs).
164 |
165 | A customer list is one of the four types of Pinterest audiences: for more information, see Audience targeting or the Audiences section of the ads management guide.
166 |
167 | Please review our requirements for what type of information is allowed when uploading a customer list.
168 |
169 | When you create a customer list, the system scans the list for existing Pinterest accounts; the list must include at least 100 Pinterest accounts. Your original list will be deleted when the matching process is complete. The filtered list – containing only the Pinterest accounts that were included in your starting list – is what will be used to create the audience.
170 |
171 | Note that once you have created your customer list, you must convert it into an audience (of the “CUSTOMER_LIST” type) using the create audience endpoint before it can be used.
172 |
173 |
174 |
175 |
176 |
177 | **Args:**
178 |
179 | - `ad_account_id` (str): Unique identifier of an ad account.
180 | - `name` (str): Customer list name.
181 | - `records` (str): Records list. Can be any combination of emails, MAIDs, or IDFAs. Emails must be lowercase and can be plain text or hashed using SHA1, SHA256, or MD5. MAIDs and IDFAs must be hashed with SHA1, SHA256, or MD5.
182 | - `list_type` (str, optional): User list type. Possible Enum: "EMAIL" "IDFA" "MAID" "LR_ID" "DLX_ID" "HASHED_PINNER_ID". Defaults to "EMAIL".
183 | - `client` (PinterestSDKClient, optional): Defaults to default_api_client.
184 |
185 | Keyword Args: Any valid keyword arguments or query parameters for endpoint.
186 |
187 |
188 |
189 | **Returns:**
190 |
191 | - `CustomerList`: CustomerList object
192 |
193 | ---
194 |
195 |
196 |
197 | ### classmethod `get_all`
198 |
199 | ```python
200 | get_all(
201 | ad_account_id: 'str',
202 | page_size: 'int' = None,
203 | order: 'str' = None,
204 | bookmark: 'str' = None,
205 | client: 'PinterestSDKClient' = None,
206 | **kwargs
207 | ) → tuple[list[CustomerList], Bookmark]
208 | ```
209 |
210 | Get a list of the customer lists in the AdAccount, filtered by the specified arguments
211 |
212 |
213 |
214 | **Args:**
215 |
216 | - `ad_account_id` (str): Campaign's Ad Account ID.
217 | - `page_size` (int, 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 customer lists.
218 | - `order` (str, optional): _description_. Defaults to None.
219 | - `bookmark` (str, optional): Cursor used to fetch the next page of items. Defaults to None.
220 | - `client` (PinterestSDKClient, optional): PinterestSDKClient Object
221 |
222 | Keyword Args: Any valid keyword arguments or query parameters for endpoint.
223 |
224 |
225 |
226 | **Returns:**
227 |
228 | - `list[CustomerList]`: List of CustomerList Objects
229 | - `Bookmark`: Bookmark for pagination if present, else None.
230 |
231 | ---
232 |
233 |
234 |
235 | ### method `remove_record`
236 |
237 | ```python
238 | remove_record(record)
239 | ```
240 |
241 | Remove records to an existing customer list.
242 |
243 |
244 |
245 | **Args:**
246 |
247 | - `record` (str): Records list. Can be any combination of emails, MAIDs, or IDFAs. Emails must be lowercase and can be plain text or hashed using SHA1, SHA256, or MD5. MAIDs and IDFAs must be hashed with SHA1, SHA256, or MD5.
248 |
249 |
250 |
251 | **Returns:**
252 |
253 | - `bool`: If record was removed to the customer list fields were successfully updated
254 |
255 | ---
256 |
257 |
258 |
259 | ### method `update_fields`
260 |
261 | ```python
262 | update_fields(**kwargs)
263 | ```
264 |
265 | Update customer lists fields with valid values
266 |
267 | Keywords Args: Any valid customer list field with valid value
268 |
269 |
270 |
--------------------------------------------------------------------------------