├── .env.example ├── .flake8 ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── cron_integrationtests.yml │ ├── integrationtests.yml │ ├── lint.yml │ ├── packagetest.yml │ ├── publish-pypi-test.yml │ ├── publish-pypi.yml │ └── unittests.yml ├── .gitignore ├── .pylintrc ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── config.json ├── config.json.example ├── dev-requirements.txt ├── docs ├── pinterest │ ├── .pages │ ├── README.md │ ├── ads.ad_accounts.md │ ├── ads.ad_groups.md │ ├── ads.ads.md │ ├── ads.audiences.md │ ├── ads.campaigns.md │ ├── ads.conversion_events.md │ ├── ads.conversion_tags.md │ ├── ads.customer_lists.md │ ├── ads.keywords.md │ ├── ads.md │ ├── client.md │ ├── organic.boards.md │ ├── organic.md │ ├── organic.pins.md │ ├── utils.base_model.md │ ├── utils.bookmark.md │ ├── utils.error_handling.md │ ├── utils.load_json_config.md │ ├── utils.md │ ├── utils.refresh_access_token.md │ └── utils.sdk_exceptions.md └── utils │ ├── README.md │ ├── python-sdk-doc.yaml │ ├── script.py │ └── skeleton-spec.yaml ├── integration_tests ├── __init__.py ├── ads │ ├── __init__.py │ ├── test_ad_accounts.py │ ├── test_ad_groups.py │ ├── test_ads.py │ ├── test_audiences.py │ ├── test_campaigns.py │ ├── test_conversion_events.py │ ├── test_conversion_tags.py │ ├── test_customer_lists.py │ └── test_keywords.py ├── base_test.py ├── clean_organic_data.py ├── client │ └── test_client.py ├── config.py ├── organic │ ├── test_boards.py │ └── test_pins.py ├── test_pinterest_base_model.py └── utils │ ├── ads_utils.py │ └── organic_utils.py ├── package_test ├── main.py └── run.sh ├── pinterest ├── __init__.py ├── ads │ ├── __init__.py │ ├── ad_accounts.py │ ├── ad_groups.py │ ├── ads.py │ ├── audiences.py │ ├── campaigns.py │ ├── conversion_events.py │ ├── conversion_tags.py │ ├── customer_lists.py │ └── keywords.py ├── bin │ ├── __init__.py │ └── get_config.py ├── client │ └── __init__.py ├── config.py ├── organic │ ├── __init__.py │ ├── boards.py │ └── pins.py ├── utils │ ├── __init__.py │ ├── base_model.py │ ├── bookmark.py │ ├── error_handling.py │ ├── load_json_config.py │ ├── refresh_access_token.py │ └── sdk_exceptions.py └── version.py ├── pytest.ini ├── requirements.txt ├── samples ├── .gitkeep ├── create_ad.py ├── create_adgroup.py ├── create_audience.py ├── create_campaign.py ├── create_customer_list.py └── create_keyword.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── src ├── __init__.py └── pinterest ├── __init__.py ├── ads ├── test_ad_accounts.py ├── test_ad_groups.py ├── test_ads.py ├── test_audiences.py ├── test_campaigns.py ├── test_conversion_events.py ├── test_conversion_tags.py ├── test_customer_lists.py └── test_keywords.py ├── bin └── get_config_test.py ├── client └── client_test.py ├── organic ├── test_boards.py └── test_pins.py └── utils ├── __init__.py ├── test_error_handling.py └── test_load_json_config.py /.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 -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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. -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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/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/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/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 | -------------------------------------------------------------------------------- /.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/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/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 -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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! -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"state":"package_test"} 2 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/.pages: -------------------------------------------------------------------------------- 1 | title: API Reference 2 | nav: 3 | - Overview: README.md 4 | - ... 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/ads.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `ads` 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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/organic.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `organic` 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/pinterest/utils.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `utils` 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /integration_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/integration_tests/__init__.py -------------------------------------------------------------------------------- /integration_tests/ads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/integration_tests/ads/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pinterest/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pinterest SDK for ads 3 | """ 4 | __path__ = __import__("pkgutil").extend_path(__path__, __name__) 5 | -------------------------------------------------------------------------------- /pinterest/ads/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/pinterest/ads/__init__.py -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /pinterest/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/pinterest/bin/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pinterest/organic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/pinterest/organic/__init__.py -------------------------------------------------------------------------------- /pinterest/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/pinterest/utils/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pinterest/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pinterest SDK Packages Version 3 | """ 4 | __version__ = '0.2.5' 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/pytest.ini -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /samples/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/samples/.gitkeep -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=99 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/tests/__init__.py -------------------------------------------------------------------------------- /tests/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/tests/src/__init__.py -------------------------------------------------------------------------------- /tests/src/pinterest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/tests/src/pinterest/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/src/pinterest/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pinterest/pinterest-python-sdk/8ff57779fd4cfa1e916cc30ac285c57f02e5ec81/tests/src/pinterest/utils/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------