import json
import configparser
import os
import requests
from pathlib import Path
import sqlite3
import pandas
from math import ceil
import datetime

class SearchAnalyzer():
    session = requests.Session()

    __BASE_API_URL = 'https://line.pr-cy.ru/api/v1.1.0'
    low_price = 0.25
    high_price = 0.3

    __tasks_resaults = []

    def __init__(self, *, key: str = None, use_config: bool = True, search_engine: str, search_type: str, search_depth: str, file_path: str, search_region: int = 1) -> None:
        if use_config:
            config = configparser.ConfigParser()
            config.read(os.path.join('cfg', 'config.ini'))
            self.__API_KEY = config['DEFAULT']['API_KEY']
            self.low_price = config['DEFAULT']['low_price']
            self.height_price = config['DEFAULT']['high_price']
            if search_engine == 'yandex':
                self.region_id = config['DEFAULT']['REGION_ID_Yandex']
            if search_engine == 'google':
                self.region_id = config['DEFAULT']['REGION_ID_Google']

        else:
            if not key:
                raise ValueError(
                    'Должен быть указан API ключ при инициализации класа, либо же использоваться use_config = True')
            else:
                self.__API_KEY = key

        self.search_engine = search_engine
        self.search_type = search_type
        self.search_depth = search_depth
        if Path(file_path).is_file():
            self.query_file_path = file_path
        else:
            raise FileExistsError('Файл не найден!')

    def check_balanc(self, keywords):
        balance = self.get_balance(self.session)
        if len(keywords) > 100:
            if (balance - (len(keywords)*self.low_price)) > 0:
                print(
                    f'Баланс составляет: {balance}, стоимость текущего задания ~ { len(keywords)*self.low_price }')
                return True
            else:
                print(
                    f'Баланс составляет: {balance}, стоимость текущего задания ~ { len(keywords)*self.low_price }')
                return False

        if len(keywords) <= 100:
            if (balance - (len(keywords)*self.high_price)) > 0:
                print(
                    f'Баланс составляет: {balance}, стоимость текущего задания ~ { len(keywords)*self.high_price }')
                return True
            else:
                print(
                    f'Баланс составляет: {balance}, стоимость текущего задания ~ { len(keywords)*self.high_price }')
                return False

    def get_balance(self,):
        response = self.session.get(
            self.__BASE_API_URL+'/user/balance', params={'key': self.__API_KEY})
        if response.status_code == 200:
            return json.loads(response.text)['balance']
        else:
            print(f"Вохникла ошибка: {json.loads(response.text).get('error')}")
            return None

    def creat_task(self, keywords):
        """Принимает до 1000 ключевых запросов"""
        if len(keywords) >=1000:
            raise ValueError('Передано слишком много ключей на проверку')

        response = self.session.post(self.__BASE_API_URL+'/task/create', params = {'key' : self.__API_KEY},
                    data = {
                    'keywords[]': keywords,
                    'engine' : self.search_engine,
                    'regionId' : self.region_id,
                    'device' : self.search_type,
                    'language' : 'ru'
                    })
        if response.status_code == 200:
            taskId = json.loads(response.text)['taskId']
            return taskId
        else:
            raise Exception(f'Ошибка создания задачи\n{response.status_code}\n{json.loads(response.text)}')

    def task_status_check(self, taskId):
        response = self.session.get(self.__BASE_API_URL+ f'/task/status/{taskId}', params = {'key': self.__API_KEY})
        if response.status_code == 200:
            status = json.loads(response.text)['status']
            if status == 'done':
                return True
            else:
                print(f"Статус задачи {taskId}: {status}")
                return False
        else:
            raise Exception(f'Ошибка проверки статуса задачи\n{response.status_code}\n{json.loads(response.text)}')

    def get_keywords_from_file(self) -> list:
        """return : Возращает список со списками ключевых слов из файла с количеством ключевиков до 1000 шт"""
        df = pandas.read_excel(self.query_file_path, header=None)
        data = df.iloc[ : , 0].tolist()
        self.keywords_count = len(data)
        self.keywords = []
        if len(data) >=1000:
            n = ceil( len(data)/ ceil(len(data)/999))
            for x in range(0, len(data), n):
                e_c = data[x : n + x]
                self.keywords.append(e_c)
        else:
            self.keywords.append(data)
        return self.keywords

    def get_task_resault(self, taskId):
        response = self.session.get(self.__BASE_API_URL+f'/task/result/{taskId}', params = {'key':self.__API_KEY})
        if response.status_code == 200:
            result = json.loads(response.text)
            self.__tasks_resaults.append(result)
            return result
        else:
            raise ConnectionError(f'Не удалось получить результаты {taskId}')

    @property
    def tasks_resaults(self):
        return [item for item in self.__tasks_resaults]

    def get_distinct_hosts(self):
        distinct_hosts = []
        for task_resault in self.__tasks_resaults:
            for keyword in task_resault['keywords']:
                for resault in keyword['serp'][:self.search_depth]:
                    host = resault['host'].replace('www.', '').lower()
                    if host not in distinct_hosts:
                        distinct_hosts.append(host)
        return distinct_hosts

    def get_query_resaults(self):
        self.final_data = []
        for task_resaults in self.__tasks_resaults:
            for keyword in task_resaults['keywords']:
                for resault in keyword['serp'][:self.search_depth]:
                    res={}
                    res['query'] = keyword['query']
                    res['position'] = resault['position']
                    res['url'] = resault['url']
                    res['host'] = resault['host']
                    res['title'] = resault['title']
                    self.final_data.append(res)

        return self.final_data

    def get_distinct_hosts_statistic(self, ):
        result = []
        for distinct_host in self.get_distinct_hosts():
            counter = 0
            for task_resaults in self.tasks_resaults:
                for keyword in task_resaults['keywords']:
                    for resault in keyword['serp'][:self.search_depth]:
                        if distinct_host.lower() in resault['host'].lower():
                            counter +=1
                            break;
            result.append(
                (distinct_host.lower(), round((counter / self.keywords_count)*100, 2))
            )
        return result

    def get_distinct_hosts_statistic_top10(self, ):
        result = []
        for distinct_host in self.get_distinct_hosts():
            counter = 0
            for task_resaults in self.tasks_resaults:
                for keyword in task_resaults['keywords']:
                    for resault in keyword['serp'][:10]:
                        if distinct_host.lower() in resault['host'].lower():
                            counter +=1
                            break;
            result.append(
                (distinct_host.lower(), round((counter / self.keywords_count)*100, 2))
            )
        return result

