<commit_before># coding=utf-8
# Copyright 2021 Google LLC..
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Responsible for campaign creation of Product DSAs to upload into Google Ads
Uploading will initially be only supported through Google Ads Editor
i.e. The expected output is a CSV file.
"""
import collections
import csv
import re
import os
import decimal
import logging
from typing import Any, Dict, List
from google.auth import credentials
from google.cloud import storage
from google.api_core import exceptions
from common import config_utils, file_utils, sheets_utils
from forex_python.converter import CurrencyCodes

# Google Ads Editor header names
CAMP_NAME = 'Campaign'
CAMP_BUDGET = 'Budget'
# BUDGET_TYPE = 'Budget type'
# BID_STRATEGY_TYPE = 'Bid Strategy Type'
# TARGET_ROAS = 'Target ROAS'
# START_DATE = 'Start Date'
# END_DATE = 'End Date'
# AD_SCHEDULE = 'Ad Schedule'
# AD_ROTATION = 'Ad rotation'
# TARGETING_METHOD = 'Targeting method'
# EXCLUSION_MTHOD = 'Exclusion method'
DSA_WEBSITE = 'DSA Website'
DSA_LANG = 'DSA Language'
DSA_TARGETING_SOURCE = 'DSA targeting source'
DSA_PAGE_FEEDS = 'DSA page feeds'
ADGROUP_NAME = 'Ad Group'
ADGROUP_MAX_CPM = 'Max CPM'
ADGROUP_TARGET_CPM = 'Target CPM'
ADGROUP_TYPE = 'Ad Group Type'
TARGET_CONDITION = 'Dynamic Ad Target Condition 1'
TARGET_VALUE = 'Dynamic Ad Target Value 1'
AD_TYPE = 'Ad type'
AD_DESCRIPTION_ORIG = 'Description Line 1#Original'
AD_DESCRIPTION = 'Description Line 1'
IMAGE = 'Image'

# Default campaign names to use
PDSA_PRODUCT_CAMPAIGN_NAME = 'PDSA Products'
PDSA_CATEGORY_CAMPAIGN_NAME = 'PDSA Categories'
AD_DESCRIPTION_MAX_LENGTH = 90
AD_DESCRIPTION_MIN_LENGTH = 35


class GoogleAdsEditorMgr:

  def __init__(self, config: config_utils.Config):
    self._config = config
    self._headers = [
        CAMP_NAME, CAMP_BUDGET, DSA_WEBSITE, DSA_LANG, DSA_TARGETING_SOURCE,
        DSA_PAGE_FEEDS, ADGROUP_NAME, ADGROUP_MAX_CPM, ADGROUP_TARGET_CPM,
        ADGROUP_TYPE, TARGET_CONDITION, TARGET_VALUE, AD_TYPE,
        AD_DESCRIPTION_ORIG, AD_DESCRIPTION, IMAGE
    ]
    self._rows = []
    self._orig_descriptions = {}

  def __create_row(self):
    ''' Creates an object that represents an empty row of the csv file '''
    row = {}
    for header in self._headers:
      row[header] = ''
    return row

  def __split_to_sentances(self, descrption):
    ''' break a paragraph into sentences and return a list '''
    sentenceEnders = re.compile('[.,!?\|]')
    sentenceList = sentenceEnders.split(descrption)
    return sentenceList

  def __get_ad_description(self, product):
    ''' The limit for description is 90 characters, which can be easily exceeded.
    This method will decide what information to take as the ad description. It also
    removes any commas that exist in the string to avoid messing up the CSV file
    If no valid sentance is found, we will leave it empty to be modified from
    Google Ads Editor
    '''
    # Try to use adcustomizers
    if self._config.ad_description_template:
      # NOTE: inside template we have macros like {field},
      # they should be translated to adcustomizer syntax  {=AD_CUSTOMIZER_FEED.field}
      def replacer(match: re.Match):
        return '{=' + self._config.adcustomizer_feed_name + '.' + match.group(
            1) + '}'

      description = re.sub('\{([^}]+)\}', replacer,
                           self._config.ad_description_template)
      if description != self._config.ad_description_template:
        return description

    if len(product.description) <= AD_DESCRIPTION_MAX_LENGTH:
      return product.description

    if len(product.title) <= AD_DESCRIPTION_MAX_LENGTH:
      return product.title

    # The description and title are too long, split them to sentances
    all_sentences = self.__split_to_sentances(
        product.description) + self.__split_to_sentances(product.title)
    for sentence in all_sentences:
      if (len(sentence) >= AD_DESCRIPTION_MIN_LENGTH and
          len(sentence) <= AD_DESCRIPTION_MAX_LENGTH):
        return sentence
    return ''

  def __get_category_description(self, label):
    desc = self._config.category_ad_descriptions.get(label, None)
    if desc is None:
      raise ValueError(
          f'Mapping for label {label} is missing in configuration (category_ad_descriptions)'
      )
    return desc

  def get_headers(self):
    return self._headers

  def set_original_description(self, orig_desc):
    self._orig_descriptions = orig_desc

  def add_campaign(self, name):
    campaign = self.__create_row()
    campaign_details = {
        CAMP_NAME: name,
        DSA_WEBSITE: self._config.dsa_website,
        DSA_LANG: self._config.dsa_lang or '',
        DSA_TARGETING_SOURCE: 'Page feed',
        DSA_PAGE_FEEDS: self._config.page_feed_name
    }
    campaign.update(campaign_details)
    self._rows.append(campaign)

  def add_adgroup(self, campaign_name: str, adgroup_name: str,
                  is_product_level: bool, product, label: str):
    # Add the ad group row
    adgroup = self.__create_row()

    orig_ad_description = self._orig_descriptions.get(
        (campaign_name, adgroup_name)) or ''
    # TODO: current __get_category_description fails if a mapping (label-category) is missing in config
    ad_description = self.__get_ad_description(
        product) if is_product_level else self.__get_category_description(label)
    adgroup_details = {
        CAMP_NAME: campaign_name,
        ADGROUP_NAME: adgroup_name,
        ADGROUP_MAX_CPM: '0.01',
        ADGROUP_TARGET_CPM: '0.01',<commit_msg>Ad standard ad when using ad customizers<commit_after># coding=utf-8
# Copyright 2021 Google LLC..
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Responsible for campaign creation of Product DSAs to upload into Google Ads
Uploading will initially be only supported through Google Ads Editor
i.e. The expected output is a CSV file.
"""
import collections
import csv
import re
import os
import decimal
import logging
from typing import Any, Dict, List
from google.auth import credentials
from google.cloud import storage
from google.api_core import exceptions
from common import config_utils, file_utils, sheets_utils
from forex_python.converter import CurrencyCodes

