Source code for ppc_robot_lib.reporting.input.account_info

import numpy
import pandas
from django.db import transaction

from ppc_robot_lib import tasks
from ppc_robot_lib.clients.enums import ClientGoalType
from ppc_robot_lib.clients.resolver import ClientAccountGoalResolver, get_resolver_manager
from ppc_robot_lib.clients.types import GoalValue
from ppc_robot_lib import models
from ppc_robot_lib.reporting import exceptions


__all__ = ['join_account_info', 'has_goal', 'get_goal', 'get_budget', 'get_client_id', 'get_billing_period_id']


[docs] def join_account_info( table: pandas.DataFrame, properties=list[str], client_id_column: str = None, context_client_id: bool = False, suffix='_account', ) -> pandas.DataFrame: """ Fetches account information and settings and joins them to the given table. The accounts are fetched either by value in column ``client_id_column``, or by client ID specified in task context. Lookups are always performed by the ``ext_id`` column in the :py:class:`ppc_robot_lib.models.client_account.ClientAccount` model. Properties is either a list of columns to be fetched, any field from :py:class:`ppc_robot_lib.models.client_account.ClientAccount` can be used. If the column already exits in the table, specified suffix is appended to the result name (``_account`` by default). A copy of the given DataFrame with additional details is returned. :param table: Table name to which the account information should be added. :param properties: Properties of the :py:class:`ppc_robot_lib.models.client_account.ClientAccount` model which should be added as columns to the table. :param client_id_column: Use the specified column values as input for the lookup. :param context_client_id: Set to ``True`` if you would like to use client_id stored in the task context. :param suffix: Suffix to add to the columns if they already exists in the table. :return: DataFrame with new information. """ task_ctx = tasks.get_context() if client_id_column is None and context_client_id is False: raise ValueError('You must either specify client_id_column, or set context_client_id to True.') select_columns = properties.copy() select_columns.append('ext_id') with transaction.atomic(): user_credentials = task_ctx.user_credentials service_account_id = task_ctx.target_object.service_account.id if client_id_column is not None: ext_ids = table[client_id_column].unique().tolist() client_account_list = ( models.ClientAccount.objects.filter(service_account_id=service_account_id, ext_id__in=ext_ids) .only(*select_columns) .values() ) # Determine join column type for the conversion. col_type = table[client_id_column].dtype if numpy.issubdtype(col_type, numpy.integer): transform = int elif numpy.issubdtype(col_type, numpy.floating): transform = float else: transform = str # Create index from ext_ids. idx = [transform(row['ext_id']) for row in client_account_list] # Import results to DataFrame. client_list_table = pandas.DataFrame(data=list(client_account_list), index=idx, columns=properties) # Join the account information to the main table. if not client_list_table.empty and not table.empty: return pandas.merge( table, client_list_table, how='left', left_on=client_id_column, right_index=True, sort=False, suffixes=('', suffix), ) else: new_table = table.copy(deep=False) for col in properties: new_table[col] = None return new_table else: client_id = task_ctx.client_id if client_id is None: raise ValueError('Client ID is unknown, cannot fetch account information!') client_accounts = models.ClientAccount.objects.filter( user_id=user_credentials.user_id, service_account_id=service_account_id, ext_id=client_id ).only(*select_columns) client_account: models.ClientAccount = client_accounts.first() if client_account is None: raise ValueError( f'Client account with ID {client_id} for user {user_credentials.user_id} was not found!' ) col_iter = properties new_table = table.copy(deep=False) for column in col_iter: if column not in new_table: new_table[column] = getattr(client_account, column) else: new_table[column + suffix] = getattr(client_account, column) return new_table
def has_goal(goal_type: ClientGoalType, allow_alternative: bool = True) -> bool: """ Checks if a goal of given type is defined for currently active client account. Fails if there the task does not run for a client account and there is no active client account. :param goal_type: Type of the goal. :param allow_alternative: Can we look for the alternative goal (e.g. PNO <-> ROAS)? :return: """ with transaction.atomic(): task_ctx = tasks.get_context() resolver = _get_account_resolver(task_ctx) return resolver.has_goal(goal_type, allow_alternative) def get_goal(goal_type: ClientGoalType, allow_alternative: bool = True, convert=True) -> GoalValue | None: """ Gets a goal of given type defined for currently active client account. Fails if there the task does not run for a client account and there is no active client account. :param goal_type: Type of the goal. :param allow_alternative: Can we look for the alternative goal (e.g. PNO <-> ROAS)? :param convert: Convert the return value to currently active currency? :return: """ with transaction.atomic(): task_ctx = tasks.get_context() resolver = _get_account_resolver(task_ctx) goal_value = resolver.get_goal(goal_type, allow_alternative) if not goal_value: return None if convert and task_ctx.currency: return goal_value.convert_to(task_ctx.currency) else: return goal_value def get_budget(convert=True) -> GoalValue | None: """ Gets a budget defined for currently active client account. Fails if there the task does not run for a client account and there is no active client account. :param convert: Convert the return value to currently active currency? :return: Object with budget amount, ``None`` if no budget is set. """ with transaction.atomic(): task_ctx = tasks.get_context() resolver = _get_account_resolver(task_ctx) budget = resolver.get_budget() if not budget: return None if convert and task_ctx.currency: return budget.convert_to(task_ctx.currency) else: return budget def get_client_id() -> int: """ :return: ID of the currently active client. If the job is run for a client, the client ID is returned. If the job is run for a ClientAccount, an ID of client to which the ClientAccount is linked is returned. :raises exceptions.ClientNotAvailableError: if the job is run for a service account or the ClientAccount is not linked to any client. """ task_ctx = tasks.get_context() target_object = task_ctx.target_object if target_object.is_client(): return target_object.client.id elif target_object.is_client_account(): try: return target_object.client_account.client_link.client_id except models.ClientAccountLink.DoesNotExist: raise exceptions.ClientNotAvailableError( 'Client ID is not available, because the job is run for a ClientAccount that is not linked to a client.' ) else: raise exceptions.ClientNotAvailableError( 'Client ID is available only for jobs run at the client level, or for client accounts linked to a client.' ) def get_billing_period_id() -> int: task_ctx = tasks.get_context() user_id = task_ctx.user_credentials.user_id try: team = models.Team.objects.get(team_user__user_id=user_id) return team.active_billing_period.id except models.Team.DoesNotExist: raise exceptions.BillingPeriodNotAvailableError( 'Billing Period is not available, because the user is not linked to any team.' ) except models.TeamBillingPeriod.DoesNotExist: raise exceptions.BillingPeriodNotAvailableError( 'Billing Period is not available, because the team has no active billing period.' ) def _get_account_resolver(task_ctx) -> ClientAccountGoalResolver: if not task_ctx.target_object or not task_ctx.target_object.is_client_account(): raise ValueError('Goals and budgets for accounts can be retrieved only in client account level tasks.') return get_resolver_manager().get_account_resolver(task_ctx.target_object.client_account.id)