class DomainAnalyzer():

    session = requests.Session()

    __BASE_API_URL = 'https://apis.pr-cy.ru/api/v1.1.0'
    __tasks_resaults = []

    api_query_count = 0

    def __init__(self, *, key: str = None, use_config: bool = True, file_path: str = None ) -> None:
        if use_config:
            config = configparser.ConfigParser()
            config.read(os.path.join('cfg', 'config.ini'))
            self.__API_KEY = config['DEFAULT']['DOMAIN_API_KEY']
            self.google_spreadsheet_url = config['DEFAULT']['Google_spreadsheet_url'] + '/export?format=xlsx'
            self.FRESHNESS = int(config['DEFAULT']['FRESHNESS_days'])

        else:
            if not key:
                raise ValueError(
                    'Должен быть указан API ключ при инициализации класа, либо же использоваться use_config = True')
            else:
                self.__API_KEY = key

        if Path(file_path).is_file():
            self.query_file_path = file_path
        else:
            raise FileExistsError('Файл не найден!')

    @property
    def tasks_resaults(self):
        return self.__tasks_resaults

    def get_domains_from_file(self) -> list:
        df = pandas.read_excel(self.query_file_path, header=None)
        data = df.iloc[ : , 0].tolist()
        self.domains_count = len(data)
        self.domains = data
        if self.domains_count >= 200:
            print(f'ПРЕДУПРЕЖДЕНИЕ! Количество доменов для анализа привышает 200 ({self.domains_count}), \nОграничение api: 200 запросов в час, обработка может занять много времени')
        return data

    def get_data_from_google_sheet(self):
        # with sqlite3.connect('local.db') as connection:
            # create a cursor object from the cursor class
            # cur = connection.cursor()
        df = pandas.read_excel(self.google_spreadsheet_url, skiprows=1)
        return df

    def update_domain_analysis(self, domain):
        url = f'/analysis/update/base/{domain}'
        response = requests.post(self.__BASE_API_URL+url, params={'key':self.__API_KEY})
        self.api_query_count+=1
        if response.status_code != 200:
            print(f'Ошибка при попытке отправки запроса на обновление {domain}')
            return False

    def get_domain_analysis(self, domain):
        url = f'/analysis/base/{domain}'
        response = requests.get(self.__BASE_API_URL+url,
                                params={
                                            'key': self.__API_KEY,
                                            'tests':'avgVisitDuration,googleIndex,ip,mainPageExternalLinks,megaindexLinksCount,pageSpeedDesktop,pageSpeedMobile,pagesPerVisit,publicStatistics,sitemap,trafficGeography,trafficSources,whoisCreationDate,yandexAchievements,yandexIndex,yandexReviews',
                                            'excludeHistory': 1
                                        }
                                )
        self.api_query_count+=1
        if response.status_code == 429:
            return False
        data = json.loads(response.text)
        data['host'] = domain
        self.__tasks_resaults.append(data)
        return data

    def check_domain_status(self, domain):
        url = f'/analysis/status/base/{domain}'
        response = requests.get(self.__BASE_API_URL+url, params={'key': self.__API_KEY})
        response_content = json.loads(response.text)
        if response.status_code != 200:
            return (False, 'new')
        if not response_content.get('updated'):
            return (False, 'updating')
        if datetime.datetime.strptime(response_content.get('updated'), "%Y-%m-%dT%H:%M:%S+03:00") >= datetime.datetime.now()- datetime.timedelta(days=self.FRESHNESS) and \
                not response_content.get('isUpdating'):
            return (True, 'Ок')
        if datetime.datetime.strptime(response_content.get('updated'), "%Y-%m-%dT%H:%M:%S+03:00") <= datetime.datetime.now()- datetime.timedelta(days=self.FRESHNESS):
            return (False, 'overdue')
        if response_content.get('isUpdating'):
            return (False, 'updating')

    def get_final_records(self):
        google_df = self.get_data_from_google_sheet()
        final_data = []
        for host_info in self.__tasks_resaults:

            res={}
            res['host'] = host_info['host']
            try: res['googleIndex'] = host_info['googleIndex']['googleIndex']
            except: res['googleIndex'] = ''
            try: res['yandexIndex'] = host_info['yandexIndex']['yandexIndex']
            except: res['yandexIndex'] = ''
            try: res['Длина визита, сек'] = host_info['avgVisitDuration']['avgVisitDuration']
            except: res['Длина визита, сек'] = ''
            try: res['ip'] = host_info['ip']['ip']
            except: res['ip'] = ''
            try: res['Кол-во внешних ссылок по Мегаиндекс'] = host_info['megaindexLinksCount']['megaindexLinksCount']
            except: res['Кол-во внешних ссылок по Мегаиндекс'] = ''
            try: res['ПК pageSpeed оценка'] = host_info['pageSpeedDesktop']['pageSpeed']['score']
            except: res['ПК pageSpeed оценка'] = ''
            try: res['ПК pageSpeed в секундах'] = host_info['pageSpeedDesktop']['pageSpeed']['value']
            except:  res['ПК pageSpeed в секундах'] =''
            try: res['Мобильный pageSpeed оценка'] = host_info['pageSpeedMobile']['pageSpeed']['score']
            except:  res['Мобильный pageSpeed оценка']  = ''
            try: res['Мобильный pageSpeed в секундах'] = host_info['pageSpeedMobile']['pageSpeed']['value']
            except: res['Мобильный pageSpeed в секундах'] = ''
            try: res['Просмотры в мес'] = host_info['publicStatistics']['publicStatisticsPageViewsMonthly']
            except: res['Просмотры в мес'] = ''
            try: res['Посетители в мес'] = host_info['publicStatistics']['publicStatisticsPrcyVisitsMonthly']
            except: res['Посетители в мес'] = ''
            try: res['sitemap'] = host_info['sitemap']['sitemapUrl']
            except: res['sitemap'] = ''
            try: res['Трафик по странам'] = host_info['trafficGeography']['topCountryGeography']
            except: res['Трафик по странам'] = ''
            try: res['Ист. траф. Прямые заходы'] = host_info['trafficSources']['trafficSourcesDirect']/100
            except: res['Ист. траф. Прямые заходы'] = ''
            try: res['Ист. траф. Почтовые рассылки'] = host_info['trafficSources']['trafficSourcesMail']/100
            except: res['Ист. траф. Почтовые рассылки'] = ''
            try: res['Ист. траф. Ссылки на сайтах'] = host_info['trafficSources']['trafficSourcesReferrals']/100
            except:  res['Ист. траф. Ссылки на сайтах'] =''
            try: res['Ист. траф. Поисковые системы'] = host_info['trafficSources']['trafficSourcesSearch']/100
            except: res['Ист. траф. Поисковые системы'] = ''
            try: res['Ист. траф. Соц сети'] = host_info['trafficSources']['trafficSourcesSocial']/100
            except: res['Ист. траф. Соц сети'] = ''
            try: res['Дата создания сайта'] = datetime.datetime.strptime(host_info['whoisCreationDate']['whoisCreationDate'], '%Y-%m-%dT%H:%M:%S+03:00')
            except: res['Дата создания сайта'] = ''
            try: res['Сайт на https?'] = host_info['yandexAchievements']['yandexAchievementsHttps']
            except: res['Сайт на https?']  = ''
            try: res['Есть Турбо?'] = host_info['yandexAchievements']['yandexAchievementsTurbo']
            except: res['Есть Турбо?'] = ''
            try: res['Отзывов в Яндекс'] = host_info['yandexReviews']['count']
            except: res['Отзывов в Яндекс'] = ''
            try: res['Внешних ссылок на главную'] = host_info['mainPageExternalLinks']['externalIndexCount']
            except: res['Внешних ссылок на главную'] = ''
            try: res['Наш партнер?'] = google_df.loc[google_df['Домен'] == host_info['host']].iloc[0]['Наш партнер?']
            except: res['Наш партнер?'] = ''
            try:res['Ответственный менеджер'] = google_df.loc[google_df['Домен'] == host_info['host']].iloc[0]['Ответственный менеджер']
            except: res['Ответственный менеджер'] = ''
            try:res['Тип партнера'] = google_df.loc[google_df['Домен'] == host_info['host']].iloc[0]['Тип партнера']
            except:res['Тип партнера'] =''

            final_data.append(res)

        return final_data

