├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── facebookmarketing ├── __init__.py ├── client.py ├── decorators.py ├── enumerators.py └── exceptions.py ├── pyproject.toml └── tests ├── __init__.py └── test_client.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 | .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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 GearPlug 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # facebookmarketing-python 2 | 3 | facebookmarketing is an API wrapper for Facebook and Instagram written in Python. 4 | 5 | ## Installing 6 | ``` 7 | pip install facebookmarketing-python 8 | ``` 9 | 10 | ## Facebook Usage 11 | 12 | #### Client instantiation 13 | ``` 14 | from facebookmarketing.client import Client 15 | 16 | client = Client('APP_ID', 'APP_SECRET', 'v4.0') 17 | ``` 18 | 19 | ### OAuth 2.0 20 | 21 | For more information: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/ 22 | 23 | #### Get authorization url 24 | ``` 25 | url = client.authorization_url('REDIRECT_URL', 'STATE', ['pages_manage_ads', 'pages_manage_metadata', 'pages_read_engagement', 'leads_retrieval']) 26 | ``` 27 | 28 | #### Exchange the code for an access token 29 | ``` 30 | response = client.exchange_code('REDIRECT_URL', 'CODE') 31 | access_token = response['access_token'] 32 | ``` 33 | 34 | #### Extend a short-lived access token for a long-lived access token 35 | ``` 36 | response = client.extend_token(access_token) # From previous step 37 | access_token = response['access_token'] 38 | ``` 39 | 40 | #### Get app token 41 | ``` 42 | response = client.get_app_token() 43 | app_access_token = response['access_token'] 44 | ``` 45 | 46 | #### Inspect a token 47 | ``` 48 | response = client.inspect_token(access_token, app_access_token) # From previous step 49 | ``` 50 | 51 | #### Set the access token in the library 52 | ``` 53 | client.set_access_token(access_token) # From previous step 54 | ``` 55 | 56 | ### User 57 | 58 | For more information: https://developers.facebook.com/docs/graph-api/reference/user/ 59 | 60 | #### Get account information 61 | ``` 62 | response = client.get_account() 63 | ``` 64 | 65 | #### Get account pages 66 | ``` 67 | response = client.get_pages() 68 | ``` 69 | 70 | #### Get page token 71 | ``` 72 | page_access_token = client.get_page_token('PAGE_ID') # From previous step 73 | ``` 74 | 75 | ### Page 76 | 77 | For more information: https://developers.facebook.com/docs/graph-api/reference/page/ 78 | 79 | #### Get lead generation forms given the page 80 | ``` 81 | response = client.get_ad_account_leadgen_forms('PAGE_ID', page_access_token) # From previous step 82 | ``` 83 | #### Get leads info given the lead generation form 84 | ``` 85 | response = client.get_ad_leads('LEADGEN_FORM_ID') 86 | ``` 87 | 88 | #### Get a sigle lead info 89 | ``` 90 | response = client.get_leadgen('LEADGEN_ID') 91 | ``` 92 | 93 | ### Webhooks 94 | 95 | For more information: https://developers.facebook.com/docs/graph-api/webhooks 96 | 97 | The following methods cover Step 2 and 4 of the Webhook lead retrieval guide: 98 | Webhooks: https://developers.facebook.com/docs/marketing-api/guides/lead-ads/retrieving/ 99 | 100 | #### Create a webhook for leads retrieval 101 | ``` 102 | response = client.create_app_subscriptions('page', 'callback_url', 'leadgen', 'abc123', app_access_token) # You get app_access_token from get_app_token() method 103 | 104 | response = client.create_page_subscribed_apps('PAGE_ID', page_access_token, params={'subscribed_fields': 'leadgen'}) # You get page_access_token from get_page_token() method 105 | ``` 106 | 107 | ## Instagram Usage 108 | 109 | #### Client instantiation 110 | ``` 111 | from facebookmarketing.client import Client 112 | 113 | client = Client('APP_ID', 'APP_SECRET', 'v12.0') 114 | ``` 115 | 116 | ### OAuth 2.0 117 | 118 | For more information: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/ 119 | 120 | #### Get authorization url 121 | ``` 122 | url = client.authorization_url('REDIRECT_URL', 'STATE', ['instagram_basic', 'pages_show_list']) 123 | ``` 124 | 125 | #### Exchange the code for an access token 126 | ``` 127 | response = client.exchange_code('REDIRECT_URL', 'CODE') 128 | access_token = response['access_token'] 129 | ``` 130 | 131 | ### Page 132 | 133 | #### Get page id 134 | ``` 135 | response = client.get_instagram(page_id, ['instagram_business_account']) 136 | page_id = response['instagram_business_account']['id'] 137 | ``` 138 | 139 | ### Media 140 | 141 | #### Get media 142 | ``` 143 | response = client.get_instagram_media(page_id) 144 | ``` 145 | 146 | #### Get media object 147 | ``` 148 | response = client.get_instagram_media_object(media_id, fields=['id','media_type','media_url','owner','timestamp']) 149 | ``` 150 | 151 | ### Hashtag 152 | 153 | #### Search hashtag 154 | ``` 155 | response = (client.get_instagram_hashtag_search(page_id, 'coke')) 156 | ``` 157 | 158 | #### Get hashtag object 159 | ``` 160 | response = client.get_instagram_hashtag_object(hashtag_id, fields=['id', 'name']) 161 | ``` 162 | 163 | #### Get hashtag top media 164 | ``` 165 | response = client.get_instagram_hashtag_top_media(hashtag_id, instagram_id, ['id','media_type','comments_count','like_count', 'caption']) 166 | ``` 167 | 168 | ## Requirements 169 | - requests 170 | 171 | ## Contributing 172 | We are always grateful for any kind of contribution including but not limited to bug reports, code enhancements, bug fixes, and even functionality suggestions. 173 | 174 | #### You can report any bug you find or suggest new functionality with a new [issue](https://github.com/GearPlug/facebookmarketing-python/issues). 175 | 176 | #### If you want to add yourself some functionality to the wrapper: 177 | 1. Fork it ( https://github.com/GearPlug/facebookmarketing-python ) 178 | 2. Create your feature branch (git checkout -b my-new-feature) 179 | 3. Commit your changes (git commit -am 'Adds my new feature') 180 | 4. Push to the branch (git push origin my-new-feature) 181 | 5. Create a new Pull Request 182 | -------------------------------------------------------------------------------- /facebookmarketing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GearPlug/facebookmarketing-python/15f8a5414ea14d5fb72981219d46924ea98b21dc/facebookmarketing/__init__.py -------------------------------------------------------------------------------- /facebookmarketing/client.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | from hashlib import sha256 4 | from urllib.parse import urlencode, urlparse 5 | from uuid import uuid4 6 | 7 | import requests 8 | 9 | from facebookmarketing import exceptions 10 | from facebookmarketing.decorators import access_token_required 11 | from facebookmarketing.enumerators import ErrorEnum 12 | 13 | 14 | class Client(object): 15 | BASE_URL = "https://graph.facebook.com/" 16 | 17 | def __init__( 18 | self, 19 | app_id: str, 20 | app_secret: str, 21 | version: str = "v12.0", 22 | requests_hooks: dict = None, 23 | paginate: bool = True, 24 | limit: int = 100, 25 | ) -> None: 26 | self.app_id = app_id 27 | self.app_secret = app_secret 28 | if not version.startswith("v"): 29 | version = "v" + version 30 | self.version = version 31 | self.access_token = None 32 | self.paginate = paginate 33 | self.limit = limit 34 | self.BASE_URL += self.version 35 | if requests_hooks and not isinstance(requests_hooks, dict): 36 | raise Exception( 37 | 'requests_hooks must be a dict. e.g. {"response": func}. http://docs.python-requests.org/en/master/user/advanced/#event-hooks' 38 | ) 39 | self.requests_hooks = requests_hooks 40 | 41 | def set_access_token(self, token: str) -> None: 42 | """Sets the User Access Token for its use in this library. 43 | 44 | Args: 45 | token (str): User Access Token. 46 | """ 47 | self.access_token = token 48 | 49 | def get_app_token(self) -> dict: 50 | """Generates an Application Token. 51 | 52 | Returns: 53 | dict: App token data. 54 | """ 55 | params = {"client_id": self.app_id, "client_secret": self.app_secret, "grant_type": "client_credentials"} 56 | return self._get("/oauth/access_token", params=params) 57 | 58 | def authorization_url(self, redirect_uri: str, state: str, scope: list = None) -> str: 59 | """Generates an Authorization URL. 60 | 61 | Args: 62 | redirect_uri (str): A string with the redirect_url set in the app config. 63 | state (str): A unique code for validation. 64 | scope (list, optional): A list of strings with the scopes. Defaults to None. 65 | 66 | Raises: 67 | Exception: Scope argument is not a list. 68 | 69 | Returns: 70 | str: Url for oauth. 71 | """ 72 | if scope is None: 73 | scope = [] 74 | if not isinstance(scope, list): 75 | raise Exception("scope argument must be a list") 76 | 77 | params = { 78 | "client_id": self.app_id, 79 | "redirect_uri": redirect_uri, 80 | "state": state, 81 | "scope": " ".join(scope), 82 | "response_type": "code", 83 | } 84 | url = "https://facebook.com/{}/dialog/oauth?".format(self.version) + urlencode(params) 85 | return url 86 | 87 | def exchange_code(self, redirect_uri: str, code: str) -> dict: 88 | """Exchanges an oauth code for an user token. 89 | 90 | Args: 91 | redirect_uri (str): A string with the redirect_url set in the app config. 92 | code (str): A string containing the code to exchange. 93 | 94 | Returns: 95 | dict: User token data. 96 | """ 97 | params = { 98 | "client_id": self.app_id, 99 | "redirect_uri": redirect_uri, 100 | "client_secret": self.app_secret, 101 | "code": code, 102 | } 103 | return self._get("/oauth/access_token", params=params) 104 | 105 | def extend_token(self, token: str) -> dict: 106 | """Extends a short-lived User Token for a long-lived User Token. 107 | 108 | Args: 109 | token (str): User token to extend. 110 | 111 | Returns: 112 | dict: User token data. 113 | """ 114 | params = { 115 | "grant_type": "fb_exchange_token", 116 | "client_id": self.app_id, 117 | "client_secret": self.app_secret, 118 | "fb_exchange_token": token, 119 | } 120 | return self._get("/oauth/access_token", params=params) 121 | 122 | def inspect_token(self, input_token: str, token: str) -> dict: 123 | """Inspects an User Access Token. 124 | 125 | Args: 126 | input_token (str): A string with the User Access Token to inspect. 127 | token (str): A string with the Developer Token (App Owner) or an Application Token. 128 | 129 | Returns: 130 | dict: User token data. 131 | """ 132 | params = {"input_token": input_token, "access_token": token} 133 | return self._get("/debug_token", params=params) 134 | 135 | @access_token_required 136 | def get_account(self) -> dict: 137 | """Gets the authed account information. 138 | 139 | Returns: 140 | dict: Account data. 141 | """ 142 | params = self._get_params() 143 | return self._get("/me", params=params) 144 | 145 | @access_token_required 146 | def get_pages(self) -> dict: 147 | """Gets the authed account pages. 148 | 149 | Returns: 150 | dict: Pages data. 151 | """ 152 | params = self._get_params() 153 | params["limit"] = self.limit 154 | return self._get("/me/accounts", params=params) 155 | 156 | @access_token_required 157 | def get_page_token(self, page_id: str) -> str: 158 | """Gets page token for the given page. 159 | 160 | Args: 161 | page_id (str): String with Page's ID. 162 | 163 | Returns: 164 | dict: Page token data. 165 | """ 166 | pages = self.get_pages() 167 | page = next((p for p in pages["data"] if p["id"] == str(page_id)), None) 168 | if not page: 169 | return None 170 | return page["access_token"] 171 | 172 | def get_page_subscribed_apps(self, page_id: str, token: str) -> dict: 173 | """Get a list of apps subscribed to the Page's webhook updates. 174 | 175 | https://developers.facebook.com/docs/graph-api/reference/page/subscribed_apps/ 176 | 177 | Args: 178 | page_id (str): Page's ID. 179 | token (str): Page's token. 180 | 181 | Returns: 182 | dict: Graph API Response. 183 | """ 184 | params = self._get_params(token) 185 | return self._get("/{}/subscribed_apps".format(page_id), params=params) 186 | 187 | def create_page_subscribed_apps(self, page_id: str, token: str, params: dict = None) -> dict: 188 | """Associates an Application to a Page's webhook updates. 189 | 190 | https://developers.facebook.com/docs/graph-api/reference/page/subscribed_apps/ 191 | 192 | Args: 193 | page_id (str): Page's ID. 194 | token (str): Page's token. 195 | params (dict, optional): Page Webhooks fields that you want to subscribe. Defaults to None. 196 | 197 | Returns: 198 | dict: Graph API Response. 199 | """ 200 | _params = self._get_params(token) 201 | if params and isinstance(params, dict): 202 | _params.update(params) 203 | return self._post("/{}/subscribed_apps".format(page_id), params=_params) 204 | 205 | def delete_page_subscribed_apps(self, page_id: str, token: str) -> dict: 206 | """Dissociates an Application from a Page's webhook updates. 207 | 208 | https://developers.facebook.com/docs/graph-api/reference/page/subscribed_apps/ 209 | 210 | Args: 211 | page_id (str): Page's ID. 212 | token (str): Page's token. 213 | 214 | Returns: 215 | dict: Graph API Response. 216 | """ 217 | params = self._get_params(token) 218 | return self._delete("/{}/subscribed_apps".format(page_id), params=params) 219 | 220 | def get_app_subscriptions(self, application_token: str) -> dict: 221 | """Retrieves Webhook subscriptions for an App. 222 | 223 | https://developers.facebook.com/docs/graph-api/reference/v12.0/app/subscriptions 224 | 225 | Args: 226 | application_token (str): Application Token. 227 | 228 | Returns: 229 | dict: Graph API Response. 230 | """ 231 | params = self._get_params(application_token) 232 | return self._get("/{}/subscriptions".format(self.app_id), params=params) 233 | 234 | def create_app_subscriptions( 235 | self, object: str, callback_url: str, fields: str, verify_token: str, token: str 236 | ) -> dict: 237 | """Creates a Webhook subscription for an App. 238 | 239 | https://developers.facebook.com/docs/graph-api/reference/v12.0/app/subscriptions 240 | 241 | Args: 242 | object (str): Indicates the object type that this subscription applies to. 243 | callback_url (str): The URL that will receive the POST request when an update is triggered. 244 | fields (str): The set of fields in this object that are subscribed to. 245 | verify_token (str): Indicates whether or not the subscription is active. 246 | token (str): Application Token. 247 | 248 | Raises: 249 | exceptions.HttpsRequired: callback_url is not https. 250 | 251 | Returns: 252 | dict: Graph API Response. 253 | """ 254 | o = urlparse(callback_url) 255 | 256 | if o.scheme != "https": 257 | raise exceptions.HttpsRequired 258 | 259 | params = self._get_params(token) 260 | params.update({"object": object, "callback_url": callback_url, "fields": fields, "verify_token": verify_token}) 261 | return self._post("/{}/subscriptions".format(self.app_id), params=params) 262 | 263 | def delete_app_subscriptions(self, token: str) -> dict: 264 | """Deletes a Webhook subscription for an App. 265 | 266 | https://developers.facebook.com/docs/graph-api/reference/v12.0/app/subscriptions 267 | 268 | Args: 269 | token (str): Application Token. 270 | 271 | Returns: 272 | dict: Graph API Response. 273 | """ 274 | params = self._get_params(token) 275 | return self._delete("/{}/subscriptions".format(self.app_id), params=params) 276 | 277 | @access_token_required 278 | def get_ad_account_leadgen_forms(self, page_id: str, page_access_token: str = None) -> dict: 279 | """Gets the forms for the given page. 280 | 281 | Args: 282 | page_id (str): A string with Page's ID. 283 | page_access_token (str, optional): Page Access Token. Defaults to None. 284 | 285 | Returns: 286 | dict: Graph API Response. 287 | """ 288 | params = self._get_params(token=page_access_token) 289 | params["limit"] = self.limit 290 | return self._get("/{}/leadgen_forms".format(page_id), params=params) 291 | 292 | @access_token_required 293 | def get_leadgen(self, leadgen_id: str, fields: list = None) -> dict: 294 | """Get a single leadgen given an id. 295 | 296 | Args: 297 | leadgen_id (str): A string with the leadgen's ID. 298 | 299 | Returns: 300 | dict: Graph API Response. 301 | """ 302 | params = self._get_params() 303 | if fields: 304 | params["fields"] = ",".join(fields) 305 | return self._get("/{0}".format(leadgen_id), params=params) 306 | 307 | @access_token_required 308 | def get_ad_leads( 309 | self, leadgen_form_id: str, from_date: str = None, to_date: str = None, after: str = None, fields: list = None 310 | ) -> dict: 311 | """Gets the leads for the given form. 312 | 313 | Args: 314 | leadgen_form_id (str): A string with the Form's ID. 315 | from_date (str, optional): A timestamp. Defaults to None. 316 | to_date (str, optional): A timestamp. Defaults to None. 317 | after (str, optional): A cursor. Defaults to None. 318 | 319 | Returns: 320 | dict: Graph API Response. 321 | """ 322 | params = self._get_params() 323 | if from_date: 324 | params["from_date"] = from_date 325 | if to_date: 326 | params["to_date"] = to_date 327 | if after: 328 | params["after"] = after 329 | if fields: 330 | params["fields"] = ",".join(fields) 331 | return self._get("/{}/leads".format(leadgen_form_id), params=params) 332 | 333 | def get_custom_audience(self, account_id: str, fields: list = None) -> dict: 334 | """Retrieve a custom audience data. 335 | 336 | Args: 337 | account_id (str): Ad account id. 338 | fields (list, optional): Fields to include in the response. Defaults to None. 339 | 340 | Returns: 341 | dict: Graph API Response. 342 | """ 343 | params = self._get_params() 344 | if fields and isinstance(fields, list): 345 | params["fields"] = ",".join(fields) 346 | return self._get("/{}/customaudiences".format(account_id), params=params) 347 | 348 | def create_custom_audience( 349 | self, 350 | account_id: str, 351 | name: str, 352 | description: str, 353 | subtype: str = "CUSTOM", 354 | customer_file_source: str = "USER_PROVIDED_ONLY", 355 | ) -> dict: 356 | """Create an empty Custom Audience. 357 | 358 | Args: 359 | account_id (str): Ad account id 360 | name (str): Custom Audience name 361 | description (str): Custom Audience description 362 | subtype (str): Type of Custom Audience 363 | customer_file_source (str): Describes how the customer information in your Custom Audience was originally collected. 364 | Values: 365 | USER_PROVIDED_ONLY 366 | Advertisers collected information directly from customers 367 | PARTNER_PROVIDED_ONLY 368 | Advertisers sourced information directly from partners (e.g., agencies or data providers) 369 | BOTH_USER_AND_PARTNER_PROVIDED 370 | Advertisers collected information directly from customers and it was also sourced from partners (e.g., agencies) 371 | 372 | https://developers.facebook.com/docs/marketing-api/audiences/guides/custom-audiences 373 | 374 | Returns: 375 | dict: Graph API Response. 376 | """ 377 | params = self._get_params() 378 | json = { 379 | "name": name, 380 | "description": description, 381 | "subtype": subtype, 382 | "customer_file_source": customer_file_source, 383 | } 384 | return self._post("/{}/customaudiences".format(account_id), params=params, json=json) 385 | 386 | def add_user_to_audience(self, audience_id: str, schema: str, data: list) -> dict: 387 | """Add people to your ad's audience with a hash of data from your business. 388 | 389 | https://developers.facebook.com/docs/marketing-api/reference/custom-audience/users/ 390 | 391 | Args: 392 | audience_id (str): Audience id. 393 | schema (str): Specify what type of information you will be providing. 394 | data (list): List of data corresponding to the schema. 395 | 396 | Returns: 397 | dict: Graph API Response. 398 | """ 399 | params = self._get_params() 400 | json = { 401 | "session": { 402 | "session_id": int(str(uuid4().int)[:7]), 403 | "batch_seq": 1, 404 | "last_batch_flag": True, 405 | "estimated_num_total": len(data), 406 | }, 407 | "payload": {"schema": schema, "data": [sha256(i.encode("utf-8")).hexdigest() for i in data]}, 408 | } 409 | return self._post("/{}/users".format(audience_id), params=params, json=json) 410 | 411 | def remove_user_to_audience(self, audience_id: str, schema: str, data: list) -> dict: 412 | """Remove people from your ad's audience with a hash of data from your business. 413 | 414 | https://developers.facebook.com/docs/marketing-api/reference/custom-audience/users/ 415 | 416 | Args: 417 | audience_id (str): Audience id. 418 | schema (str): Specify what type of information you will be providing. 419 | data (list): List of data corresponding to the schema. 420 | 421 | Returns: 422 | dict: Graph API Response. 423 | """ 424 | params = self._get_params() 425 | json = { 426 | "session": { 427 | "session_id": int(str(uuid4().int)[:7]), 428 | "batch_seq": 1, 429 | "last_batch_flag": True, 430 | "estimated_num_total": len(data), 431 | }, 432 | "payload": {"schema": schema, "data": [sha256(i.encode("utf-8")).hexdigest() for i in data]}, 433 | } 434 | return self._delete("/{}/users".format(audience_id), params=params, json=json) 435 | 436 | def get_adaccounts(self, fields: list = None) -> dict: 437 | """Retrieves Ad Accounts. 438 | 439 | Args: 440 | fields (list, optional): Fields to include in the response. Defaults to None. 441 | 442 | Returns: 443 | dict: Graph API Response. 444 | """ 445 | params = self._get_params() 446 | if fields and isinstance(fields, list): 447 | params["fields"] = ",".join(fields) 448 | return self._get("/me/adaccounts", params=params) 449 | 450 | def get_instagram(self, page_id: str, fields: list = None) -> dict: 451 | """[summary] 452 | 453 | Args: 454 | page_id (str): [description] 455 | fields (list, optional): [description]. Defaults to None. 456 | 457 | Returns: 458 | dict: [description] 459 | """ 460 | params = self._get_params() 461 | if fields and isinstance(fields, list): 462 | params["fields"] = ",".join(fields) 463 | return self._get("/{}".format(page_id), params=params) 464 | 465 | def get_instagram_media(self, page_id: str, fields: list = None) -> dict: 466 | """[summary] 467 | 468 | Args: 469 | page_id (str): [description] 470 | fields (list, optional): [description]. Defaults to None. 471 | 472 | Returns: 473 | dict: [description] 474 | """ 475 | params = self._get_params() 476 | if fields and isinstance(fields, list): 477 | params["fields"] = ",".join(fields) 478 | return self._get("/{}/media".format(page_id), params=params) 479 | 480 | def get_instagram_media_object(self, media_id: str, fields: list = None) -> dict: 481 | """[summary] 482 | 483 | Args: 484 | media_id (str): [description] 485 | fields (list, optional): [description]. Defaults to None. 486 | 487 | Returns: 488 | dict: [description] 489 | """ 490 | params = self._get_params() 491 | if fields and isinstance(fields, list): 492 | params["fields"] = ",".join(fields) 493 | return self._get("/{}".format(media_id), params=params) 494 | 495 | def get_instagram_media_comment(self, media_id: str) -> dict: 496 | """[summary] 497 | 498 | Args: 499 | media_id (str): [description] 500 | 501 | Returns: 502 | dict: [description] 503 | """ 504 | params = self._get_params() 505 | return self._get("/{}/comments".format(media_id), params=params) 506 | 507 | def get_instagram_hashtag(self, page_id: str, fields: list = None) -> dict: 508 | """[summary] 509 | 510 | Args: 511 | page_id (str): [description] 512 | fields (list, optional): [description]. Defaults to None. 513 | 514 | Returns: 515 | dict: [description] 516 | """ 517 | params = self._get_params() 518 | if fields and isinstance(fields, list): 519 | params["fields"] = ",".join(fields) 520 | return self._get("/{}/media".format(page_id), params=params) 521 | 522 | def get_instagram_hashtag_search(self, user_id: str, query: str) -> dict: 523 | """[summary] 524 | 525 | Args: 526 | user_id (str): [description] 527 | query (str): [description] 528 | 529 | Returns: 530 | dict: [description] 531 | """ 532 | params = self._get_params() 533 | params["user_id"] = user_id 534 | params["q"] = query 535 | return self._get("/ig_hashtag_search", params=params) 536 | 537 | def get_instagram_hashtag_object(self, hashtag_id: str, fields: list = None) -> dict: 538 | """[summary] 539 | 540 | Args: 541 | hashtag_id (str): [description] 542 | fields (list, optional): [description]. Defaults to None. 543 | 544 | Returns: 545 | dict: [description] 546 | """ 547 | params = self._get_params() 548 | if fields and isinstance(fields, list): 549 | params["fields"] = ",".join(fields) 550 | return self._get("/{}".format(hashtag_id), params=params) 551 | 552 | def get_instagram_hashtag_recent_media(self, hashtag_id: str, user_id: str, fields: list = None) -> dict: 553 | """[summary] 554 | 555 | Args: 556 | hashtag_id (str): [description] 557 | user_id (str): [description] 558 | fields (list, optional): [description]. Defaults to None. 559 | 560 | Returns: 561 | dict: [description] 562 | """ 563 | params = self._get_params() 564 | params["user_id"] = user_id 565 | if fields and isinstance(fields, list): 566 | params["fields"] = ",".join(fields) 567 | return self._get("/{}/recent_media".format(hashtag_id), params=params) 568 | 569 | def get_instagram_hashtag_top_media(self, hashtag_id: str, user_id: str, fields: list = None) -> dict: 570 | """[summary] 571 | 572 | Args: 573 | hashtag_id (str): [description] 574 | user_id (str): [description] 575 | fields (list, optional): [description]. Defaults to None. 576 | 577 | Returns: 578 | dict: [description] 579 | """ 580 | params = self._get_params() 581 | params["user_id"] = user_id 582 | if fields and isinstance(fields, list): 583 | params["fields"] = ",".join(fields) 584 | return self._get("/{}/top_media".format(hashtag_id), params=params) 585 | 586 | def _get_params(self, token: str = None) -> dict: 587 | """Sets parameters for requests. 588 | 589 | Args: 590 | token (str, optional): Access token. Defaults to None. 591 | 592 | Returns: 593 | dict: Access token and hashed access token. 594 | """ 595 | _token = token if token else self.access_token 596 | return {"access_token": _token, "appsecret_proof": self._get_app_secret_proof(_token)} 597 | 598 | def _get_app_secret_proof(self, token: str) -> str: 599 | """Generates app secret proof. 600 | 601 | https://developers.facebook.com/docs/graph-api/security 602 | 603 | Args: 604 | token (str): Access token to hash. 605 | 606 | Returns: 607 | str: Hashed access token. 608 | """ 609 | key = self.app_secret.encode("utf-8") 610 | msg = token.encode("utf-8") 611 | h = hmac.new(key, msg=msg, digestmod=hashlib.sha256) 612 | return h.hexdigest() 613 | 614 | def _paginate_response(self, response: dict, **kwargs) -> dict: 615 | """Cursor-based Pagination 616 | 617 | https://developers.facebook.com/docs/graph-api/results 618 | 619 | Args: 620 | response (dict): Graph API Response. 621 | 622 | Returns: 623 | dict: Graph API Response. 624 | """ 625 | if not self.paginate: 626 | return response 627 | while "paging" in response and "next" in response["paging"]: 628 | data = response["data"] 629 | params = kwargs.get("params", {}) 630 | if "limit" in params: 631 | params.pop("limit") 632 | response = self._get(response["paging"]["next"].replace(self.BASE_URL, ""), **kwargs) 633 | response["data"] += data 634 | return response 635 | 636 | def _get(self, endpoint, **kwargs): 637 | return self._paginate_response(self._request("GET", endpoint, **kwargs), **kwargs) 638 | 639 | def _post(self, endpoint, **kwargs): 640 | return self._request("POST", endpoint, **kwargs) 641 | 642 | def _delete(self, endpoint, **kwargs): 643 | return self._request("DELETE", endpoint, **kwargs) 644 | 645 | def _request(self, method, endpoint, headers=None, **kwargs): 646 | _headers = {"Accept": "application/json", "Content-Type": "application/json"} 647 | if headers: 648 | _headers.update(headers) 649 | if self.requests_hooks: 650 | kwargs.update({"hooks": self.requests_hooks}) 651 | return self._parse(requests.request(method, self.BASE_URL + endpoint, headers=_headers, **kwargs)) 652 | 653 | def _parse(self, response): 654 | if "application/json" in response.headers["Content-Type"]: 655 | r = response.json() 656 | else: 657 | return response.text 658 | 659 | if "error" in r: 660 | error = r["error"] 661 | elif "data" in r and "error" in r["data"]: 662 | error = r["data"]["error"] 663 | else: 664 | error = None 665 | 666 | if error: 667 | code = error["code"] 668 | message = error["message"] 669 | try: 670 | error_enum = ErrorEnum(code) 671 | except Exception: 672 | raise exceptions.UnexpectedError("Error: {}. Message {}".format(code, message)) 673 | if error_enum == ErrorEnum.UnknownError: 674 | raise exceptions.UnknownError(message) 675 | elif error_enum == ErrorEnum.AppRateLimit: 676 | raise exceptions.AppRateLimitError(message) 677 | elif error_enum == ErrorEnum.AppPermissionRequired: 678 | raise exceptions.AppPermissionRequiredError(message) 679 | elif error_enum == ErrorEnum.UserRateLimit: 680 | raise exceptions.UserRateLimitError(message) 681 | elif error_enum == ErrorEnum.InvalidParameter: 682 | raise exceptions.InvalidParameterError(message) 683 | elif error_enum == ErrorEnum.SessionKeyInvalid: 684 | raise exceptions.SessionKeyInvalidError(message) 685 | elif error_enum == ErrorEnum.IncorrectPermission: 686 | raise exceptions.IncorrectPermissionError(message) 687 | elif error_enum == ErrorEnum.InvalidOauth20AccessToken: 688 | raise exceptions.PermissionError(message) 689 | elif error_enum == ErrorEnum.ExtendedPermissionRequired: 690 | raise exceptions.ExtendedPermissionRequiredError(message) 691 | else: 692 | raise exceptions.BaseError("Error: {}. Message {}".format(code, message)) 693 | 694 | return r 695 | -------------------------------------------------------------------------------- /facebookmarketing/decorators.py: -------------------------------------------------------------------------------- 1 | from facebookmarketing.exceptions import AccessTokenRequired 2 | from functools import wraps 3 | 4 | 5 | def access_token_required(func): 6 | @wraps(func) 7 | def helper(*args, **kwargs): 8 | client = args[0] 9 | if not client.access_token: 10 | raise AccessTokenRequired('You must set the Access Token.') 11 | return func(*args, **kwargs) 12 | 13 | return helper 14 | -------------------------------------------------------------------------------- /facebookmarketing/enumerators.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ErrorEnum(Enum): 5 | UnknownError = 1 6 | AppRateLimit = 4 7 | AppPermissionRequired = 10 8 | UserRateLimit = 17 9 | InvalidParameter = 100 10 | SessionKeyInvalid = 102 11 | IncorrectPermission = 104 12 | InvalidOauth20AccessToken = 190 13 | PermissionError = 200 14 | ExtendedPermissionRequired = 294 15 | -------------------------------------------------------------------------------- /facebookmarketing/exceptions.py: -------------------------------------------------------------------------------- 1 | class BaseError(Exception): 2 | pass 3 | 4 | 5 | class AccessTokenRequired(BaseError): 6 | pass 7 | 8 | 9 | class HttpsRequired(BaseError): 10 | pass 11 | 12 | 13 | class UnknownError(BaseError): 14 | pass 15 | 16 | 17 | class UnexpectedError(BaseError): 18 | pass 19 | 20 | 21 | class AppRateLimitError(BaseError): 22 | pass 23 | 24 | 25 | class AppPermissionRequiredError(BaseError): 26 | pass 27 | 28 | 29 | class UserRateLimitError(BaseError): 30 | pass 31 | 32 | 33 | class InvalidParameterError(BaseError): 34 | pass 35 | 36 | 37 | class SessionKeyInvalidError(BaseError): 38 | pass 39 | 40 | 41 | class IncorrectPermissionError(BaseError): 42 | pass 43 | 44 | 45 | class InvalidOauth20AccessTokenError(BaseError): 46 | pass 47 | 48 | 49 | class PermissionError(BaseError): 50 | pass 51 | 52 | 53 | class ExtendedPermissionRequiredError(BaseError): 54 | pass 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "facebookmarketing-python" 3 | version = "1.1.2" 4 | description = "API wrapper for Facebook written in Python" 5 | authors = ["Miguel Ferrer "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "facebookmarketing"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.7" 12 | requests = "^2.26.0" 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GearPlug/facebookmarketing-python/15f8a5414ea14d5fb72981219d46924ea98b21dc/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | from facebookmarketing.client import Client 4 | from urllib.parse import urlparse, parse_qs 5 | 6 | 7 | class FacebookMarketingTestCases(TestCase): 8 | def setUp(self): 9 | self.app_id = os.environ.get('api_id') 10 | self.app_secret = os.environ.get('app_secret') 11 | self.client = Client(self.app_id, self.app_secret, 'v2.10') 12 | self.redirect_url = os.environ.get('redirect_url') 13 | self.scope = ['manage_pages'] 14 | 15 | def test_authorization_url(self): 16 | url = self.client.authorization_url(self.redirect_url, self.scope) 17 | self.assertIsInstance(url, str) 18 | o = urlparse(url) 19 | query = parse_qs(o.query) 20 | self.assertIn('client_id', query) 21 | self.assertEqual(query['client_id'][0], self.app_id) 22 | self.assertIn('redirect_uri', query) 23 | self.assertEqual(query['redirect_uri'][0], self.redirect_url) 24 | self.assertIn('scope', query) 25 | self.assertEqual(query['scope'], self.scope) 26 | 27 | def test_app_token(self): 28 | response = self.client.get_app_token() 29 | self.assertIsInstance(response, dict) 30 | self.assertIn('access_token', response) 31 | self.assertIn('token_type', response) 32 | self.assertEqual(response['token_type'], 'bearer') 33 | --------------------------------------------------------------------------------