예제 #1
0
def _process_single_day_jobs_concurrently(job_list: typing.List[JobQueueItem],
                                          n_threads: int) -> None:
    if n_threads < 1:
        raise ValueError(
            '_process_single_day_jobs_concurrently should have n_threads > 0')
    heapq.heapify(job_list)
    thread_args: ThreadArgs = ThreadArgs(job_list)
    # store the default API since the worker threads will change it
    default_api: FacebookAdsApi = FacebookAdsApi.get_default_api()

    thread_list: typing.List[threading.Thread] = list()
    thread: threading.Thread = threading.Thread(target=_retry_thread_func,
                                                args=(thread_args, ))
    thread_list.append(thread)
    thread.start()
    for i in range(0, n_threads):
        thread = threading.Thread(target=_job_thread_func,
                                  args=(thread_args, ))
        thread_list.append(thread)
        thread.start()

    thread_args.state_changed_cv.acquire()
    try:
        while (not thread_args.error_occured) and (thread_args.jobs_left > 0):
            thread_args.state_changed_cv.wait()
    except:
        thread_args.error_occured = True
    finally:
        thread_args.state_changed_cv.release()
        # notify all waiting threads, so they can see that they are done
        # release -> aquire ordering matters due to potential deadlocking
        # use a second variable to identify a done variable
        # that requires only a single lock rather than all three
        with thread_args.job_list_cv:
            thread_args.job_thread_done = True
            thread_args.job_list_cv.notify_all()
        with thread_args.retry_queue_cv:
            thread_args.retry_thread_done = True
            thread_args.retry_queue_cv.notify()

    with thread_args.logging_mutex:
        logging.info('waiting for all threads to exit'.format(
            threading.get_ident()))
    for thread in thread_list:
        thread.join()

    # restore the default API in case something else needs it after this function
    FacebookAdsApi.set_default_api(default_api)

    if thread_args.error_occured:
        sys.exit(1)
def _job_thread_func(args: ThreadArgs) -> None:
    job: typing.Optional[JobQueueItem]
    # Api objects do not seem thread safe at all, create on per thread and nuke the default for
    # good measure
    api: FacebookAdsApi = FacebookAdsApi.init(config.app_id(),
                                              config.app_secret(),
                                              config.access_token())
    FacebookAdsApi.set_default_api(None)
    # No need to lock since this can only change to True, so  worst case this does one extra
    # iteration. Also assignment / reading of booleans should be atomic anyway.
    while not args.done:

        job = _get_job_from_queue(args)
        if job is None:
            continue

        _process_job(args, job, api)

    _log(logging.info, args.logging_mutex,
         ['thread {0} exited'.format(threading.get_ident())])
예제 #3
0
def _job_thread_func(args: ThreadArgs) -> None:
    job: typing.Optional[JobQueueItem]
    # Api objects do not seem thread safe at all, create on per thread and nuke the default for
    # good measure
    api: FacebookAdsApi = FacebookAdsApi.init(config.app_id(),
                                              config.app_secret(),
                                              config.access_token())
    FacebookAdsApi.set_default_api(None)
    with args.job_list_cv:
        while not args.job_thread_done:

            if len(args.job_list) > 0:
                job = heapq.heappop(args.job_list)
                args.job_list_cv.release()
                _process_job(args, job, api)
                args.job_list_cv.acquire()
            else:
                args.job_list_cv.wait()

    _log(logging.info, args.logging_mutex,
         ['thread {0} exited'.format(threading.get_ident())])
예제 #4
0
    def init(cls,
             app_id=None,
             app_secret=None,
             access_token=None,
             account_id=None,
             api_version=None,
             proxies=None,
             timeout=None,
             pool_maxsize=10,
             max_retries=0):
        # connection pool size is +1 because there also is the main thread
        #  that can also issue a request
        session = FacebookSession(app_id, app_secret, access_token,
                                  proxies=proxies, timeout=timeout,
                                  pool_maxsize=pool_maxsize+1, max_retries=max_retries)
        api = cls(session, api_version=api_version, threadpool_size=pool_maxsize)
        cls.set_default_api(api)
        # TODO: how to avoid this hack?
        FacebookAdsApi.set_default_api(api)

        if account_id:
            cls.set_default_account_id(account_id)