class SearchRepeater():
    __tasks_resaults = []
    __BASE_API_URL = 'https://line.pr-cy.ru/api/v1.1.0'
    session = requests.Session()

    def __init__(self, search_depth, keywords_count):
        config = configparser.ConfigParser()
        config.read(os.path.join('cfg', 'config.ini'))
        self.__API_KEY = config['DEFAULT']['API_KEY']
        self.search_depth = search_depth

        self.keywords_count = keywords_count

    def task_status_check(self, taskId):
        response = self.session.get(self.__BASE_API_URL+ f'/task/status/{taskId}', params = {'key': self.__API_KEY})
        if response.status_code == 200:
            status = json.loads(response.text)['status']
            if status == 'done':
                return True
            else:
                print(f"Статус задачи {taskId}: {status}")
                return False
        else:
            raise Exception(f'Ошибка проверки статуса задачи\n{response.status_code}\n{json.loads(response.text)}')

    @property
    def tasks_resaults(self):
        return [item for item in self.__tasks_resaults]

    def get_distinct_hosts(self):
        distinct_hosts = []
        for task_resault in self.__tasks_resaults:
            for keyword in task_resault['keywords']:
                for resault in keyword['serp'][:self.search_depth]:
                    host = resault['host'].replace('www.', '').lower()
                    if host not in distinct_hosts:
                        distinct_hosts.append(host)
        return distinct_hosts

    def get_query_resaults(self):
        self.final_data = []
        for task_resaults in self.__tasks_resaults:
            for keyword in task_resaults['keywords']:
                for resault in keyword['serp'][:self.search_depth]:
                    res={}
                    res['query'] = keyword['query']
                    res['position'] = resault['position']
                    res['url'] = resault['url']
                    res['host'] = resault['host']
                    res['title'] = resault['title']
                    self.final_data.append(res)

        return self.final_data

    def get_distinct_hosts_statistic(self, ):
        result = []
        for distinct_host in self.get_distinct_hosts():
            counter = 0
            for task_resaults in self.tasks_resaults:
                for keyword in task_resaults['keywords']:
                    for resault in keyword['serp'][:self.search_depth]:
                        if distinct_host.lower() in resault['host'].lower():
                            counter +=1
                            break;
            result.append(
                (distinct_host.lower(), round((counter / self.keywords_count)*100, 2))
            )
        return result

    def get_distinct_hosts_statistic_top10(self, ):
        result = []
        for distinct_host in self.get_distinct_hosts():
            counter = 0
            for task_resaults in self.tasks_resaults:
                for keyword in task_resaults['keywords']:
                    for resault in keyword['serp'][:10]:
                        if distinct_host.lower() in resault['host'].lower():
                            counter +=1
                            break;
            result.append(
                (distinct_host.lower(), round((counter / self.keywords_count)*100, 2))
            )
        return result

    def get_task_resault(self, taskId):
        response = self.session.get(self.__BASE_API_URL+f'/task/result/{taskId}', params = {'key':self.__API_KEY})
        if response.status_code == 200:
            result = json.loads(response.text)
            self.__tasks_resaults.append(result)
            return result
        else:
            raise ConnectionError(f'Не удалось получить результаты {taskId}')


