├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── create_pin.md ├── pinterest ├── __init__.py ├── __version__.py ├── board.py ├── config.py ├── err.py ├── me.py ├── oauth2.py ├── pin.py ├── section.py ├── user.py └── util.py └── setup.py /.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 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | htmlcov/ 27 | .tox/ 28 | .coverage 29 | .coverage.* 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | *,cover 34 | .hypothesis/ 35 | instance/ 36 | docs/_build/ 37 | target/ 38 | .ipynb_checkpoints 39 | .python-version 40 | .env 41 | venv/ 42 | *.pkl 43 | .* 44 | !.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Bryan Andrade 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Pinterest API 2 | 3 | ![license MIT](https://s3-us-west-1.amazonaws.com/bryand1/images/badges/license-MIT-blue.svg) 4 | ![python 3.6 | 3.7](https://s3-us-west-1.amazonaws.com/bryand1/images/badges/python-3.6-3.7.svg) 5 | 6 | 7 | ## Getting Started 8 | 9 | ```bash 10 | pip install pinterest-api 11 | ``` 12 | 13 | 14 | ## Usage 15 | 16 | ```python 17 | import pinterest 18 | 19 | # Generate OAuth2 authorization link 20 | link = pinterest.oauth2.authorization_url(app_id, redirect_uri) 21 | 22 | # Initialize API by passing OAuth2 token 23 | api = pinterest.Pinterest(token="ApFF9WBrjug_xhJPsETri2jp9pxgFVQfZNayykxFOjJQhWAw") 24 | 25 | # Fetch authenticated user's data 26 | api.me() 27 | 28 | # Fetch authenticated user's boards 29 | api.boards() 30 | 31 | # Create board 32 | api.board().create("Halloween", description="Fun Costumes") 33 | 34 | # Fetch board 35 | api.board("695665542379607495").fetch() 36 | api.board("username/halloween").fetch() 37 | 38 | # Fetch pins on board 39 | api.board("username/halloween").pins() 40 | 41 | # Edit board 42 | api.board("username/halloween").edit(new_name="Costumes", new_description="Halloween Costume Ideas") 43 | 44 | # Delete board 45 | api.board("username/halloween").delete() 46 | 47 | # Fetch board suggestions 48 | api.suggest_boards(pin=162129655315312286) 49 | 50 | # Fetch authenticated user's pins 51 | api.pins() 52 | 53 | # Create a pin 54 | api.pin().create(board, note, link, image_url=image_url) 55 | 56 | # Fetch a pin 57 | api.pin(162129655315312286).fetch() 58 | 59 | # Edit a pin 60 | api.pin(162129655315312286).edit(board, note, link) 61 | 62 | # Delete a pin 63 | api.pin(162129655315312286).delete() 64 | 65 | # Search boards (Optional cursor) 66 | api.search_boards(query, cursor=None) 67 | 68 | # Search pins (Optional cursor) 69 | api.search_pins(query, cursor=None) 70 | 71 | # Follow a board 72 | api.follow_board(board) 73 | 74 | # Follow a user 75 | api.follow_user(username) 76 | 77 | # Return the users who follow the authenticated user 78 | api.followers(cursor=None) 79 | 80 | # Return the boards that the authenticated user follows 81 | api.following_boards(cursor=None) 82 | 83 | # Return the topics the authenticated user follows 84 | api.following_interests(cursor=None) 85 | 86 | # Return the users the authenticated user follows 87 | api.following_users(cursor=None) 88 | 89 | # Unfollow board 90 | api.unfollow_board(board) 91 | 92 | # Make authenticated user unfollow user 93 | api.unfollow_user(username) 94 | 95 | # Fetch another user's info 96 | api.user(username) 97 | 98 | # Fetch board sections 99 | api.board("695665542379586148").sections() 100 | 101 | # Create board section 102 | api.board("695665542379586148").section("Section Title").create() 103 | 104 | # Delete board section 105 | api.board("695665542379586148").section("4989415010584246390").delete() 106 | 107 | # Fetch pins in board section 108 | api.board("695665542379586148").section("4989343507360527350").pins() 109 | ``` 110 | 111 | 112 | ## Responses 113 | 114 | The Pinterest API responses are in JSON format. 115 | 116 | ```python 117 | api.me() # By default, retry http request up to 3 times 118 | ``` 119 | 120 | ```javascript 121 | { 122 | "data": { 123 | "first_name": "Bryan", 124 | "id": "695665611098925391", 125 | "last_name": "Andrade", 126 | "url': "https://www.pinterest.com/bandrade1815/" 127 | }, 128 | "ratelimit": { 129 | "limit": 10, 130 | "remaining": 9 131 | } 132 | } 133 | ``` 134 | 135 | 136 | ## Resources 137 | 138 | [Pinterest Developer API](https://developers.pinterest.com/docs/getting-started/introduction/) 139 | [Pinterest API Explorer](https://developers.pinterest.com/tools/api-explorer/) 140 | -------------------------------------------------------------------------------- /docs/create_pin.md: -------------------------------------------------------------------------------- 1 | # Create Pin 2 | 3 | 4 | Pinterest allows three different ways to post images: 5 | 1. **image** - file handle to image on local filesystem 6 | 2. **image_url** 7 | 3. **image_base64** - base64 encoded string 8 | 9 | 10 | ### Usage 11 | 12 | ```python 13 | import base64 14 | import pinterest 15 | 16 | api = pinterest.Pinterest(token="ApFF9WBrjug_xhJPsETri2jp9pxgFVQfZNayykxFOjJQhWAw") 17 | 18 | # Create pin using image on local filesystem 19 | api.pin().create( 20 | 'bryand1/technology', # 21 | 'Git data structures', # note 22 | 'https://github.com', # the pin will link to this url 23 | image='./images/git.png' # available on local filesystem 24 | ) 25 | 26 | # Create pin using image url 27 | api.pin().create( 28 | 'bryand1/technology', # 29 | 'Git data structures', # note 30 | 'https://github.com', # the pin will link to this url 31 | image_url='https://s3.amazonaws.com/bryand1/images/git.png' 32 | ) 33 | 34 | # Create pin using base64 encoded image 35 | with open('./images/git.png', 'rb') as fh: 36 | image_base64 = base64.b64encode(fh.read()).decode() 37 | api.pin().create( 38 | 'bryand1/technology', # 39 | 'Git data structures', # note 40 | 'https://github.com', # the pin will link to this url 41 | image_base64=image_base64 42 | ) 43 | 44 | ``` 45 | 46 | 47 | ### Response 48 | 49 | ```javascript 50 | { 51 | "data": { 52 | "id": "695665473666621315", 53 | "link": "https://www.pinterest.com/r/pin/695665473666621315/4989327273560649264/a248f805acfbde23a36cbc0a6cbbb7cfa5377fcb8f5e647a9104801c75e99cbb", 54 | "note": "Git data structures", 55 | "url": "https://www.pinterest.com/pin/695665473666621315/" 56 | }, 57 | "ratelimit": { 58 | "limit": 10, 59 | "remaining": 9 60 | } 61 | } 62 | 63 | ``` 64 | 65 | 66 | ### Resources 67 | 68 | [Pinterest API - Pins](https://developers.pinterest.com/docs/api/pins/) 69 | [Base64](https://en.wikipedia.org/wiki/Base64) 70 | -------------------------------------------------------------------------------- /pinterest/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from .board import Board 4 | from .err import PinterestException, PinterestHttpException 5 | from .me import Me 6 | from . import oauth2 7 | from .pin import Pin 8 | from .user import User 9 | 10 | 11 | class Pinterest: 12 | 13 | def __init__(self, token: str): 14 | self.token = token 15 | self._me = Me(self.token) 16 | 17 | def me(self, fields: List[str] = None) -> Dict: 18 | """Return authenticated user's information""" 19 | return self._me(fields=fields) 20 | 21 | def user(self, username: str, fields: List[str] = None) -> Dict: 22 | """Return a user's information""" 23 | return User(self.token, username).fetch(fields=fields) 24 | 25 | def board(self, identifier: str = None) -> Board: 26 | return Board(self.token, identifier=identifier) 27 | 28 | def pin(self, pin_id: str = None) -> Pin: 29 | return Pin(self.token, pin_id=pin_id) 30 | 31 | def __getattr__(self, attr): 32 | """Dispatch methods from Me() dynamically""" 33 | if hasattr(self._me, attr): 34 | def wrapper(*args, **kwargs): 35 | return getattr(self._me, attr)(*args, **kwargs) 36 | return wrapper 37 | -------------------------------------------------------------------------------- /pinterest/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'pinterest-api' 2 | __description__ = 'Pinterest API client' 3 | __url__ = 'https://github.com/bryand1/python-pinterest-api' 4 | __version__ = '0.0.8' 5 | __author__ = 'Bryan Andrade' 6 | __author_email__ = 'me@bryanandrade.com' 7 | __license__ = 'MIT' 8 | __copyright__ = 'Copyright 2018 Bryan Andrade' 9 | -------------------------------------------------------------------------------- /pinterest/board.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from . import config 4 | from .err import PinterestException 5 | from .section import Section 6 | from .util import pinterest_request 7 | 8 | 9 | class Board: 10 | 11 | def __init__(self, token: str, identifier: str = None): 12 | """identifier uses board:spec format '/' or string of integers 695665542379586148""" 13 | self.token = token 14 | self.identifier = identifier 15 | 16 | def create(self, name: str, description: str = None, fields: List[str] = None) -> Dict: 17 | """ 18 | Creates a board for the authenticated user. 19 | 20 | The default response returns the ID, URL and name of the created board. 21 | 22 | fields: counts, created_at, creator, description, id, image, name, privacy, reason, url 23 | 24 | POST/v1/boards/ 25 | """ 26 | if fields is None: 27 | fields = ['name', 'id', 'url'] 28 | url = config.api_url + "/v1/boards/" 29 | params = { 30 | 'access_token': self.token, 31 | 'fields': ','.join(fields) 32 | } 33 | data = { 34 | "name": name, 35 | "description": description, 36 | } 37 | return pinterest_request('post', url, params=params, data=data) 38 | 39 | def fetch(self, fields: List[str] = None) -> Dict: 40 | """ 41 | The default response returns the ID, URL and name of the specified board. 42 | 43 | GET /v1/boards// 44 | """ 45 | if fields is None: 46 | fields = ['id', 'url', 'name'] 47 | url = config.api_url + "/v1/boards/{identifier}/".format(identifier=self.identifier) 48 | params = {'access_token': self.token, 'fields': ','.join(fields)} 49 | return pinterest_request('get', url, params=params) 50 | 51 | def pins(self, fields: List[str] = None, cursor: str = None, limit: int = None) -> Dict: 52 | """The default response returns a list of ordered Pins on the board with their ID, URL, link and description. 53 | 54 | Pins are ordered from most recently created to least recent. 55 | 56 | fields: attribution, board, color, counts, created_at, creator, id, 57 | image, link, media, metadata, note, original_link, url 58 | 59 | GET /v1/boards//pins/ 60 | """ 61 | if fields is None: 62 | fields = ['id', 'url', 'link', 'note'] 63 | url = config.api_url + "/v1/boards/{identifier}/pins/".format(identifier=self.identifier) 64 | params = {'access_token': self.token, 'fields': ','.join(fields)} 65 | if cursor is not None: 66 | params['cursor'] = cursor 67 | if limit is not None: 68 | params['limit'] = limit 69 | return pinterest_request('get', url, params=params) 70 | 71 | def edit(self, new_name: str = None, new_description: str = None, fields: List[str] = None) -> Dict: 72 | """Changes the chosen board’s name and/or description. 73 | 74 | The default response returns the ID, URL and name of the edited board. 75 | 76 | fields: counts, created_at, creator, description, id, image, name, privacy, reason, url 77 | 78 | PATCH /v1/boards// 79 | """ 80 | if new_name is None and new_description is None: 81 | raise PinterestException("Board: edit() requires valid name or description") 82 | if fields is None: 83 | fields = ['id', 'name', 'url'] 84 | url = config.api_url + '/v1/boards/{identifier}/'.format(identifier=self.identifier) 85 | params = {'access_token': self.token, 'fields': ','.join(fields)} 86 | data = {"name": new_name, "description": new_description} 87 | return pinterest_request('patch', url, params=params, data=data) 88 | 89 | def delete(self) -> Dict: 90 | """ 91 | Deletes the specified board. This action is permanent and cannot be undone. 92 | 93 | DELETE /v1/boards// 94 | """ 95 | if self.identifier is None: 96 | raise PinterestException("Board: delete() requires valid identifier") 97 | url = config.api_url + "/v1/boards/{identifier}/".format(identifier=self.identifier) 98 | params = {'access_token': self.token} 99 | return pinterest_request('delete', url, params=params) 100 | 101 | def sections(self, cursor: str = None) -> Dict: 102 | """ 103 | Gets sections for a board. 104 | 105 | GET /v1/board//sections/ 106 | """ 107 | if self.identifier is None: 108 | raise PinterestException("Board: sections() requires valid identifier") 109 | url = config.api_url + '/v1/board/{identifier}/sections/'.format(identifier=self.identifier) 110 | params = {'access_token': self.token} 111 | if cursor is not None: 112 | params['cursor'] = cursor 113 | return pinterest_request('get', url, params=params) 114 | 115 | def section(self, identifier: str = None) -> Section: 116 | return Section(self.token, self.identifier, identifier=identifier) 117 | -------------------------------------------------------------------------------- /pinterest/config.py: -------------------------------------------------------------------------------- 1 | api_url = "https://api.pinterest.com" 2 | -------------------------------------------------------------------------------- /pinterest/err.py: -------------------------------------------------------------------------------- 1 | class PinterestException(Exception): 2 | """""" 3 | 4 | 5 | class PinterestHttpException(PinterestException): 6 | 7 | def __init__(self, code, url, message: str = None): 8 | super().__init__() 9 | self.code = code 10 | self.url = url 11 | self.message = message or ' ' 12 | 13 | def __str__(self): 14 | return "PinterestHttpException: [{code}] {url} {message}".format( 15 | code=self.code, url=self.url, message=self.message) 16 | 17 | def __repr__(self): 18 | return "".format(code=self.code) 19 | -------------------------------------------------------------------------------- /pinterest/me.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from . import config 4 | from .util import pinterest_request 5 | 6 | 7 | class Me: 8 | 9 | def __init__(self, token): 10 | self.token = token 11 | 12 | def boards(self, fields: List[str] = None) -> Dict: 13 | """ 14 | The default response returns an ordered list of the authenticated user’s public boards, 15 | including the URL, ID and name. The boards are sorted first by user order, then by creation date, 16 | with the most recently created boards being first. 17 | 18 | fields: counts, created_at, creator, description, id, image, name, privacy, reason, url 19 | 20 | GET /v1/me/boards/ 21 | """ 22 | if fields is None: 23 | fields = ['url', 'id', 'name'] 24 | url = config.api_url + '/v1/me/boards/' 25 | params = {'access_token': self.token, 'fields': ','.join(fields)} 26 | return pinterest_request('get', url, params=params) 27 | 28 | def suggest_boards(self, pin_id) -> Dict: 29 | """ 30 | Returns the boards that Pinterest would suggest to the authenticated user if they were 31 | to save the specified Pin. The default response returns the ID, URL and name of the boards. 32 | 33 | GET /v1/me/boards/suggested/ 34 | """ 35 | url = config.api_url + '/v1/me/boards/suggested/' 36 | params = {'access_token': self.token, 'pin': pin_id} 37 | return pinterest_request('get', url, params=params) 38 | 39 | def pins(self, fields: List[str] = None, cursor: str = None, limit: int = None) -> Dict: 40 | """ 41 | The default response returns the ID, link, URL and descriptions of the authenticated user’s Pins. 42 | 43 | GET /v1/me/pins/ 44 | """ 45 | if fields is None: 46 | fields = ['id', 'link', 'url'] 47 | url = config.api_url + '/v1/me/pins/' 48 | params = {'access_token': self.token, 'fields': ','.join(fields)} 49 | if cursor is not None: 50 | params['cursor'] = cursor 51 | if limit is not None: 52 | params['limit'] = limit 53 | return pinterest_request('get', url, params=params) 54 | 55 | def search_boards(self, query: str, cursor: str = None, limit: int = None) -> Dict: 56 | """ 57 | Searches the authenticated user’s board names (but not Pins on boards). 58 | An empty response indicates that nothing was found that matched your search terms. 59 | The default response returns the ID, name and URL of boards matching your query. 60 | 61 | GET /v1/me/search/boards/ 62 | """ 63 | url = config.api_url + '/v1/me/search/boards/' 64 | params = {'access_token': self.token, 'query': query} 65 | if cursor is not None: 66 | params['cursor'] = cursor 67 | if limit is not None: 68 | params['limit'] = limit 69 | return pinterest_request('get', url, params=params) 70 | 71 | def search_pins(self, query: str, cursor: str = None, limit: int = None) -> Dict: 72 | """ 73 | Searches the authenticated user’s Pin descriptions. 74 | An empty response indicates that nothing was found that matched your search terms. 75 | The default response returns the ID, link, URL and description of Pins matching your query. 76 | 77 | GET /v1/me/search/pins/ 78 | """ 79 | url = config.api_url + '/v1/me/search/pins/' 80 | params = {'access_token': self.token, 'query': query} 81 | if cursor is not None: 82 | params['cursor'] = cursor 83 | if limit is not None: 84 | params['limit'] = limit 85 | return pinterest_request('get', url, params=params) 86 | 87 | def follow_board(self, board) -> Dict: 88 | """ 89 | Makes the authenticated user follow the specified board. 90 | A empty response (or lack of error code) indicates success. 91 | 92 | POST /v1/me/following/boards/ 93 | """ 94 | url = config.api_url + '/v1/me/following/boards/' 95 | data = {'access_token': self.token, 'board': board} 96 | return pinterest_request('post', url, data=data) 97 | 98 | def follow_user(self, user) -> Dict: 99 | """ 100 | Makes the authenticated user follow the specified user. 101 | A empty response (or lack of error code) indicates success. 102 | 103 | POST /v1/me/following/users/ 104 | """ 105 | url = config.api_url + '/v1/me/following/users/' 106 | data = {'access_token': self.token, 'user': user} 107 | return pinterest_request('post', url, data=data) 108 | 109 | def followers(self, cursor: str = None, limit: int = None) -> Dict: 110 | """ 111 | Returns the users who follow the authenticated user. 112 | The default response returns the first and last name, ID and URL of the users. 113 | 114 | GET /v1/me/followers/ 115 | """ 116 | url = config.api_url + '/v1/me/followers/' 117 | params = {'access_token': self.token} 118 | if cursor is not None: 119 | params['cursor'] = cursor 120 | if limit is not None: 121 | params['limit'] = limit 122 | return pinterest_request('get', url, params=params) 123 | 124 | def following_boards(self, cursor: str = None, limit: int = None) -> Dict: 125 | """ 126 | Returns the boards that the authenticated user follows. 127 | The default response returns the ID, name and URL of the board. 128 | 129 | GET /v1/me/following/boards/ 130 | """ 131 | url = config.api_url + '/v1/me/following/boards/' 132 | params = {'access_token': self.token} 133 | if cursor is not None: 134 | params['cursor'] = cursor 135 | if limit is not None: 136 | params['limit'] = limit 137 | return pinterest_request('get', url, params=params) 138 | 139 | def following_interests(self, fields: List[str] = None, cursor: str = None, limit: int = None) -> Dict: 140 | """ 141 | Returns the topics (e.g, modern architecture, Sherlock) that the authenticated user follows. 142 | The default response returns the ID and name of the topic. 143 | 144 | fields: id, name 145 | 146 | GET /v1/me/following/interests/ 147 | """ 148 | url = config.api_url + '/v1/me/following/interests/' 149 | params = {'access_token': self.token} 150 | if fields is None: 151 | fields = ['id', 'name'] 152 | if cursor is not None: 153 | params['cursor'] = cursor 154 | if limit is not None: 155 | params['limit'] = limit 156 | params['fields'] = ','.join(fields) 157 | return pinterest_request('get', url, params=params) 158 | 159 | def following_users(self, fields: List[str] = None, cursor: str = None, limit: int = None) -> Dict: 160 | """ 161 | Returns the users that the authenticated user follows. 162 | The default response returns the first and last name, ID and URL of the users. 163 | 164 | fields: account_type, bio, counts, created_at, first_name, id, image, last_name, url, username 165 | 166 | GET /v1/me/following/users/ 167 | """ 168 | url = config.api_url + '/v1/me/following/users/' 169 | params = {'access_token': self.token} 170 | if fields is None: 171 | fields = ['first_name', 'id', 'last_name', 'url'] 172 | if cursor is not None: 173 | params['cursor'] = cursor 174 | if limit is not None: 175 | params['limit'] = limit 176 | params['fields'] = ','.join(fields) 177 | return pinterest_request('get', url, params=params) 178 | 179 | def unfollow_board(self, board: str) -> Dict: 180 | """ 181 | Makes the authenticated user unfollow the specified board. 182 | A empty response (or lack of error code) indicates success. 183 | 184 | DELETE /v1/me/following/boards// 185 | """ 186 | url = config.api_url + '/v1/me/following/boards/{board}/'.format(board=board) 187 | data = {'access_token': self.token} 188 | return pinterest_request('delete', url, data=data) 189 | 190 | def unfollow_user(self, user): 191 | """ 192 | Makes the authenticated user unfollow the specified user. 193 | A empty response (or lack of error code) indicates success. 194 | 195 | DELETE /v1/me/following/users// 196 | """ 197 | url = config.api_url + '/v1/me/following/users/{user}/'.format(user=user) 198 | params = {'access_token': self.token} 199 | return pinterest_request('delete', url, params=params) 200 | 201 | def __call__(self, fields: List[str] = None) -> Dict: 202 | """ 203 | The default response returns the first and last name, ID and URL of the authenticated user. 204 | 205 | fields: account_type, bio, counts, created_at, first_name, id, image, last_name, url, username 206 | 207 | GET /v1/me/ 208 | """ 209 | url = config.api_url + '/v1/me/' 210 | params = {'access_token': self.token} 211 | if fields is None: 212 | fields = ['first_name', 'last_name', 'id', 'url'] 213 | params['fields'] = ','.join(fields) 214 | return pinterest_request('get', url, params=params) 215 | -------------------------------------------------------------------------------- /pinterest/oauth2.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import Dict, List 3 | 4 | from urllib.parse import urlencode 5 | 6 | from .util import do_request 7 | 8 | 9 | def authorization_url(app_id: str, redirect_uri: str, scope: List[str] = None) -> str: 10 | """Generate Pinterest OAuth2 authorization url""" 11 | if scope is None: 12 | scope = ['read_public', 'write_public', 'read_relationships', 'write_relationships'] 13 | params = { 14 | 'response_type': 'code', 15 | 'client_id': app_id, 16 | 'redirect_uri': redirect_uri, 17 | 'state': _random_state(), 18 | 'scope': ','.join(scope), 19 | } 20 | return 'https://api.pinterest.com/oauth/?' + urlencode(params) 21 | 22 | 23 | def _random_state(k: int = 8) -> str: 24 | """Produce a random string for use in the Pinterest OAuth2 authorization url""" 25 | population = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 26 | return random.choices(population, k=k) 27 | 28 | 29 | def access_token(app_id: str, app_secret: str, code: str) -> Dict: 30 | """Use code from callback querystring parameter to generate permanent access token 31 | 32 | Sample response: 33 | 34 | { 35 | "access_token": "AjYjREC3BKx...", 36 | "token_type": "bearer", 37 | "scope": ["read_public", "write_public", "read_private", "write_private", 38 | "read_relationships", "write_relationships", "read_write_all"] 39 | } 40 | 41 | """ 42 | params = { 43 | 'grant_type': 'authorization_code', 44 | 'client_id': app_id, 45 | 'client_secret': app_secret, 46 | 'code': code 47 | } 48 | url = 'https://api.pinterest.com/v1/oauth/token?' + urlencode(params) 49 | resp = do_request('post', url) 50 | return resp.json() 51 | -------------------------------------------------------------------------------- /pinterest/pin.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List 3 | 4 | from . import config 5 | from .err import PinterestException 6 | from .util import pinterest_request 7 | 8 | 9 | class Pin: 10 | 11 | def __init__(self, token: str, pin_id: str = None): 12 | self.token = token 13 | self.pin_id = pin_id 14 | 15 | def create(self, board: str, note: str, link: str = None, 16 | image: str = None, image_url: str = None, image_base64: str = None, 17 | fields: List[str] = None) -> Dict: 18 | """ 19 | Creates a Pin for the authenticated user. 20 | The default response returns the note, URL, link and ID of the created Pin. 21 | 22 | board (required): The board you want the new Pin to be on. In the format /. 23 | note (required): The Pin’s description. 24 | link (optional): The URL the Pin will link to when you click through. 25 | 26 | And one of the following three options is required: 27 | image: Upload the image you want to pin using multipart form data. 28 | image_url: The link to the image that you want to Pin. 29 | image_base64: The link of a Base64 encoded image. 30 | 31 | fields: attribution, board, color, counts, created_at, creator, 32 | id, image, link, media, metadata, note, original_link, url 33 | 34 | POST /v1/pins/ 35 | """ 36 | if fields is None: 37 | fields = ['note', 'url', 'link', 'id'] 38 | url = config.api_url + '/v1/pins/' 39 | params = {'access_token': self.token, 'fields': ','.join(fields)} 40 | data = { 41 | 'board': board, 42 | 'note': note, 43 | 'link': link 44 | } 45 | files = {} 46 | if image is not None: 47 | if not os.path.exists(image): 48 | raise PinterestException("Pin: image does not exist") 49 | files['image'] = open(image, 'rb') 50 | elif image_url is not None: 51 | data['image_url'] = image_url 52 | elif image_base64 is not None: 53 | data['image_base64'] = image_base64 54 | else: 55 | raise PinterestException("Pin: create() requires either image, image_url, or image_base64") 56 | return pinterest_request('post', url, params=params, data=data, files=files) 57 | 58 | def fetch(self, fields: List[str] = None) -> Dict: 59 | """ 60 | The default response returns the ID, link, URL and note of the Pin. 61 | 62 | fields: attribution, board, color, counts, created_at, creator, 63 | id, image, link, media, metadata, note, original_link, url 64 | 65 | GET /v1/pins// 66 | """ 67 | if fields is None: 68 | fields = ['id', 'link', 'url', 'note'] 69 | url = config.api_url + '/v1/pins/{pin_id}/'.format(pin_id=self.pin_id) 70 | params = {'access_token': self.token, 'fields': ','.join(fields)} 71 | return pinterest_request('get', url, params=params) 72 | 73 | def edit(self, board: str = None, note: str = None, link: str = None, fields: List[str] = None) -> Dict: 74 | """ 75 | Changes the board, description and/or link of the Pin. 76 | 77 | pin (required): The ID (unique string of numbers and letters) of the Pin you want to edit. 78 | board (optional): The board you want to move the Pin to, in the format /. 79 | note (optional): The new Pin description. 80 | link (optional): The new Pin link. Note: You can only edit the link of a repinned Pin if 81 | the pinner owns the domain of the Pin in question, or if the Pin itself 82 | has been created by the pinner. 83 | 84 | fields: attribution, board, color, counts, created_at, creator, 85 | id, image, link, media, metadata, note, original_link, url 86 | 87 | PATCH /v1/pins// 88 | """ 89 | if board is None and note is None and link is None: 90 | raise PinterestException("Pin: edit() requires valid board, note, or link") 91 | if fields is None: 92 | fields = ['id', 'link', 'url', 'note'] 93 | url = config.api_url + '/v1/pins/{pin_id}/'.format(pin_id=self.pin_id) 94 | params = {'access_token': self.token, 'fields': ','.join(fields)} 95 | data = {} 96 | if board is not None: 97 | data['board'] = board 98 | if note is not None: 99 | data['note'] = note 100 | if link is not None: 101 | data['link'] = link 102 | return pinterest_request('patch', url, params=params, data=data) 103 | 104 | def delete(self) -> Dict: 105 | """ 106 | Deletes the specified Pin. This action is permanent and cannot be undone. 107 | 108 | DELETE /v1/pins// 109 | """ 110 | url = config.api_url + '/v1/pins/{pin_id}/'.format(pin_id=self.pin_id) 111 | params = {'access_token': self.token} 112 | return pinterest_request('delete', url, params=params) 113 | -------------------------------------------------------------------------------- /pinterest/section.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from . import config 4 | from .err import PinterestException 5 | from .util import pinterest_request 6 | 7 | 8 | class Section: 9 | 10 | def __init__(self, token, board: str, identifier: str = None): 11 | self.token = token 12 | self.board = board 13 | self.identifier = identifier 14 | 15 | def create(self) -> Dict: 16 | """ 17 | Creates a section for the authenticated user. The default response returns the ID of the created section. 18 | 19 | PUT /v1/board//sections/ 20 | """ 21 | if self.identifier is None: 22 | raise PinterestException("Section: create() requires valid section title") 23 | url = config.api_url + '/v1/board/{board}/sections/'.format(board=self.board) 24 | params = {'access_token': self.token} 25 | data = {'title': self.identifier} 26 | return pinterest_request('put', url, params=params, data=data) 27 | 28 | def pins(self) -> Dict: 29 | """ 30 | Gets the pins for a board section. 31 | 32 | GET /v1/board/sections//pins/ 33 | """ 34 | if self.identifier is None: 35 | raise PinterestException("Section: pins() requires valid section identifier") 36 | url = config.api_url + '/v1/board/sections/{identifier}/pins/'.format(identifier=self.identifier) 37 | params = {'access_token': self.token} 38 | return pinterest_request('put', url, params=params) 39 | 40 | def delete(self) -> Dict: 41 | """ 42 | Deletes a board section 43 | 44 |
identifier should be a string of integers (e.g. "4989415010584246390") 45 | 46 | DELETE /v1/board/sections/
/ 47 | """ 48 | if self.identifier is None: 49 | raise PinterestException("Section: delete() requires valid section identifier") 50 | url = config.api_url + '/v1/board/sections/{identifier}/'.format(identifier=self.identifier) 51 | params = {'access_token': self.token} 52 | return pinterest_request('delete', url, params=params) 53 | -------------------------------------------------------------------------------- /pinterest/user.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from . import config 4 | from .util import pinterest_request 5 | 6 | 7 | class User: 8 | 9 | def __init__(self, token, username): 10 | self.token = token 11 | self.username = username 12 | 13 | def fetch(self, fields: List[str] = None) -> Dict: 14 | """ 15 | Return a user's information 16 | 17 | fields: account_type, bio, counts, created_at, 18 | first_name, id, image, last_name, url, username 19 | 20 | GET/v1/users// 21 | """ 22 | if fields is None: 23 | fields = ['first_name', 'id', 'last_name', 'url'] 24 | url = config.api_url + '/v1/users/{username}/'.format(username=self.username) 25 | params = {'access_token': self.token, 'fields': ','.join(fields)} 26 | return pinterest_request('get', url, params=params) 27 | -------------------------------------------------------------------------------- /pinterest/util.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | import requests 4 | from requests.adapters import HTTPAdapter 5 | from requests.packages.urllib3.util.retry import Retry 6 | 7 | from .err import PinterestHttpException 8 | 9 | 10 | def _requests_retry_session( 11 | retries=3, 12 | backoff_factor=0.3, 13 | status_forcelist=(500, 502, 504), 14 | session=None, 15 | ) -> requests.Session: 16 | session = session or requests.Session() 17 | retry = Retry( 18 | total=retries, 19 | read=retries, 20 | connect=retries, 21 | backoff_factor=backoff_factor, 22 | status_forcelist=status_forcelist, 23 | ) 24 | adapter = HTTPAdapter(max_retries=retry) 25 | session.mount('http://', adapter) 26 | session.mount('https://', adapter) 27 | return session 28 | 29 | 30 | # Initialize session to improve efficiency 31 | _session = _requests_retry_session() 32 | 33 | 34 | def do_request(req_type: str, url: str, *args, **kwargs) -> requests.Response: 35 | """ 36 | Perform HTTP request 37 | 38 | req_type: Request method (e.g. get, post, patch, delete) 39 | 40 | >> do_request('post', 'https://api.pinterest.com/v1/pins/', data=data) 41 | """ 42 | session = _session if _session else _requests_retry_session() 43 | resp = getattr(session, req_type)(url, *args, **kwargs) 44 | if resp.status_code // 100 != 2: 45 | raise PinterestHttpException(resp.status_code, resp.url, message=resp.text) 46 | return resp 47 | 48 | 49 | def accept(resp: requests.Response) -> Dict: 50 | """ 51 | Receive response and convert to JSON. Extract Pinterest API headers: 52 | 53 | + x-ratelimit-remaining 54 | + x-ratelimit-limit 55 | """ 56 | ret = resp.json() 57 | ret['ratelimit'] = { 58 | 'remaining': cast_int(resp.headers.get('x-ratelimit-remaining')), 59 | 'limit': cast_int(resp.headers.get('x-ratelimit-limit')) 60 | } 61 | return ret 62 | 63 | 64 | def pinterest_request(*args, **kwargs) -> Dict: 65 | """Query Pinterest API and return JSON response with ratelimit info""" 66 | return accept(do_request(*args, **kwargs)) 67 | 68 | 69 | def cast_int(s: Optional[str]) -> Optional[int]: 70 | return int(s) if s is not None else None 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import setup, Command 13 | 14 | NAME = 'pinterest-api' 15 | PACKAGE = 'pinterest' 16 | DESCRIPTION = 'Pinterest API client' 17 | URL = 'https://github.com/bryand1/python-pinterest-api' 18 | EMAIL = 'me@bryanandrade.com' 19 | AUTHOR = 'Bryan Andrade' 20 | REQUIRES_PYTHON = '>=3.6.0' 21 | REQUIRED = ['requests'] 22 | 23 | here = os.path.abspath(os.path.dirname(__file__)) 24 | 25 | try: 26 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 27 | long_description = '\n' + f.read() 28 | except FileNotFoundError: 29 | long_description = DESCRIPTION 30 | 31 | about = {} 32 | with open(os.path.join(here, PACKAGE, '__version__.py')) as f: 33 | exec(f.read(), about) 34 | 35 | 36 | class UploadCommand(Command): 37 | """Support setup.py upload.""" 38 | 39 | description = 'Build and publish the package.' 40 | user_options = [] 41 | 42 | @staticmethod 43 | def status(s): 44 | """Prints things in bold.""" 45 | print('\033[1m{0}\033[0m'.format(s)) 46 | 47 | def initialize_options(self): 48 | pass 49 | 50 | def finalize_options(self): 51 | pass 52 | 53 | def run(self): 54 | try: 55 | self.status('Removing previous builds…') 56 | rmtree(os.path.join(here, 'dist')) 57 | except OSError: 58 | pass 59 | 60 | self.status('Building Source and Wheel (universal) distribution…') 61 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 62 | 63 | self.status('Uploading the package to PyPI via Twine…') 64 | os.system('twine upload dist/*') 65 | 66 | self.status('Pushing git tags…') 67 | os.system('git tag v{0}'.format(about['__version__'])) 68 | os.system('git push --tags') 69 | 70 | sys.exit() 71 | 72 | 73 | setup( 74 | name=NAME, 75 | version=about['__version__'], 76 | description=DESCRIPTION, 77 | long_description=long_description, 78 | long_description_content_type='text/markdown', 79 | author=AUTHOR, 80 | author_email=EMAIL, 81 | python_requires=REQUIRES_PYTHON, 82 | url=URL, 83 | install_requires=REQUIRED, 84 | include_package_data=True, 85 | packages=[PACKAGE], 86 | zip_safe=False, 87 | license='MIT', 88 | classifiers=[ 89 | 'Development Status :: 4 - Beta', 90 | 'License :: OSI Approved :: MIT License', 91 | 'Intended Audience :: Developers', 92 | 'Natural Language :: English', 93 | 'Programming Language :: Python', 94 | 'Programming Language :: Python :: 3', 95 | 'Programming Language :: Python :: 3.6', 96 | 'Programming Language :: Python :: 3.7', 97 | 'Programming Language :: Python :: Implementation :: CPython', 98 | ], 99 | cmdclass={ 100 | 'upload': UploadCommand, 101 | }, 102 | ) 103 | --------------------------------------------------------------------------------