# Google Ads Editor header names
CAMP_NAME = 'Campaign'
CAMP_BUDGET = 'Budget'
# BUDGET_TYPE = 'Budget type'
# BID_STRATEGY_TYPE = 'Bid Strategy Type'
# TARGET_ROAS = 'Target ROAS'
# START_DATE = 'Start Date'
# END_DATE = 'End Date'
# AD_SCHEDULE = 'Ad Schedule'
# AD_ROTATION = 'Ad rotation'
# TARGETING_METHOD = 'Targeting method'
# EXCLUSION_MTHOD = 'Exclusion method'
DSA_WEBSITE = 'DSA Website'
DSA_LANG = 'DSA Language'
DSA_TARGETING_SOURCE = 'DSA targeting source'
DSA_PAGE_FEEDS = 'DSA page feeds'
ADGROUP_NAME = 'Ad Group'
ADGROUP_MAX_CPM = 'Max CPM'
ADGROUP_TARGET_CPM = 'Target CPM'
ADGROUP_TYPE = 'Ad Group Type'
TARGET_CONDITION = 'Dynamic Ad Target Condition 1'
TARGET_VALUE = 'Dynamic Ad Target Value 1'
AD_TYPE = 'Ad type'
AD_DESCRIPTION_ORIG = 'Description Line 1#Original'
AD_DESCRIPTION = 'Description Line 1'
IMAGE = 'Image'

# Default campaign names to use
PDSA_PRODUCT_CAMPAIGN_NAME = 'PDSA Products'
PDSA_CATEGORY_CAMPAIGN_NAME = 'PDSA Categories'
AD_DESCRIPTION_MAX_LENGTH = 90
AD_DESCRIPTION_MIN_LENGTH = 35