from random import choice
import os
import argparse

dir_path = os.path.dirname(os.path.realpath(__file__))

with open(os.path.join(dir_path, 'safe.txt'), encoding='utf-8') as f:
	safe_facts = [
		fact.rstrip('\r\n ')
		for fact in f.readlines()
		if fact.rstrip('\r\n ') != ''
	]

with open(os.path.join(dir_path, 'unsafe.txt'), encoding='utf-8') as f:
	unsafe_facts = [
		fact.rstrip('\r\n ')
		for fact in f.readlines()
		if fact.rstrip('\r\n ') != ''
	]

all_facts = safe_facts + unsafe_facts

def get_fact(filter_enabled: bool = True, only_unsafe: bool = False) -> str:
	"""This function returns a random fact.

	Parameters
	----------
	filter_enabled : bool
		The `filter_enabled` parameter determines if the function will filter
		out potentially inappropriate facts. Defaults to True.

	only_unsafe : bool
		The `only_unsafe` parameter determines if the function will only give
		unsafe (NSFW) facts. Takes precedence over the `filter_enabled` argument.

	Returns
	------
	str
		A random fact.

	"""

	if only_unsafe:
		return choice(unsafe_facts)
	if filter_enabled is False:
		return choice(all_facts)
	return choice(safe_facts)

