def testBusinessLimit(self): r = RateLimit() headers = { "x-business-use-case-usage": "{\"112130216863063\":[{\"type\":\"pages\",\"call_count\":1,\"total_cputime\":1,\"total_time\":1,\"estimated_time_to_regain_access\":0}]}"} r.set_limit(headers) self.assertEqual(r.get_limit(object_id="112130216863063", endpoint="pages").call_count, 1)
def testAppLimit(self): headers = {'x-app-usage': '{"call_count":91,"total_cputime":15,"total_time":12}'} r = RateLimit() r.set_limit(headers) self.assertEqual(r.get_limit().call_count, 91) self.assertEqual(r.get_limit().max_percent(), 91) self.assertEqual(r.get_max_percent(), 91) self.assertEqual(r.get_sleep_seconds(), 2)
class BaseApi(object): VALID_API_VERSIONS = ["v3.3", "v4.0", "v5.0", "v6.0", "v7.0", "v8.0"] GRAPH_URL = "https://graph.facebook.com/" DEFAULT_AUTHORIZATION_URL = 'https://www.facebook.com/dialog/oauth' DEFAULT_EXCHANGE_ACCESS_TOKEN_URL = 'https://graph.facebook.com/oauth/access_token' DEFAULT_REDIRECT_URI = 'https://localhost/' DEFAULT_SCOPE = [] DEFAULT_STATE = 'PyFacebook' def __init__( self, app_id=None, # type: Optional[str] app_secret=None, # type: Optional[str] short_token=None, # type: Optional[str] long_term_token=None, # type: Optional[str] application_only_auth=False, # type: bool initial_access_token=True, # type: bool version=None, # type: Optional[str] timeout=None, # type: Optional[int] sleep_on_rate_limit=False, # type: bool sleep_seconds_mapping=None, # type: Dict[int, int] proxies=None, # type: Optional[dict] debug_http=False # type: bool ): # type: (...) -> None """ :param app_id: Your app id. :param app_secret: Your app secret. :param short_token: short-lived token :param long_term_token: long-lived token. :param application_only_auth: Use the `App Access Token` only. :param initial_access_token: If you want use api do authorize, set this with False. :param version: The version for the graph api. :param timeout: Request time out :param sleep_on_rate_limit: Use this will sleep between two request. :param sleep_seconds_mapping: Mapping for percent to sleep. :param proxies: Your proxies config. :param debug_http: Set to True to enable debug output from urllib when performing any HTTP requests. Defaults to False. """ self.app_id = app_id self.app_secret = app_secret self.short_token = short_token self.__timeout = timeout self.base_url = self.GRAPH_URL self.proxies = proxies self.session = requests.Session() self.sleep_on_rate_limit = sleep_on_rate_limit self.instagram_business_id = None self._debug_http = debug_http self.authorization_url = self.DEFAULT_AUTHORIZATION_URL self.exchange_access_token_url = self.DEFAULT_EXCHANGE_ACCESS_TOKEN_URL self.redirect_uri = self.DEFAULT_REDIRECT_URI self.scope = self.DEFAULT_SCOPE self.auth_session = None # Authorization session self.rate_limit = RateLimit() if version is None: # default version is last new. self.version = self.VALID_API_VERSIONS[-1] else: version = str(version) if not version.startswith('v'): version = 'v' + version version_regex = re.compile(r"^v\d.\d{1,2}$") match = version_regex.search(str(version)) if match is not None: if version not in self.VALID_API_VERSIONS: raise PyFacebookException( ErrorMessage(code=ErrorCode.INVALID_PARAMS, message="Valid API version are {}".format( ",".join(self.VALID_API_VERSIONS)))) else: self.version = version else: raise PyFacebookException( ErrorMessage( code=ErrorCode.INVALID_PARAMS, message= "Version string is invalid for {0}. You can provide with like: 5.0 or v5.0" .format(version), )) self.sleep_seconds_mapping = self._build_sleep_seconds_resource( sleep_seconds_mapping) if long_term_token: self._access_token = long_term_token elif short_token and all([self.app_id, self.app_secret]): token = self.get_long_token(app_id=self.app_id, app_secret=self.app_secret, short_token=self.short_token) self._access_token = token.access_token elif application_only_auth and all([self.app_id, app_secret]): token = self.get_app_token() self._access_token = token.access_token elif not initial_access_token and all([self.app_id, app_secret]): self._access_token = None else: raise PyFacebookException( ErrorMessage( code=ErrorCode.MISSING_PARAMS, message= ("You can initial api with three methods: \n" "1. Just provide long(short) lived token or app access token with param `long_term_token`.\n" "2. Provide a short lived token and app credentials. Api will auto exchange long term token.\n" "3. Provide app credentials and with application_only_auth set to true. " "Api will auto get and use app access token.\n" "4. Provide app credentials and prepare for do authorize(This will not retrieve access token)" ))) if debug_http: from six.moves import http_client http_client.HTTPConnection.debuglevel = 1 logging.basicConfig( ) # you need to initialize logging, otherwise you will not see anything from requests logging.getLogger().setLevel(logging.DEBUG) requests_log = logging.getLogger("requests.packages.urllib3") requests_log.setLevel(logging.DEBUG) requests_log.propagate = True @staticmethod def _build_sleep_seconds_resource(sleep_seconds_mapping): # type: (Optional[Dict]) -> Optional[List[PercentSecond]] """ Sort and convert data :param sleep_seconds_mapping: mapping for sleep. :return: """ if sleep_seconds_mapping is None: return None mapping_list = [ PercentSecond(percent=p, seconds=s) for p, s in six.iteritems(sleep_seconds_mapping) ] return sorted(mapping_list, key=lambda ps: ps.percent) @staticmethod def _generate_secret_proof(secret, access_token): # type: (str, str) -> Optional[str] if secret is None: logging.warning( "Calls from a server can be better secured by adding a parameter called appsecret_proof. " "And need your app secret.") return None return hmac.new(secret.encode("utf-8"), msg=access_token.encode("utf-8"), digestmod=hashlib.sha256).hexdigest() def _request(self, path, method="GET", args=None, post_args=None, enforce_auth=True): # type: (str, str, Optional[dict], Optional[dict], bool) -> Response """ Build the request and send request to Facebook. :param path: The path for resource on facebook. :param method: Http methods. :param args: GET parameters. :param post_args: POST parameters. :param enforce_auth: Set to True mean this request need access token. :return: The Response instance. """ if args is None: args = dict() if post_args is not None: method = "POST" if enforce_auth: if method == "POST" and "access_token" not in post_args: post_args["access_token"] = self._access_token elif method == "GET" and "access_token" not in args: args["access_token"] = self._access_token # add appsecret_proof parameter # Refer: https://developers.facebook.com/docs/graph-api/securing-requests/ if method == "POST" and "appsecret_proof" not in post_args: secret_proof = self._generate_secret_proof( self.app_secret, post_args["access_token"]) if secret_proof is not None: post_args["appsecret_proof"] = secret_proof elif method == "GET" and "appsecret_proof" not in args: secret_proof = self._generate_secret_proof( self.app_secret, args["access_token"]) if secret_proof is not None: args["appsecret_proof"] = secret_proof # check path if not path.startswith("https"): path = self.base_url + path try: response = self.session.request(method, path, timeout=self.__timeout, params=args, data=post_args, proxies=self.proxies) except requests.HTTPError as e: raise PyFacebookException( ErrorMessage(code=ErrorCode.HTTP_ERROR, message=e.args[0])) headers = response.headers self.rate_limit.set_limit(headers) if self.sleep_on_rate_limit: sleep_seconds = self.rate_limit.get_sleep_seconds( sleep_data=self.sleep_seconds_mapping) time.sleep(sleep_seconds) return response def _parse_response(self, response): # type: (Response) -> dict data = response.json() self._check_graph_error(data) return data @staticmethod def _check_graph_error(data): # type: (dict) -> None """ Check the facebook response data. If have error raise a PyFacebookException. :param data: The data return by facebook. :return: None """ if 'error' in data: error_data = data['error'] raise PyFacebookException(error_data) def get_long_token(self, short_token, app_id=None, app_secret=None, return_json=False): # type: (str, Optional[str], Optional[str], bool) -> Optional[Union[AuthAccessToken, Dict]] """ Generate a long-lived token from a short-lived User access token. A long-lived token generally lasts about 60 days. :param app_id: Your app id. :param app_secret: Your app secret. :param short_token: short-lived token from the app. :param return_json: Set to false will return instance of AuthAccessToken. Or return json data. Default is false. """ if app_id is None: app_id = self.app_id app_secret = self.app_secret response = self._request(method='GET', path='{}/oauth/access_token'.format( self.version), args={ 'grant_type': 'fb_exchange_token', 'client_id': app_id, 'client_secret': app_secret, 'fb_exchange_token': short_token }, enforce_auth=False) data = self._parse_response(response) if return_json: return data else: return AuthAccessToken.new_from_json_dict(data) def get_app_token(self, return_json=False): # type: (bool)-> Optional[Union[Dict, AuthAccessToken]] """ Use app credentials to generate an app access token. :param return_json: Set to false will return instance of AuthAccessToken. Or return json data. Default is false. """ resp = self._request(method="GET", path="{}/oauth/access_token".format(self.version), args={ 'grant_type': 'client_credentials', 'client_id': self.app_id, 'client_secret': self.app_secret, }, enforce_auth=False) data = self._parse_response(resp) if return_json: return data else: return AuthAccessToken.new_from_json_dict(data) def get_token_info(self, input_token=None, return_json=False): # type: (Optional[str], bool) -> Optional[Union[Dict, AccessToken]] """ Obtain the current access token info if provide the app_id and app_secret. :param input_token: :param return_json: Set to false will return instance of AccessToken. Or return json data. Default is false. """ if input_token is None: input_token = self._access_token if all([self.app_id, self.app_secret]): access_token = "{0}|{1}".format(self.app_id, self.app_secret) else: access_token = self._access_token args = { "input_token": input_token, "access_token": access_token, } resp = self._request('{0}/debug_token'.format(self.version), args=args) data = self._parse_response(resp) if return_json: return data["data"] else: return AccessToken.new_from_json_dict(data['data']) def get_authorization_url(self, redirect_uri=None, scope=None, **kwargs): # type: (str, List, Dict) -> (str, str) """ Build authorization url to do authorize. Refer: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow :param redirect_uri: The URL that you want to redirect the person logging in back to. Note: Your redirect uri need be set to `Valid OAuth redirect URIs` items in App Dashboard. :param scope: A list of Permissions to request from the person using your app. :param kwargs: Extend args for oauth. :return: Authorization url and state. """ if not all([self.app_id, self.app_secret]): raise PyFacebookException( ErrorMessage( code=ErrorCode.MISSING_PARAMS, message="To do authorization need your app credentials")) if redirect_uri is None: redirect_uri = self.redirect_uri if scope is None: scope = self.scope session = OAuth2Session(client_id=self.app_id, scope=scope, redirect_uri=redirect_uri, state=self.DEFAULT_STATE, **kwargs) self.auth_session = facebook_compliance_fix(session) authorization_url, state = self.auth_session.authorization_url( url=self.authorization_url) return authorization_url, state def exchange_access_token(self, response, return_json=False): # type: (str, bool) -> Union[AuthAccessToken, Dict] """ :param response: The whole response url for your previous authorize step. :param return_json: Set to false will return instance of AuthAccessToken. Or return json data. Default is false. :return: """ if self.auth_session is None: raise PyFacebookException( ErrorMessage( code=ErrorCode.MISSING_PARAMS, message="exchange token should do authorize first")) self.auth_session.fetch_token(self.exchange_access_token_url, client_secret=self.app_secret, authorization_response=response) self._access_token = self.auth_session.access_token if return_json: return self.auth_session.token else: return AuthAccessToken.new_from_json_dict(self.auth_session.token) def exchange_insights_token(self, page_id, access_token=None): # type: (str, Optional[str]) -> str """ Use user access token to exchange page(managed by that user) access token. Refer: 1. https://developers.facebook.com/docs/pages/access-tokens 2. https://developers.facebook.com/docs/facebook-login/access-tokens :param page_id: The id for page :param access_token: user access token """ if access_token is None: access_token = self._access_token args = {"access_token": access_token, "fields": "access_token"} resp = self._request(method="GET", path="{version}/{page_id}".format( version=self.version, page_id=page_id), args=args, enforce_auth=False) data = self._parse_response(resp) if "access_token" not in data: raise PyFacebookException( ErrorMessage( code=ErrorCode.INVALID_PARAMS, message= ("Can not change page access token. Confirm: \n" "1. Your user access token has `page_show_list` or `manage_pages` permission.\n" "2. You have the target page's manage permission."))) return data["access_token"]
class BaseApi(object): VALID_API_VERSIONS = ["v3.3", "v4.0"] GRAPH_URL = "https://graph.facebook.com/" INTERVAL_BETWEEN_REQUEST = 1 # seconds def __init__( self, app_id=None, app_secret=None, short_token=None, long_term_token=None, version=None, timeout=None, interval_between_request=None, # if loop get data. should use this. sleep_on_rate_limit=False, proxies=None, is_instagram=False, ): self.app_id = app_id self.app_secret = app_secret self.short_token = short_token self.__timeout = timeout self.base_url = Api.GRAPH_URL self.proxies = proxies self.session = requests.Session() self.sleep_on_rate_limit = sleep_on_rate_limit self.is_instagram = is_instagram self.instagram_business_id = None if self.is_instagram: self.rate_limit = InstagramRateLimit() else: self.rate_limit = RateLimit() self.interval_between_request = interval_between_request if self.interval_between_request is None: self.interval_between_request = Api.INTERVAL_BETWEEN_REQUEST if self.interval_between_request < 1: raise PyFacebookError({"message": "Min interval is 1"}) if version is None: # default version is last new. self.version = Api.VALID_API_VERSIONS[-1] else: version = str(version) if not version.startswith('v'): version = 'v' + version version_regex = re.compile(r"^v\d.\d{1,2}$") match = version_regex.search(str(version)) if match is not None: if version not in Api.VALID_API_VERSIONS: raise PyFacebookError({ "message": "Valid API version are {}".format(",".join( Api.VALID_API_VERSIONS)) }) else: self.version = version else: self.version = Api.VALID_API_VERSIONS[-1] if not (long_term_token or all([self.app_id, self.app_secret, self.short_token])): raise PyFacebookError( {'message': 'Missing long term token or app account'}) if long_term_token: self.token = long_term_token else: self.set_token(app_id=self.app_id, app_secret=self.app_secret, short_token=self.short_token) def set_token(self, app_id, app_secret, short_token): response = self._request(method='GET', path='{}/oauth/access_token'.format( self.version), args={ 'grant_type': 'fb_exchange_token', 'client_id': app_id, 'client_secret': app_secret, 'fb_exchange_token': short_token }, enforce_auth=False) data = self._parse_response(response.content.decode('utf-8')) self.token = data['access_token'] def _request(self, path, method=None, args=None, post_args=None, enforce_auth=True): if method is None: method = 'GET' if args is None: args = dict() if post_args is not None: method = "POST" if enforce_auth: if post_args and "access_token" not in post_args: post_args["access_token"] = self.token elif "access_token" not in args: args["access_token"] = self.token try: if self.sleep_on_rate_limit: interval = self.rate_limit.get_sleep_interval() time.sleep(interval) else: time.sleep(self.interval_between_request) response = self.session.request(method, self.base_url + path, timeout=self.__timeout, params=args, data=post_args, proxies=self.proxies) except requests.HTTPError as e: response = json.loads(e.read()) raise PyFacebookError(response) headers = response.headers # do update app rate limit if self.is_instagram: self.rate_limit.set_limit(headers, self.instagram_business_id) else: self.rate_limit.set_limit(headers) return response def _parse_response(self, json_data): try: data = json.loads(json_data) except ValueError: raise PyFacebookError(json_data) self._check_graph_error(data) return data @staticmethod def _check_graph_error(data): if 'error' in data: try: error = data['error'] raise PyFacebookError(error) except (KeyError, TypeError): raise PyFacebookError({'message': data}) def get_token_info(self, return_json=False): """ Obtain the current access token info if provide the app_id and app_secret. Args: return_json (bool, optional): If True JSON data will be returned, instead of pyfacebook.AccessToken Returns: Current access token's info, pyfacebook.AccessToken instance. """ if all([self.app_id, self.app_secret]): access_token = "{0}|{1}".format(self.app_id, self.app_secret) else: access_token = self.token args = { "input_token": self.token, "access_token": access_token, } resp = self._request('{0}/debug_token'.format(self.version), args=args) data = self._parse_response(resp.content.decode('utf-8')) if return_json: return data else: return AccessToken.new_from_json_dict(data['data'])
def testAppLimit(self): headers = {'x-app-usage': '{"call_count":91,"total_cputime":15,"total_time":12}'} r = RateLimit() r.set_limit(headers) self.assertEqual(r.get_limit().call_count, 91)