class GoogleAdsEditorMgr:

  def __init__(self, config: config_utils.Config):
    self._config = config
    self._headers = [
        CAMP_NAME, CAMP_BUDGET, DSA_WEBSITE, DSA_LANG, DSA_TARGETING_SOURCE,
        DSA_PAGE_FEEDS, ADGROUP_NAME, ADGROUP_MAX_CPM, ADGROUP_TARGET_CPM,
        ADGROUP_TYPE, TARGET_CONDITION, TARGET_VALUE, AD_TYPE,
        AD_DESCRIPTION_ORIG, AD_DESCRIPTION, IMAGE
    ]
    self._rows = []
    self._orig_descriptions = {}

  def __create_row(self):
    ''' Creates an object that represents an empty row of the csv file '''
    row = {}
    for header in self._headers:
      row[header] = ''
    return row

  def __split_to_sentances(self, descrption):
    ''' break a paragraph into sentences and return a list '''
    sentenceEnders = re.compile('[.,!?\|]')
    sentenceList = sentenceEnders.split(descrption)
    return sentenceList

  def __get_ad_description_from_template(self, product):
    ''' This method tries to generate an ad description using ad customizers template.
        The limit for description is 90 characters.
    '''
    # Try to use adcustomizers
    if self._config.ad_description_template:
      # NOTE: inside template we have macros like {field},
      # they should be translated to adcustomizer syntax  {=AD_CUSTOMIZER_FEED.field}
      def replacer(match: re.Match):
        return '{=' + self._config.adcustomizer_feed_name + '.' + match.group(
            1) + '}'

      description = re.sub('\{([^}]+)\}', replacer,
                           self._config.ad_description_template)
      if description != self._config.ad_description_template:
        return description
      return ''

  def __get_ad_description(self, product):
    ''' The limit for description is 90 characters, which can be easily exceeded.
    This method will decide what information to take as the ad description. It also
    removes any commas that exist in the string to avoid messing up the CSV file
    If no valid sentance is found, we will leave it empty to be modified from
    Google Ads Editor
    '''
    if len(product.description) <= AD_DESCRIPTION_MAX_LENGTH:
      return product.description

    if len(product.title) <= AD_DESCRIPTION_MAX_LENGTH:
      return product.title

    # The description and title are too long, split them to sentances
    all_sentences = self.__split_to_sentances(
        product.description) + self.__split_to_sentances(product.title)
    for sentence in all_sentences:
      if (len(sentence) >= AD_DESCRIPTION_MIN_LENGTH and
          len(sentence) <= AD_DESCRIPTION_MAX_LENGTH):
        return sentence
    return ''

  def __get_category_description(self, label):
    desc = self._config.category_ad_descriptions.get(label, None)
    if desc is None:
      raise ValueError(
          f'Mapping for label {label} is missing in configuration (category_ad_descriptions)'
      )
    return desc

  def get_headers(self):
    return self._headers

  def set_original_description(self, orig_desc):
    self._orig_descriptions = orig_desc

  def add_campaign(self, name):
    campaign = self.__create_row()
    campaign_details = {
        CAMP_NAME: name,
        DSA_WEBSITE: self._config.dsa_website,
        DSA_LANG: self._config.dsa_lang or '',
        DSA_TARGETING_SOURCE: 'Page feed',
        DSA_PAGE_FEEDS: self._config.page_feed_name
    }
    campaign.update(campaign_details)
    self._rows.append(campaign)

  def add_adgroup(self, campaign_name: str, adgroup_name: str,
                  is_product_level: bool, product, label: str):

    if is_product_level:
      ad_description_from_template = self.__get_ad_description_from_template(
          product)
      if ad_description_from_template:
        adgroup = self.__create_row()
        adgroup_details = {
            CAMP_NAME: campaign_name,
            ADGROUP_NAME: adgroup_name,
            ADGROUP_MAX_CPM: '0.01',
            ADGROUP_TARGET_CPM: '0.01',
            ADGROUP_TYPE: 'Dynamic',
            TARGET_CONDITION: 'CUSTOM_LABEL',
            TARGET_VALUE: label,
            AD_TYPE: 'Expanded Dynamic Search Ad',
            AD_DESCRIPTION_ORIG: '',
            AD_DESCRIPTION: ad_description_from_template.strip()
        }
        adgroup.update(adgroup_details)
        self._rows.append(adgroup)

    # TODO: current __get_category_description fails if a mapping (label-category) is missing in config
    # Add the ad group row
    adgroup = self.__create_row()

    orig_ad_description = self._orig_descriptions.get(
        (campaign_name, adgroup_name)) or ''

    ad_description = self.__get_ad_description(
        product) if is_product_level else self.__get_category_description(label)
    adgroup_details = {
        CAMP_NAME: campaign_name,
        ADGROUP_NAME: adgroup_name,
        ADGROUP_MAX_CPM: '0.01',
        ADGROUP_TARGET_CPM: '0.01',
{'fraction_non_alphanumeric': 0.07872210333231427, 'fraction_numerical': 0.0045857535921736475, 'mean_word_length': 3.633498583569405, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1158557', 'n_tokens_mistral': 4021, 'n_tokens_neox': 3909, 'n_words': 1194}