def _cli_entrypoint():
	"""Entrypoint for execution via command-line."""

	parser = argparse.ArgumentParser(
		description='Generate random facts from the command-line'
	)

	group = parser.add_mutually_exclusive_group()
	group.add_argument(
		'-m',
		'--mixed',
		action='store_true',
		help='Include safe and unsafe facts'
	)

	group.add_argument(
		'-u',
		'--unsafe',
		action='store_true',
		help='Only include unsafe facts'
	)

	args = parser.parse_args()

	if args.mixed:
		print(get_fact(False))
	elif args.unsafe:
		print(get_fact(only_unsafe=True))
	else:
		print(get_fact())

if __name__ == '__main__':
	_cli_entrypoint()


import os
import json
import boto3
from aws_lambda_powertools import Logger, Tracer

sns_client = boto3.client("sns")
TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN")
# Initialize Logger and Tracer
tracer = Tracer()
logger = Logger()

@tracer.capture_lambda_handler
def lambda_handler(event, context):
    print(json.dumps(event))
    try:
        logger.debug(f"Received event: {json.dumps(event)}")

        event_name = event[0]['eventName']
        logger.info(f"event_name")
        if event_name in [
                "INSERT",
                "MODIFY"
                ]:
            new_image = event[0]['dynamodb'].get("NewImage")
            if (
                    new_image and 'OrderId' in new_image
                    ):
                sns_client.publish(
                        TopicArn=TOPIC_ARN,
                        Message=f"New entry added: {json.dumps(new_image)}",
                        Subject="New Entry in DDB"
                        )

        logger.info("SNS Sent successfully!")
        return {
            "statusCode": 200,
            "body": json.dumps("Function executed successfully!"),
        }

    except Exception as e:
        logger.error(f"Error in processing: {str(e)}")
        return {"statusCode": 500, "body": json.dumps(f"Error in processing: {str(e)}")}