예제 #5
0
    def _eval_context_facebook(self, secrets, eval_context):
        """Adds Facebook SDK classes:
        * FB.Application
        * FB.Lead
        Docs:
        * https://github.com/facebook/facebook-python-business-sdk/tree/master/facebook_business/adobjects

        The classes use App Access Token. To switch to other access levels, pass api= parameter on class instance initialization.
        * FB_TOKEN.page
        * FB_TOKEN.user

        Token generation tools.
        * FB_EXCHANGE_TOKEN.user2page
        Docs:
        * https://developers.facebook.com/docs/facebook-login/access-tokens
        * https://developers.facebook.com/docs/pages/access-tokens

        General methods for graph API:
        * FB_GRAPH_API.app(method, url, **kwargs)
        * FB_GRAPH_API.page(method, url, **kwargs)
        * FB_GRAPH_API.user(method, url, **kwargs)
        Docs:
        * https://developers.facebook.com/docs/graph-api/
        * https://docs.python-requests.org/en/latest/api/#requests.request
        """
        log_transmission = eval_context["log_transmission"]
        log = eval_context["log"]
        LOG_INFO = eval_context["LOG_INFO"]
        LOG_ERROR = eval_context["LOG_ERROR"]
        LOG_CRITICAL = eval_context["LOG_CRITICAL"]
        params = eval_context["params"]
        if not all([params.APP_ID, secrets.APP_SECRET]):
            raise UserError(_("Facebook Credentials are not set"))

        access_token_app = params.APP_ID + "|" + secrets.APP_SECRET
        access_token_page = secrets.PAGE_ACCESS_TOKEN
        access_token_user = secrets.USER_ACCESS_TOKEN

        def _graph_api(token_type, access_token, method, path, **kwargs):
            url = f"https://graph.facebook.com/{params.GRAPH_API_VERSION}/" + path
            log_transmission("Facebook Graph API (%s)" % token_type,
                             "{}\n{}".format(path, kwargs))
            kwargs.setdefault("timeout", 5)
            if access_token:
                kwargs.setdefault("data",
                                  {}).setdefault("access_token", access_token)
            response = requests.request(method, url, **kwargs)
            log_level = LOG_INFO
            try:
                data = response.json()
                if "error" in data:
                    log_level = LOG_ERROR
            except Exception:
                log_level = LOG_CRITICAL
            log("Graph API RESPONSE:\n{}".format(response.text), log_level)
            return response

        def graph_api_app(method, path, **kwargs):
            return _graph_api(
                "App",
                access_token_app,
                method,
                "%s/%s" % (params.APP_ID, path),
                **kwargs,
            )

        def graph_api_page(method, path, **kwargs):
            return _graph_api(
                "Page",
                access_token_page,
                method,
                "%s/%s" % (params.PAGE_ID, path),
                **kwargs,
            )

        def graph_api_user(method, path, **kwargs):
            return _graph_api(
                "User",
                access_token_user,
                method,
                "%s/%s" % (params.USER_ID, path),
                **kwargs,
            )

        # Patch FacebookAdsApi class to add transmission logs
        class Api(FacebookAdsApi):
            def call(
                self,
                method,
                path,
                params=None,
                headers=None,
                files=None,
                url_override=None,
                api_version=None,
            ):
                session_name = "unknown session"
                if self._session == session_app:
                    session_name = "App"
                elif self._session == session_app:
                    session_name = "Page"
                elif self._session == session_user:
                    session_name = "User"

                log_transmission(
                    "Facebook (%s)" % session_name,
                    "{} {}\nparams: {}\nheaders: {}\nfiles: {}\nurl_override: {}\napi_version: {}"
                    .format(
                        method,
                        path,
                        params,
                        headers,
                        files.keys() if files else None,
                        url_override,
                        api_version,
                    ),
                )
                fb_response = super().call(
                    method,
                    path,
                    params=params,
                    headers=headers,
                    files=files,
                    url_override=url_override,
                    api_version=api_version,
                )
                log("RESPONSE:\n{}".format(fb_response.json()))
                return fb_response

        session_app = FacebookSession(
            params.APP_ID,
            secrets.APP_SECRET,
            access_token_app,
        )
        session_page = FacebookSession(
            params.APP_ID,
            secrets.APP_SECRET,
            access_token_page or
            access_token_app,  # TODO: if page token is not available, then page api should not be available
        )
        session_user = FacebookSession(
            params.APP_ID,
            secrets.APP_SECRET,
            access_token_user,
        )

        api_app = Api(session_app)
        api_page = Api(session_page)
        api_user = Api(session_user)
        FacebookAdsApi.set_default_api(api_app)

        def _user2user():
            res = _graph_api(
                "User",
                None,
                "GET",
                "oauth/access_token",
                params={
                    "grant_type": "fb_exchange_token",
                    "client_id": params.APP_ID,
                    "client_secret": secrets.APP_SECRET,
                    "fb_exchange_token": secrets.USER_ACCESS_TOKEN,
                },
            )
            return res.json()["access_token"]

        def user2page():
            user_token = _user2user()
            res = _graph_api(
                "User",
                None,
                "GET",
                "%s" % params.PAGE_ID,
                params={
                    "fields": "access_token",
                    "access_token": user_token,
                },
            )
            page_token = res.json()["access_token"]
            secret_param = self.env["sync.project.secret"].search([
                ("key", "=", "PAGE_ACCESS_TOKEN"), ("project_id", "=", self.id)
            ])
            if not secret_param:
                log("secret PAGE_ACCESS_TOKEN is not found", LOG_WARNING)
            else:
                secret_param.sudo().write({"value": page_token})
                log("secret PAGE_ACCESS_TOKEN is successfully updated")

        return {
            "FB":
            AttrDict(
                Application=Application,
                Lead=Lead,
            ),
            "FB_TOKEN":
            AttrDict(
                page=api_page,
                user=api_user,
            ),
            "FB_EXCHANGE_TOKEN":
            AttrDict(user2page=user2page, ),
            "FB_GRAPH_API":
            AttrDict(
                app=graph_api_app,
                page=graph_api_page,
                user=graph_api_user,
            ),
        }