"""DenseNet as encoder backbone.

Reference: https://arxiv.org/pdf/1608.06993.pdf
"""

import torch
from torch import nn
import numpy as np
import math

from training import CONFIGS_DIR, load_config

class DenseNet(nn.Module):

    @classmethod
    def from_config(cls, architecture_name, phase):
        """
        Args:
            phase (str): 'training' or 'deployment'
        """
        densenet_config = load_config(architecture_name+'.yaml')

        config = load_config('training.yaml') if phase=='training' else load_config('deployment.yaml')

        densenet_config.update(config)

        densenet_parameters = {**densenet_config}
        return DenseNet(**densenet_parameters)

    def __init__(self,
                 initial_conv_output_channels,
                 num_conv_blocks_per_stage,
                 output_channels_per_stage,
                 growth_rate_per_stage,
                 do_downsampling_per_stage,
                 provide_as_output_per_stage,
                 dropout_rate,
                 **extra_arguments):
        super().__init__()

        module_list = []
        block_input_channels = initial_conv_output_channels

        self.is_output_module = []

        for (num_conv_blocks,
             output_channels,
             growth_rate,
             do_downsampling,
             provide_as_output,) in zip(num_conv_blocks_per_stage,
                                        output_channels_per_stage,
                                        growth_rate_per_stage,
                                        do_downsampling_per_stage,
                                        provide_as_output_per_stage):

            # debug output
            # print('densenet stage in, out', block_input_channels, output_channels)

            dense_block = DenseBlock(input_channels=block_input_channels,
                                     num_conv_blocks=num_conv_blocks,
                                     growth_rate=growth_rate,
                                     activation='leaky_relu',
                                     dropout_rate=dropout_rate)

            module_list.append(dense_block)
            self.is_output_module.append(False)

            # debug output
            # print('denseblock out', dense_block.get_output_channels())

            transition_block = DenseTransitionBlock(input_channels=dense_block.get_output_channels(),
                                                    output_channels=output_channels)

            module_list.append(transition_block)
            self.is_output_module.append(True if provide_as_output else False)

            # downsampling

            if do_downsampling:
                downsampling = DenseDownsamplingBlock(input_channels=output_channels,
                                                      output_channels=output_channels)

                module_list.append(downsampling)
                self.is_output_module.append(False)

            block_input_channels = output_channels

        self.module_list = nn.ModuleList(module_list)

    def forward(self, x):
        outputs = []
        for module, provide_as_ouput in zip(self.module_list, self.is_output_module):
            x = module(x)

            if provide_as_ouput:
                outputs.append(x)

        # debug output
        # print('densenet all out:')
        # for output in outputs:
            # print('    * '+str(output.shape))

        return outputs

class DenseBlock(nn.Module):

    def __init__(self,
                 input_channels,
                 num_conv_blocks,
                 growth_rate,
                 activation,
                 dropout_rate):
        super().__init__()

        conv_blocks = []

        for conv_block_index in range(num_conv_blocks):
            conv_block = DenseConvBlockWithBottleneck(input_channels+conv_block_index*growth_rate,
                                                      growth_rate=growth_rate,
                                                      activation=activation,
                                                      dropout_rate=dropout_rate)
            conv_blocks.append(conv_block)

        self.conv_blocks = nn.ModuleList(conv_blocks)
        self.output_channels = input_channels+num_conv_blocks*growth_rate

    def get_output_channels(self):
        return self.output_channels

    def forward(self, x):
        block_outputs = [x]

        for conv_block_index, conv_block in enumerate(self.conv_blocks):
            x = torch.cat(block_outputs, dim=1)
            block_outputs.append(conv_block(x))

        # print('dense block all outputs so far:')
        # for output in block_outputs:
            # print('    * '+str(output.shape))

        final_output = torch.cat(block_outputs, dim=1)

        # print('final dense block out', final_output.shape)

        return final_output

class DenseConvBlockWithBottleneck(nn.Sequential):
    """
    A 1x1 and a 3x3 convolution combined. Denoted as DenseNet-B in the reference.
    """
    def __init__(self,
                 input_channels,
                 growth_rate,
                 activation,
                 dropout_rate):
        super().__init__()

        self.add_module('batch_norm_1', nn.BatchNorm2d(input_channels))

        if activation=='relu':
            self.add_module('relu_1', nn.ReLU())
        elif activation=='leaky_relu':
            self.add_module('leaky_relu_1', nn.LeakyReLU())
        elif activation is None or activation=='none':
            pass
        else:
            warnings.warn("Convolutional block with not-supported activation '{}'.".format(activation))

        self.add_module('conv_1', nn.Conv2d(input_channels,
                                            out_channels=4*growth_rate,
                                            kernel_size=1,
                                            stride=1,
                                            padding=0,
                                            bias=False))

        self.add_module('batch_norm_2', nn.BatchNorm2d(4*growth_rate))

        if activation=='relu':
            self.add_module('relu_2', nn.ReLU())
        elif activation=='leaky_relu':
            self.add_module('leaky_relu_2', nn.LeakyReLU())
        elif activation is None or activation=='none':
            pass
        else:
            warnings.warn("Convolutional block with not-supported activation '{}'.".format(activation))

        self.add_module('conv_2', nn.Conv2d(4*growth_rate,
                                            out_channels=growth_rate,
                                            kernel_size=3,
                                            stride=1,
                                            padding=1,
                                            bias=False))

        if dropout_rate is not None and dropout_rate>0.0:
            self.add_module('dropout', nn.Dropout2d(dropout_rate))

class DenseTransitionBlock(nn.Sequential):
    def __init__(self,
                 input_channels,
                 output_channels):
        super().__init__()

        self.add_module('batch_norm', nn.BatchNorm2d(input_channels))

        self.add_module('conv', nn.Conv2d(input_channels,
                                          out_channels=output_channels,
                                          kernel_size=1,
                                          stride=1,
                                          padding=0,
                                          bias=False))

class DenseDownsamplingBlock(nn.Sequential):
    """
    Note we use batch norm and a 3x3 convolution with stride 2 here instead
    of average pooling as in the reference.
    """
    def __init__(self,
                 input_channels,
                 output_channels):
        super().__init__()

        self.add_module('batch_norm', nn.BatchNorm2d(input_channels))

        self.add_module('conv', nn.Conv2d(input_channels,
                                          out_channels=output_channels,
                                          kernel_size=3,
                                          stride=2,
                                          padding=1,
                                          bias=False))


"""
Given A, B, C, find any string of maximum length that can be created such that no 3 consecutive characters are same. There can be at max A 'a', B 'b' and C 'c'.

Example 1:

Input: A = 1, B = 1, C = 6
Output: "ccbccacc"
Example 2:

Input: A = 1, B = 2, C = 3
Output: "acbcbc"
"""

import unittest
import heapq

class Solution:
    def longestString(self, A: int, B: int, C: int) -> str:
        heap = [(-A, "a"), (-B, "b"), (-C, "c")]
        heapq.heapify(heap)
        res = []
        while heap:
            cnt1, char1 = heapq.heappop(heap)
            if len(res) > 1 and char1 == res[-1] == res[-2]:
                if heap:
                    cnt2, char2 = heapq.heappop(heap)
                    res.append(char2)
                    cnt2 += 1
                    heapq.heappush(heap, (cnt1, char1))
                    if cnt2 < 0:
                        heapq.heappush(heap, (cnt2, char2))
            else:
                res.append(char1)
                cnt1 += 1
                if cnt1 < 0:
                    heapq.heappush(heap, (cnt1, char1))
        return "".join(res)

class TestSolution(unittest.TestCase):
    def test1(self):
        self.assertEqual(Solution().longestString(1, 1, 6), "ccaccbcc",
            "Should be \"ccaccbcc\"")

    def test2(self):
        self.assertEqual(Solution().longestString(1, 2, 3), "cbcabc",
            "Should be \"cbcabc\"")

if __name__ == "__main__":
    unittest.main()

Amazon OA. Longest String Without 3 Consecutive Characters.py