import json
import traceback

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import codecs
import time
import xlsxwriter
from os.path import exists
import os

from bs4 import BeautifulSoup

from geopy import distance

def get_distance(first_point, second_point):
    return distance.distance(first_point, second_point).meters

def passed_threshold(first_point, second_point):
    return get_distance(first_point, second_point) < 100

start_url = 'https://yandex.com/maps/1/moscow-and-moscow-oblast/category/pawnshop'#'https://yandex.kz/maps/213/moscow/search/%D0%9B%D0%BE%D0%BC%D0%B1%D0%B0%D1%80%D0%B4/' #'https://yandex.ru/maps/213/moscow/category/pawnshop'
max_resless_in_a_row = 5

def get_options():
    chrome_options = Options()
    chrome_options.add_argument("--headless")

    return chrome_options

def start_driver(url):
    driver = webdriver.Chrome(ChromeDriverManager().install(), options=get_options())
    driver = set_driver(driver)
    driver.get(url)

    return driver

def set_driver(driver):
    driver.implicitly_wait(3)
    return driver

def shallow_parse(url, driver=None):
    if not driver:
        driver = start_driver(url)

    elements_html = scroll_all_elements(driver)

    elements = shallow_parse_elements(elements_html)

    return remove_duplicates(elements)

def remove_duplicates(arr):
    res = []
    [res.append(x) for x in arr if x not in res]
    return res

def find_element_by_class(driver, element_type, class_name):
    return WebDriverWait(driver, 10).until(
        #EC.presence_of_element_located((By.XPATH, f"//{element_type}[contains(@class, '{class_name}')]"))
        EC.presence_of_element_located((By.XPATH, find_by_class_short(element_type, class_name)))
    )

def parse_pages(elements, id = 0, resless_count=0):
    j = []

    not_parsed = []

    driver = start_driver(start_url)
    i = 0
    for element in elements:
        print(f"({i}/{len(elements)}) {element['link']} (errors: {len(not_parsed)})")
        driver = set_driver(driver)
        driver.get(element['link'])

        try:
            res = deep_parse(driver)

            res['name'] = element['name']
            res['link'] = element['link']
            res['rating'] = element['rating']

            j.append(res)

            i += 1
        except Exception:
            traceback.print_exc()
            not_parsed.append(element)

    print('saving data..')
    if j:
        save(os.path.join('temp_res',f'res{id}.json'), json.dumps(j, ensure_ascii=False))
        resless_count = 0
    else:
        resless_count += 1

    if not_parsed and resless_count <= max_resless_in_a_row:
        j += parse_pages(not_parsed, id+1, resless_count)

    return j

def save(file_name, data):
    print('writing to a file')
    mode = 'w'
    if not exists(file_name):
        mode = 'x'
    with codecs.open(file_name, mode, 'utf-8') as f:
        f.write(data)

def scroll_all_elements(driver):

    def get_element_to_scroll_to(driver, elements):
        try:
            # last_element = driver.find_element(By.XPATH, "//ul[contains(@class, 'search-list-view__list')]/div[last()]")
            last_element = find_element_by_class(driver, 'ul', 'search-list-view__list').find_element(By.XPATH, 'div[last()]')
        except:
            try:
                last_element = elements[-1]
            except:
                print(driver.find_element(By.XPATH, "//ul[contains(@class, 'search-list-view__list')]").
                      get_attribute("outerHTML"))
                return None
        return last_element

    def try_scroll(element):
        try:
            driver.execute_script("arguments[0].scrollIntoView(true);", element)
        except:
            pass

    arr_max_size = 10
    iterations_res = [i for i in range(arr_max_size)]

    i = 0

    elements_count = -1

    while iterations_res[0] != elements_count:
        elements = driver.find_elements(By.XPATH, "//ul[contains(@class, 'search-list-view__list')]/li")

        elements_count = len(elements)

        iterations_res.append(elements_count)
        iterations_res.pop(0)

        try_scroll(get_element_to_scroll_to(driver, elements))

        time.sleep(.5)

    print(f"Places found {elements_count}")
    return elements

def shallow_parse_elements(elements_html):

    def parse_rating(element_html):
        try:
            rating = element_html.find_element(By.XPATH, "div/div/div/div/div[5]/div[1]/div/span/span/span[2]").text
        except:
            rating = '-'
        return rating

    elements = []
    for element_html in elements_html:
        try:
            link_and_name = element_html.find_element(By.CSS_SELECTOR, 'div.search-snippet-view__body._type_business > div > a')
        except:
            print('Skipping element')
            continue
        link = link_and_name.get_attribute("href")
        name = link_and_name.text
        rating = parse_rating(element_html)
            #address = element_html.find_element(By.CSS_SELECTOR, "div.search-business-snippet-view__address").text

        element = {
            "link": link,
            "name": name,
            "rating": rating,
        }
        elements.append(element)
        print(element)

    return elements

def deep_parse(element_page):
    #address_element = element_page.find_element(By.XPATH, "//a[contains(@class, 'business-contacts-view__address-link')]")

    #schedule = parse_schedule(element_page)

    phones = parse_phones(element_page)

    websites = parse_websites(element_page)

    #photos = parse_photos(element_page)

    address_element = find_element_by_class(element_page, 'a', 'business-contacts-view__address-link')
    address = address_element.text
    coordinates, pivot_name, pivot_distance = get_coordinates(address_element.get_attribute("href"), element_page)

    return {
        "address": address,
        "coordinates": coordinates.split(', '),
        "pivot_point_name": pivot_name,
        "pivot_point_distance": pivot_distance,
        "phones": phones,
        "websites": websites,
        #"photos": photos,
        #"schedule": schedule
    }

def parse_schedule(element_page):
    # element_page.find_element(By.XPATH, "//div[contains(@class, 'business-card-working-status-view__text')]").click()
    find_element_by_class(element_page, 'div', 'business-card-working-status-view__text').click()

    schedule_html = element_page.find_elements(By.XPATH, "//div[contains(@class, 'business-working-intervals-view__interval')]")
    schedule = list(map(lambda x: x.text, schedule_html))
    return schedule

def parse_phones(element_page):
    phone = try_find(element_page, find_by_class_short('div', 'card-phones-view__number'))
    if not phone:
        print('no phones')
        return []
    else:
        phones = [phone.text.split('\n')[0]]
    try:
        '/div/div/div[1]/div[1]/div[2]'
        find_element_by_class(element_page, 'div', 'card-phones-view__more').click()

        parent = find_element_by_class(element_page, 'div', 'card-phones-view')
        #find_element_by_class(parent, 'div', 'card-feature-view__additional').click()
        arrow = try_find(parent, 'div/div/div[1]/div[1]/div[2]')
        if not arrow:
            print(phones)
            return phones
        else:
            arrow.click()
            phones_html = element_page.find_elements(By.XPATH, "//div[contains(@class, 'card-phones-view__phone-number')]")
            #phones_html = find_element_by_class(element_page, 'div', 'card-phones-view__phone-number')
            phones = []
            for phone_html in phones_html:
                if phone_html.text != '':
                    phones.append(phone_html.text)
    except Exception:
        traceback.print_exc()
    print(phones)
    return phones

def parse_websites(element_page):
    website = try_find(element_page, find_by_class_short('span', 'business-urls-view__text'))
    if not website:
        website = find_bs4(element_page.page_source, 'span', 'business-urls-view__text')

    if not website:
        save('debug.html', element_page.page_source)
        print('no websites')
        return []

    websites_container = try_find(element_page, find_by_class_short('div', 'business-urls-view _wide'))
    print(website.text)

    # BUG it's changing it's url
    #more_websites_button = try_find(websites_container, find_by_class_short('div', 'card-feature-view__additional'))
    #if not more_websites_button:
    #    print('only one website')
    #    return [website.text]
    #else:
    #    more_websites_button.click()
    #    all_websites_html = find_all_bs4(element_page.page_source, 'span', 'business-urls-view__text')#try_find_all(websites_container, find_by_class_short('span', 'card-feature-view__additional'), crucial=True)
    #    print(f'{len(all_websites_html)} websites')
    #    return [x.text for x in all_websites_html]

    return [website.text]

def try_find(driver, xpath, crucial=False):
    try:
        return driver.find_element(By.XPATH, xpath)
    except:
        cant_find_xpath_log(xpath, crucial)

def try_find_all(driver, xpath, crucial=False):
    try:
        return driver.find_elements(By.XPATH, xpath)
    except:
        cant_find_xpath_log(xpath, crucial)

def find_bs4(source_code, elem_type, class_name):
    soup = BeautifulSoup(source_code, 'html.parser')
    return soup.find(elem_type, class_=class_name)

def find_all_bs4(source_code, elem_type, class_name):
    soup = BeautifulSoup(source_code, 'html.parser')
    return soup.find_all(elem_type, class_=class_name)

def find_by_class_short(elem_type, class_name):
    return f"//{elem_type}[contains(@class, '{class_name}')]"

def cant_find_xpath_log(xpath, crucial):
    if not crucial:
        print(f'ERROR cant find element(s) by this xpath:\n\t{xpath}')
    else:
        raise Exception(f'ERROR cant find element(s) by this xpath:\n\t{xpath}')

def parse_photos(element_page):
    find_element_by_class(element_page, 'div', 'tabs-select-view__title _name_gallery').click()

    photos = []

    photos_html = element_page.find_elements(By.XPATH, "//img[contains(@class, 'photo-wrapper__photo')]")
    for photo_html in photos_html:
        photos.append(photo_html.get_attribute('src'))
    return photos

def parse_reviews(element_page):
    find_element_by_class(element_page, 'div', 'tabs-select-view__title _name_reviews _selected').click()

    return []

def get_coordinates(address_link, driver):
    try:
        print(address_link)
        driver.get(address_link)
        driver = set_driver(driver)

        coordinates = find_element_by_class(driver, 'div', 'toponym-card-title-view__coords-badge').text
    except:
        print('wtf')
        save('debug.html', driver.page_source)
        soup = BeautifulSoup(driver.page_source, "html.parser")
        coordinates = soup.find("div", {"class": "toponym-card-title-view__coords-badge"}).text
        print(coordinates)

    try:
        pivot_point = find_element_by_class(driver, 'div', 'masstransit-stops-view__stop _type_metro')
        pivot_point_name = pivot_point.find_element(By.XPATH, "div").text
        pivot_point_distance = pivot_point.find_element(By.XPATH, "div[2]/div[2]").text
    except:
        print('no metro')
        pivot_point_name = '-'
        pivot_point_distance = '-'
    return coordinates, pivot_point_name, pivot_point_distance

def file_to_object(file_name):
    with codecs.open(file_name, 'r', 'utf-8-sig') as f:
        return json.loads(f.read())

def object_to_file(file_name, data):
    save(file_name, json.dumps(data, ensure_ascii=False))

def merge_files(first_file, second_file, final_file_name):
    first_file_data = file_to_object(first_file)
    print(f'first file len: {len(first_file_data)}')
    second_file_data = file_to_object(second_file)
    print(f'Second file len: {len(second_file_data)}')
    final_data = merge_arrays([first_file_data, second_file_data])
    print(f'Final file len: {len(final_data)}')
    object_to_file(final_file_name, final_data)

def merge_arrays(arrays):
    res = []
    for arr in arrays:
        [res.append(x) for x in arr if x not in res]
    return res

def to_xlsx(arr, name):
    workbook = xlsxwriter.Workbook(f'{name}.xlsx')
    worksheet = workbook.add_worksheet()

    bold = workbook.add_format({'bold': True})

    worksheet.write(0, 0, 'Имя', bold)
    worksheet.write(0, 1, 'Адрес', bold)
    worksheet.write(0, 2, 'Рейтинг', bold)
    worksheet.write(0, 3, 'Контакты', bold)
    worksheet.write(0, 4, 'Вебсайты', bold)
    worksheet.write(0, 5, 'Координаты', bold)
    worksheet.write(0, 6, 'Ближайшее метро', bold)
    worksheet.write(0, 7, 'Есть в официальном файле')

    for i in range(len(arr)):
        worksheet.write(i + 1, 0, arr[i]['name'])
        worksheet.write(i + 1, 1, arr[i]['address'])
        worksheet.write(i + 1, 2, arr[i]['rating'])
        worksheet.write(i + 1, 3, ', '.join(arr[i]['phones']))
        worksheet.write(i + 1, 4, ', '.join(arr[i]['websites']))
        worksheet.write(i + 1, 5, ', '.join(arr[i]['coordinates']))
        worksheet.write(i + 1, 6, f"{arr[i]['pivot_point_name']}, {arr[i]['pivot_point_distance']}")
        try:
            if arr[i]['official']:
                worksheet.write(i + 1, 7, "Да")
            else:
                worksheet.write(i + 1, 7, "Нет")
        except:
            worksheet.write(i + 1, 7, "?")

    workbook.close()

def parse(search_link, file_name):

    try:
        shallow = shallow_parse(search_link)
    except:
        try:
            print(f'ERROR OCCURRED (shallow parse): {search_link}\n\ttrying to resolve')
            traceback.print_exc()
            driver = start_driver(search_link)

            print(driver.current_url)

            elem = find_element_by_class(driver, 'div', 'small-search-form-view__icon _type_back')
            elem.click()

            print(driver.current_url)

            shallow = shallow_parse(driver.current_url, driver)
            print(shallow)
            print('SUCCESSFUL')
        except:
            mode = 'a'
            if not exists('no_elements_links.txt'):
                mode = 'x'
            with codecs.open('no_elements_links.txt', mode, 'utf-8') as f:
                f.write(f'{search_link}\n')
            print("No elements")
            shallow = []
    object_to_file(os.path.join('shallow_res', f'{file_name}_shallow.json'), shallow)

    res = parse_pages(shallow)
    object_to_file(os.path.join('res', f'{file_name}.json'), res)

    return res

def parse_urls(urls, final_file_name, add_to_url):
    results = []
    i = 1
    for url in urls:
        print(f'{i}/{len(urls)}: {url}')
        url = url.replace('\n', '')
        name = url.split('/')[-1]
        results = merge_arrays([results, parse(f'{url}{add_to_url}', name)])
        i += 1
    object_to_file(final_file_name, results)
    return results

if not os.path.isdir('temp_res'):
    os.makedirs('temp_res')
if not os.path.isdir('res'):
    os.makedirs('res')
if not os.path.isdir('shallow_res'):
    os.makedirs('shallow_res')

if exists('no_elements_links.txt'):
    with open('no_elements_links.txt', 'w'):
        pass

with open('urls.txt', 'r') as f:
    res = parse_urls(f.readlines(), 'final.json', '/search/скупка%20золото')
    to_xlsx(res, 'final')


import numpy as np

import torch
import torch.nn.functional as F
from torchvision import transforms

from typing import Iterable, Union
from pathlib import Path

class FaceId(torch.nn.Module):
    def __init__(
        self, model_path: Path, device: str, input_shape: Iterable[int] = (112, 112)
    ):
        super().__init__()

        self.input_shape = input_shape
        self.net = torch.load(model_path, map_location=torch.device("cpu"))
        self.net.eval()

        self.transform = transforms.Compose(
            [
                transforms.ToTensor(),
                transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
            ]
        )

        for n, p in self.net.named_parameters():
            assert (
                not p.requires_grad
            ), f"Parameter {n}: requires_grad: {p.requires_grad}"

        self.device = torch.device(device)
        self.to(self.device)

    def forward(
        self, img_id: Union[np.ndarray, Iterable[np.ndarray]], normalize: bool = True
    ) -> torch.Tensor:
        if isinstance(img_id, Iterable):
            img_id = [self.transform(x) for x in img_id]
            img_id = torch.stack(img_id, dim=0)
        else:
            img_id = self.transform(img_id)
            img_id = img_id.unsqueeze(0)

        img_id = img_id.to(self.device)

        img_id_112 = F.interpolate(img_id, size=self.input_shape)
        latent_id = self.net(img_id_112)
        return F.normalize(latent_id, p=2, dim=1) if normalize else latent_id


import pandas as pd
import numpy as np
import openpyxl
from datetime import date, timedelta
import re
import scipy.stats

parse_dates = ['DATE_ID']

df01 = pd.read_csv('temp/siq_v202003051200_msk.csv', delimiter=';', decimal=',', parse_dates=parse_dates)
df02 = pd.read_csv('temp/siq_v202003051200_msk.csv', delimiter=';', decimal=',', parse_dates=parse_dates)

df1 = pd.concat([df01, df02], axis=1).reindex(df01.index)
print(df01.head(5))
print(df02.head(5))
print(df1.head(5))

# full screen option (printing)
pd.set_option('display.max_columns', None)

# get yesterday
yesterday = date.today() - timedelta(days=1)
yesterday = yesterday.strftime('%Y-%m-%d')

# if case then

df1['Period'] = 'BeforeYDA'
df1.loc[df1['DATE_ID'] == yesterday, 'Period'] = 'Yesterday'

print(df1.head(3))

#
# # Take only needed columns
# df2 = df1.loc[:, ('LAC', 'Element', 'BeelineObject', 'CELL_ID', 'DATE', 'CDR', 'CunSSR')]
#
#
#

df1['DATE_ID'] = df1['DATE_ID'].dt.date
# in pivot tables - columns change value from 'DatePeriod'<->'Period'
# NOTE use DatePeriod for test !!!
#     NOTE use Period for real data !!!
#

df42 = pd.pivot_table(df1
                        , index=['Element', 'BeelineObject']
                        , columns=['DatePeriod', 'DATE_ID']
                        , values=["CDR_Drops"]
                        )#.reset_index()
df42["CDR_Drops"] = df42["CDR_Drops"].fillna(0.0).astype(int)

# df42 = df42.div(df42.sum(axis=1), axis=0)
# print('_'*20, 'Normalize', '_'*20)
# print(df42.head(5))

# axisvalues = list(range(1, len(df42.columns)+1))
#
# def calc_slope(row):
#     a = scipy.stats.linregress(row, y=axisvalues)
#     return a.slope
#
# df42['slope'] = df42.apply(calc_slope, axis=1)

# check if row is monotonic decrease
# df42['df_status'] = df42.T.apply(lambda x: x.is_monotonic)
# #df42 = df42.rename({'df_status': 'decrease'}, axis='columns')
#
# df42 = df42.rename(columns = {'NaT': 'Name1', 'NaT': 'Name2'})

print(df42.head(5))

df41 = pd.pivot_table(df1
                        , index=['Element', 'BeelineObject']
                        , columns=['DatePeriod']
                        , values=["CDR_Drops"]
                        , aggfunc={'CDR_Drops': 'mean'}
                        )#.reset_index()

# float to int:

df41["CDR_Drops"] = df41["CDR_Drops"].fillna(0.0).astype(int)

df41.columns = ['SumKPI_BeforeYDA', 'SumKPI_YDA']
df41['Diff_Drops'] = df41['SumKPI_YDA']*100/df41['SumKPI_BeforeYDA']
print(df41.head(5))

print('_'*30, 'Total', '_'*30)
pd.set_option('display.max_columns', None)
df43 = pd.concat([df42, df41], axis=1).reindex(df42.index)

headers = list(df43.columns)

# new headers
new_column_list = []

for id, item in enumerate(headers):
    if type(item) is str:
        new_column_list.insert(id, item)
    else:
        new_column_list.insert(id, str(item[2]))

# set new header
df43.columns = new_column_list

df43 = df43.rename(columns={'NaT': 'Name1', 'NaT': 'Name2'})

# for better view in Excel reset_index
df43 = df43.reset_index(drop=False)
print(df43.head(5))

df43.to_excel("temp/siq_202003051200_msk.xlsx")


import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import matplotlib.animation as animation
from Physics2D import *
from Physics3D import *

def make_segments(x, y):
    points = np.array([x, y]).T.reshape(-1, 1, 2)
    segments = np.concatenate([points[:-1], points[1:]], axis=1)
    return segments

def gradient_plot(x, y, colors, cmap=plt.get_cmap('gist_rainbow'), line_width=3, alpha=1.0):
    lines = make_segments(x, y)
    col = LineCollection(lines, array=colors, cmap=cmap, linewidth=line_width, alpha=alpha)
    return col

def update2d(i, time, x_first_ball, y_first_ball, x_second_ball, y_second_ball,
             trail, time_text, line1, line2, circle1, circle2):
    h = 0.01
    p = int(time / h) // 50
    if not i-p < 0:
        trail.set_segments(make_segments(x_second_ball[i - p:i], y_second_ball[i - p:i]))
    time_text.set_text(f'Time elapsed: {np.round(h * (i+1), 1)} seconds')
    line1.set_ydata([0, y_first_ball[i]])
    line1.set_xdata([0, x_first_ball[i]])
    line2.set_ydata([y_first_ball[i], y_second_ball[i]])
    line2.set_xdata([x_first_ball[i], x_second_ball[i]])

    circle1.center = x_first_ball[i], y_first_ball[i]
    circle2.center = x_second_ball[i], y_second_ball[i]
    return trail, time_text, line1, line2, circle1, circle2

def animate2d(initial_conditions, mass1, mass2, length1, length2, gravity, damping, time):

    h = 0.01
    steps = int(np.rint(time / h))
    time_grid = np.linspace(0, time, steps + 1)

    ans = odeint(system_dt_2d, y0=initial_conditions, t=time_grid,
                 args=(gravity, mass1, mass2, length1, length2, damping), atol=1.0e-9, rtol=1.0e-9)

    x_first_ball, y_first_ball, x_second_ball, y_second_ball = get_pos(ans[:, 0], ans[:, 1], length1, length2)

    fig = plt.figure(figsize=(10, 10))
    ax = fig.add_subplot(1, 1, 1)
    plt.subplots_adjust(bottom=0.2, left=0.15)

    half = length1 + length2
    ax.axis([-half-1, half+1, -half-1, half+1])
    ax.set_title('Damped Double Pendulum')
    ax.set_xlabel('x(t)')
    ax.set_ylabel('y(t)')
    trail = gradient_plot(x_second_ball[:0], y_second_ball[:0], time_grid[:steps + 1],
                          plt.get_cmap('gist_rainbow'), line_width=1, alpha=0.5)
    ax.add_collection(trail)

    time_text = ax.text(-half, half, '', fontsize=12)
    time_text.set_text('Time elapsed: 0 seconds')

    line1 = ax.plot([0, x_first_ball[0]], [0, y_first_ball[0]], color='k', lw=3, zorder=20)[0]
    line2 = ax.plot([x_first_ball[0], x_second_ball[0]], [y_first_ball[0], y_second_ball[0]],
                    color='k', lw=3, zorder=20)[0]

    circle1 = plt.Circle((x_first_ball[0], y_first_ball[0]), 0.1, ec="k", lw=1.5, zorder=30)
    ax.add_patch(circle1)
    circle2 = plt.Circle((x_second_ball[0], y_second_ball[0]), 0.1, ec="k", lw=1.5, zorder=30)
    ax.add_patch(circle2)

    ani = animation.FuncAnimation(fig, update2d, frames=np.arange(steps),
                                  fargs=(time,
                                         x_first_ball, y_first_ball,
                                         x_second_ball, y_second_ball,
                                         trail, time_text, line1, line2, circle1, circle2),
                                  interval=h, blit=True, repeat=True, repeat_delay=5000)
    plt.show()

    return ani

def update3d(i, x_first_ball, y_first_ball, z_first_ball, x_second_ball, y_second_ball, z_second_ball,
             line1, line2, line_trace):
    line1.set_data([0, x_first_ball[i]], [0, y_first_ball[i]])
    line1.set_3d_properties([0, z_first_ball[i]])
    line2.set_data([x_first_ball[i], x_second_ball[i]], [y_first_ball[i], y_second_ball[i]])
    line2.set_3d_properties([z_first_ball[i], z_second_ball[i]])

    line_trace.set_data(x_second_ball[:i+1], y_second_ball[:i+1])
    line_trace.set_3d_properties(z_second_ball[:i+1])
    return line1, line2, line_trace

def animate3d(initial_conditions, mass1, mass2, length1, length2, gravity, damping, time):

    h = 0.01
    steps = int(np.rint(time / h))
    time_grid = np.linspace(0, time, steps + 1)

    ans = odeint(system_dt_3d, y0=initial_conditions, t=time_grid,
                 args=(gravity, mass1, mass2, length1, length2, damping), atol=1.0e-10, rtol=1.0e-10)

    (x_first_ball, y_first_ball, z_first_ball,
     x_second_ball, y_second_ball, z_second_ball) = get_pos_3d(ans[:, 0], ans[:, 1], ans[:, 2], ans[:, 3],
                                                               length1, length2)

    fig = plt.figure(figsize=(10, 10))
    ax = fig.add_subplot(1, 1, 1, projection='3d')
    half = 4.5
    if half < length1 + length2:
        half = length1 + length2
    ax.axis([-half-1, half+1, -half-1, half+1, -half-1, half+1])
    ax.set_title('Damped Double Pendulum')
    ax.set_xlabel('x(t)')
    ax.set_ylabel('y(t)')
    ax.set_zlabel('z(t)')

    line1 = ax.plot3D([0, x_first_ball[0]], [0, y_first_ball[0]], [0, z_first_ball[0]],
                      'o-', color='blue', lw=4)[0]
    line2 = ax.plot3D([x_first_ball[0], x_second_ball[0]], [y_first_ball[0], y_second_ball[0]],
                      [z_first_ball[0], z_second_ball[0]], 'o-', color='blue', lw=4)[0]
    line_trace = ax.plot3D([], [], [], color='k', lw=1)[0]

    ani = animation.FuncAnimation(fig, update3d, frames=np.arange(steps),
                                  fargs=(x_first_ball, y_first_ball, z_first_ball,
                                         x_second_ball, y_second_ball, z_second_ball,
                                         line1, line2, line_trace),
                                  interval=h, blit=True, repeat=True, repeat_delay=1000)

    plt.show()

    return ani


import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
import requests
dynamodb = boto3.resource('dynamodb')

ELASTIC_SEARCH_URL = "<ES_URL>"
SQS_URL = "<SQS_URL>"
SNS_TOPIC_ARN = "<SNS_ARN>"

def cacheRestaurantRecommendations(messageToCache):

    table_suggestion = dynamodb.Table('prevSuggestions')
    userName = "<username>";

    response = table_suggestion.scan(FilterExpression=Attr('userID').eq(userName))
    cache_item = response['Items'][0] if len(response['Items']) > 0 else None
    if response is None or cache_item is None:

        table_suggestion.put_item(
        Item = {
            'userID': userName,
            'prevSuggestions': messageToCache
        })
    else:
        table_suggestion.update_item(
            Key={'userID': userName},
        UpdateExpression="set prevSuggestions= :messageToCache",
        ExpressionAttributeValues={
            ':messageToCache': messageToCache
        },
        ReturnValues="UPDATED_NEW")

def lambda_handler(event, context):

    try:
        sqs_client = boto3.client("sqs", region_name="us-east-1")
    except Exception as e:
        print(e)
    queue_url = SQS_URL;

    response = sqs_client.receive_message(
        QueueUrl=queue_url,
        AttributeNames=[
            'SentTimestamp'
        ],
        MaxNumberOfMessages=1,
        MessageAttributeNames=[
            'All'
        ],
        VisibilityTimeout=0,
        WaitTimeSeconds=0
    )

    message_data = response['Messages'][0]
    message_atributes = message_data['MessageAttributes']

    cuisine = message_atributes['cuisine']
    cuisine = cuisine['StringValue']

    city = message_atributes['location']
    location = city['StringValue']

    people_num = message_atributes['numberpeople']
    number_of_people = people_num['StringValue']

    phone = message_atributes['PhoneNumber']
    phone_number = phone['StringValue']

    time_data = message_atributes['time']
    time = time_data['StringValue']

    date = message_atributes['date']
    date = date['StringValue']

    if not cuisine or not phone_number:
        print("No Cuisine or Phone found in message")
        return
    print("Cuisine: {}, Phone number: {}, Time: {}, Date: {}, Number of people: {}".format(cuisine, phone_number, time, date, number_of_people))

    esUrl = ELASTIC_SEARCH_URL + "_search?q={cuisine}".format(cuisine=cuisine)

    esResponse = requests.get(esUrl, auth=("<USERNAME>", "<PASSWORD>"))

    data = json.loads(esResponse.content.decode('utf-8'))
    print(data);

    try:
        esData = data["hits"]["hits"]
    except KeyError:
        print("Error extracting hits from ES response")

    restaurant_ids = []
    for restaurant in esData:
        restaurant_ids.append(restaurant["_source"]["ID"])

    messageToSend = 'Hello! Here are the {cuisine} restaurant suggestions in {location} for {numPeople} people, for {diningDate} at {diningTime}: '.format(
            cuisine=cuisine,
            location=location,
            numPeople=number_of_people,
            diningTime=time,
            diningDate=date,
        )

    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('yelp-restaurants')
    idx = 1

    restaurant_ids = restaurant_ids[1:3]

    for id in restaurant_ids:
        response = table.get_item(Key = {"ID": id})

        item = response['Item']

        name = item['Name']
        address = item['Address']
        if response is None:
            continue
        restaurantMsg = '' + str(idx) + '. '
        name = item["Name"]
        address = item["Address"]
        restaurantMsg += name +', located at ' + str(address)[1:-1] +'. '
        messageToSend += restaurantMsg
        idx += 1

    messageToSend += "Enjoy your meal!!!"
    print("messageToSend: {}".format(messageToSend))
    sns = boto3.client('sns', region_name='us-east-1')
    try:
        sns.publish(TopicArn=SNS_TOPIC_ARN, Message=json.dumps(str(messageToSend)))
    except KeyError:
        print("Error sending ")
        sns.publish(TopicArn=SNS_TOPIC_ARN, Message=json.dumps("No recommendations found."))

    cacheRestaurantRecommendations(messageToSend);

    return {
        'statusCode': 200,
        'body': json.dumps(messageToSend)
    }


import torch
import torch.nn as nn
import glob
from main import *
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score

def scatter_plot_real(df, xmin, xmax):
    # Bar to kpa
    df['output_simulated'] = df['output_simulated'] * 100
    df['output_predicted'] = df['output_predicted'] * 100
    df['flacs'] = df['flacs'] * 100

    # get data in range
    df_subset = df.loc[df['output_simulated'] < xmax]
    df_subset = df_subset.loc[df_subset['output_simulated'] > xmin]
    print("R2_subset: {}".format(r2_score(df_subset['output_simulated'], df_subset['output_predicted'])))
    print("MAPE_subset: {}".format(df_subset['relative_error'].mean(axis=0)))

    sns.scatterplot(data=df_subset,
                    x="output_simulated",
                    y="output_predicted",
                    hue='Status',
                    style='Fluid',
                    palette='pastel',
                    s=100)

    # Add error line
    x_min = df_subset['output_simulated'].min()
    x_max = df_subset['output_simulated'].max()
    xx = np.arange(xmin, xmax, 0.001)
    y_lower = xx * (1 - 0.3)
    y_upper = xx * (1 + 0.3)
    plt.plot(xx, xx, 'r')
    plt.plot(xx, y_lower, 'r', linestyle='--')
    plt.plot(xx, y_upper, 'r', linestyle='--')

    # Add label and axe.limit
    plt.xlabel('Experiment (kPa)')
    plt.ylabel('ANN (kPa)')
    axes = plt.gca()
    axes.set_xlim([xmin, xmax])
    axes.set_ylim([xmin, xmax])
    plt.grid()

    plt.show()

def scatter_plot(df, xmin, xmax):
    # Bar to kpa
    df['output_simulated'] = df['output_simulated'] * 100
    df['output_predicted'] = df['output_predicted'] * 100

    # get data in range
    df_subset = df.loc[df['output_simulated'] < xmax]
    df_subset = df_subset.loc[df_subset['output_simulated'] > xmin]
    print("R2_subset: {}".format(r2_score(df_subset['output_simulated'], df_subset['output_predicted'])))
    print("MAPE_subset: {}".format(df_subset['relative_error'].mean(axis=0)))

    sns.scatterplot(data=df_subset,
                    x="output_simulated",
                    y="output_predicted",
                    hue='Status',
                    palette='pastel',
                    s=100)

    # Add error line
    x_min = df_subset['output_simulated'].min()
    x_max = df_subset['output_simulated'].max()
    xx = np.arange(xmin, xmax, 0.001)
    y_lower = xx * (1 - 0.3)
    y_upper = xx * (1 + 0.3)
    plt.plot(xx, xx, 'r')
    plt.plot(xx, y_lower, 'r', linestyle='--')
    plt.plot(xx, y_upper, 'r', linestyle='--')

    # Add label and axe.limit
    plt.xlabel('Experiment (kPa)')
    plt.ylabel('ANN (kPa)')
    axes = plt.gca()
    axes.set_xlim([xmin, xmax])
    axes.set_ylim([xmin, xmax])
    plt.grid()

    plt.show()

def error_analysis(df, feature, target, error):
    """
    a function to analyze the output error based on feature value.
    :param df: pandas data frame, contain feature, ground-truth, and prediction
    :param feature: string, the name of the column we are analyzing in df
    :param target: list, the value interval of the feature
    :param error: list, the value interval of error
    :return: percentage % of data points within each interval of the target feature that falls into each error interval
    """
    sumn = 0
    for i in range(len(target) - 1):
        df_subset = df.loc[df[feature] > target[i]]
        df_subset = df_subset.loc[df_subset[feature] <= target[i + 1]]
        n, _ = df_subset.shape
        sumn += n
        print("==={} < target <= {}, data={}===".format(target[i], target[i + 1], n))
        sump = 0
        for j in range(len(error) - 1):
            df_subset1 = df_subset.loc[df_subset['relative_error'] > error[j]]
            df_subset1 = df_subset1.loc[df_subset1['relative_error'] <= error[j + 1]]
            n_1, _ = df_subset1.shape
            percentage = n_1 / n
            sump += percentage
            print("{} < error <= {},  percentage = {}".format(error[j], error[j + 1], percentage))
        print(" error <= {},  percentage = {}".format(error[-1], sump))
    print("Total data points: {}. Analyzed: {}.".format(df.shape[0], sumn))

# Name of features
columns = ['Tank failure Pressure (bar)',
           'Liquid ratio (%)', 'Tank width (m)',
           'Tank length (m)',
           'Tank height (m)',
           'Height of BLEVE (m)',
           'Tank height with gas (m)',
           ' Vapour temerature (K)',
           ' liquid temerature (K)',
           'Obstacle distance to BLEVE',
           'Width of Obstacle ',
           'Height of Obstacle ',
           'Thickness of Obstacle ',
           'Angle of Obstacle ',
           'Status',
           'obstacle ID',
           'Position ID']

# Check the performance
data = np.load('data/BLEVE_Obstacle_Butane_Propane.npz')
mean = data['mean']
std = data['std']

model = MLPNet(features=[mean.shape[0], 512, 512, 512, 512, 512], activation_fn='mish')
models_name = glob.glob('models/BLEVE_open_L5_N512.pt')
models_name.sort()
model.load_state_dict(torch.load(models_name[-1]), strict=False)
model.eval()

# On training set
train_X = torch.tensor((data['train_X'] - mean) / std, dtype=torch.float32)
train_y = torch.tensor(data['train_y'], dtype=torch.float32)
pred_train = model(train_X)
pred_train = pred_train.squeeze()
print("MAPE_train: {}".format(mean_absolute_percentage_error(train_y, pred_train)))
print("R2_train: {}".format(r2_score(data['train_y'], pred_train.detach().numpy())))

df_train = pd.DataFrame(data['train_X'], columns=columns)
df_train = df_train.assign(output_simulated=data['train_y'])
df_train = df_train.assign(output_predicted=pred_train.detach().numpy())
df_train = df_train.assign(relative_error=np.abs(pred_train.detach().numpy()
                                               - data['train_y'])/data['train_y'] * 100)

# On val set
val_X = torch.tensor((data['val_X'] - mean) / std, dtype=torch.float32)
val_y = torch.tensor(data['val_y'], dtype=torch.float32)
pred_val = model(val_X)
pred_val = pred_val.squeeze()
print("MAPE_val: {}".format(mean_absolute_percentage_error(val_y, pred_val)))
print("R2_val: {}".format(r2_score(data['val_y'], pred_val.detach().numpy())))

df_val = pd.DataFrame(data['val_X'], columns=columns)
df_val = df_val.assign(output_simulated=data['val_y'])
df_val = df_val.assign(output_predicted=pred_val.detach().numpy())
df_val = df_val.assign(relative_error=np.abs(pred_val.detach().numpy()
                                               - data['val_y'])/data['val_y'] * 100)

# On test set
test_X = torch.tensor((data['test_X'] - mean) / std, dtype=torch.float32)
test_y = torch.tensor(data['test_y'], dtype=torch.float32)
pred_test = model(test_X)
pred_test = pred_test.squeeze()
print("MAPE_test: {}".format(mean_absolute_percentage_error(test_y, pred_test)))
print("R2_test: {}".format(r2_score(data['test_y'], pred_test.detach().numpy())))

df_test = pd.DataFrame(data['test_X'], columns=columns)
df_test = df_test.assign(output_simulated=data['test_y'])
df_test = df_test.assign(output_predicted=pred_test.detach().numpy())
df_test = df_test.assign(relative_error=np.abs(pred_test.detach().numpy()
                                               - data['test_y'])/data['test_y'] * 100)
# df_test.to_excel("output.xlsx", sheet_name='simulated_data')

# scatter_plot(df_test, 0, 500)

#Target pressure error analysis
target = [x for x in range(28)]
error = [0, 5, 10, 20, 30]
feature = 'Position ID'
#
# # BLEVE distance error analysis
# target = [0, 10, 20, 30, 40, 50]
# error = [0, 5, 10, 20, 30]
# feature = 'Distance to BLEVE'
#
# # Liquid ratio error analysis
# target = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9]
# error = [0, 5, 10, 20, 30]
# feature = 'Liquid ratio'
# #
# #  BLEVE distance error analysis
# target = [0, 0.3, 0.4, 0.6, 1.0, 2.0]
# error = [0, 5, 10, 20, 30]
# feature = 'Gas height  (m)'
#
error_analysis(df_test, feature, target, error)


import json
import logging
from json.decoder import JSONDecodeError
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Union, overload
from urllib.parse import urlencode

import gevent
import requests
from typing_extensions import Literal

from rotkehlchen.assets.asset import Asset
from rotkehlchen.constants import ZERO
from rotkehlchen.errors import RemoteError, UnsupportedAsset
from rotkehlchen.fval import FVal
from rotkehlchen.logging import RotkehlchenLogsAdapter
from rotkehlchen.typing import Price, Timestamp
from rotkehlchen.utils.misc import get_or_make_price_history_dir, timestamp_to_date
from rotkehlchen.utils.serialization import rlk_jsondumps, jsonloads_dict

logger = logging.getLogger(__name__)
log = RotkehlchenLogsAdapter(logger)

PRICE_HISTORY_FILE_PREFIX = 'gecko_price_history_'
COINGECKO_QUERY_RETRY_TIMES = 4

class CoingeckoImageURLs(NamedTuple):
    thumb: str
    small: str
    large: str

class CoingeckoAssetData(NamedTuple):
    identifier: str
    symbol: str
    name: str
    description: str
    images: CoingeckoImageURLs

DELISTED_ASSETS = [
    '1SG',
    'FLUZ',
    'EBCH',
    'GOLOS',
    'NPER',
    'BLN',
    'ADN',
    'PIX',
    'MTC-2',
    'LKY',
    'ARB',
    'BBI',
    'BITCAR',
    'BTR',
    'OLE',
    'ROC',
    'VIN',
    'FIH',
    'WIN-2',
    'ADH',
    'AUR',
    'BAS',
    'BYC',
    'DGS',
    'GMT',
    'HST',
    'INS',
    'IPSX',
    'SHP',
    'WDC',
    'BOST',
    'FND',
    'LDC',
    'ORI',
    'RIPT',
    'SGR',
    'LOCUS',
    'REDC',
    'SGN',
    'SOAR',
    'YUP',
    'AC',
    'APIS',
    'BITPARK',
    'CO2',
    'DAN',
    'DEC',
    'DLT',
    'DROP',
    'ERD',
    'ETBS',
    'GEN',
    'STP',
    'SYNC',
    'TBT',
    'TNT',
    'WIC',
    'XCN',
    'XTP',
    'FREC',
    'PTC',
    'ACC-3',
    'J8T',
    'MRK',
    'TTV',
    'ALX',
    'EBC',
    'RCN-2',
    'SKYM',
    'BTT-2',
    'GBX-2',
    'MIC',
    'SND',
    '1ST',
    'aLEND',
    'aREP',
    'CRBT',
    'EXC-2',
    'DT',
    'CZR',
    'ROCK2',
    'ATMI',
    'BKC',
    'CREDO',
    'ETK',
    'FNKOS',
    'FTT',
    'GIM',
    'IVY',
    'KORE',
    'NBAI',
    'PAL',
    'XEL',
    'ATS',
    'BCY',
    'yDAI+yUSDC+yUSDT+yBUSD',
    'yyDAI+yUSDC+yUSDT+yBUSD',
    'ypaxCrv',
    'crvRenWBTC',
    'crvRenWSBTC',
    'ycrvRenWSBTC',
    'SPRK',
    'VV',
    'DRP',
    'HBZ',
    'TAAS',
    'TRUMPLOSE',
    'TRUMPWIN',
    'ATX-2',
    'SONIQ',
    'TRUST',
    'CDX',
    'CRB',
    'CTX',
    'EGC',
    'ICOS',
    'MTRC',
    'NOX',
    'PHI',
    'RLT',
    'SPIN',
    'VIEW',
    'VRM',
    'VRA',
    'SPF',
    'NOBS',
    'DADI',
]

COINGECKO_SIMPLE_VS_CURRENCIES = [
    "btc",
    "eth",
    "ltc",
    "bch",
    "bnb",
    "eos",
    "xrp",
    "xlm",
    "link",
    "dot",
    "yfi",
    "usd",
    "aed",
    "ars",
    "aud",
    "bdt",
    "bhd",
    "bmd",
    "brl",
    "cad",
    "chf",
    "clp",
    "cny",
    "czk",
    "dkk",
    "eur",
    "gbp",
    "hkd",
    "huf",
    "idr",
    "ils",
    "inr",
    "jpy",
    "krw",
    "kwd",
    "lkr",
    "mmk",
    "mxn",
    "myr",
    "nok",
    "nzd",
    "php",
    "pkr",
    "pln",
    "rub",
    "sar",
    "sek",
    "sgd",
    "thb",
    "try",
    "twd",
    "uah",
    "vef",
    "vnd",
    "zar",
    "xdr",
    "xag",
    "xau",
]

class Coingecko():

    def __init__(self, data_directory: Path) -> None:
        self.session = requests.session()
        self.session.headers.update({'User-Agent': 'rotkehlchen'})
        self.data_directory = data_directory

    @overload
    def _query(
            self,
            module: Literal['coins/list'],
            subpath: Optional[str] = None,
            options: Optional[Dict[str, Any]] = None,
    ) -> List[Dict[str, Any]]:
        ...

    @overload
    def _query(
            self,
            module: Literal['coins', 'simple/price'],
            subpath: Optional[str] = None,
            options: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        ...

    def _query(
            self,
            module: str,
            subpath: Optional[str] = None,
            options: Optional[Dict[str, Any]] = None,
    ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
        """Performs a coingecko query

        May raise:
        - RemoteError if there is a problem querying coingecko
        """
        if options is None:
            options = {}
        url = f'https://api.coingecko.com/api/v3/{module}/'
        if subpath:
            url += subpath

        logger.debug(f'Querying coingecko: {url}?{urlencode(options)}')
        tries = COINGECKO_QUERY_RETRY_TIMES
        while tries >= 0:
            try:
                response = self.session.get(f'{url}?{urlencode(options)}')
            except requests.exceptions.RequestException as e:
                raise RemoteError(f'Coingecko API request failed due to {str(e)}') from e

            if response.status_code == 429:
                # Coingecko allows only 100 calls per minute. If you get 429 it means you
                # exceeded this and are throttled until the next minute window
                # backoff and retry 4 times =  2.5 + 3.33 + 5 + 10 = at most 20.8 secs
                if tries >= 1:
                    backoff_seconds = 10 / tries
                    log.debug(
                        f'Got rate limited by coingecko. '
                        f'Backing off for {backoff_seconds}',
                    )
                    gevent.sleep(backoff_seconds)
                    tries -= 1
                    continue

                # else
                log.debug(
                    f'Got rate limited by coingecko and did not manage to get a '
                    f'request through even after {COINGECKO_QUERY_RETRY_TIMES} '
                    f'incremental backoff retries',
                )

            break

        if response.status_code != 200:
            msg = (
                f'Coingecko API request {response.url} failed with HTTP status '
                f'code: {response.status_code}'
            )
            raise RemoteError(msg)

        try:
            decoded_json = json.loads(response.text)
        except json.decoder.JSONDecodeError as e:
            msg = f'Invalid JSON in Coingecko response. {e}'
            raise RemoteError(msg) from e

        return decoded_json

    def asset_data(self, asset: Asset) -> CoingeckoAssetData:
        """

        May raise:
        - UnsupportedAsset() if the asset is not supported by coingecko
        - RemoteError if there is a problem querying coingecko
        """
        options = {
            # Include all localized languages in response (true/false) [default: true]
            'localization': False,
            # Include tickers data (true/false) [default: true]
            'tickers': False,
            # Include market_data (true/false) [default: true]
            'market_data': False,
            # Include communitydata (true/false) [default: true]
            'community_data': False,
            # Include developer data (true/false) [default: true]
            'developer_data': False,
            # Include sparkline 7 days data (eg. true, false) [default: false]
            'sparkline': False,
        }
        gecko_id = asset.to_coingecko()
        data = self._query(
            module='coins',
            subpath=f'{gecko_id}',
            options=options,
        )

        try:
            parsed_data = CoingeckoAssetData(
                identifier=gecko_id,
                symbol=data['symbol'],
                name=data['name'],
                description=data['description']['en'],
                images=CoingeckoImageURLs(
                    thumb=data['image']['thumb'],
                    small=data['image']['small'],
                    large=data['image']['large'],
                ),
            )
        except KeyError as e:
            raise RemoteError(
                f'Missing expected key entry {e} in coingecko coin data response',
            ) from e

        return parsed_data

    def all_coins(self) -> List[Dict[str, Any]]:
        return self._query(module='coins/list')

    @staticmethod
    def check_vs_currencies(from_asset: Asset, to_asset: Asset, location: str) -> Optional[str]:
        vs_currency = to_asset.identifier.lower()
        if vs_currency not in COINGECKO_SIMPLE_VS_CURRENCIES:
            log.warning(
                f'Tried to query coingecko {location} from {from_asset.identifier} '
                f'to {to_asset.identifier}. But to_asset is not supported',
            )
            return None

        return vs_currency

    def query_current_price(self, from_asset: Asset, to_asset: Asset) -> Price:
        """Returns a simple price for from_asset to to_asset in coingecko

        Uses the simple/price endpoint of coingecko. If to_asset is not part of the
        coingecko simple vs currencies or if from_asset is not supported in coingecko
        price zero is returned.

        May raise:
        - RemoteError if there is a problem querying coingecko
        """
        vs_currency = Coingecko.check_vs_currencies(
            from_asset=from_asset,
            to_asset=to_asset,
            location='simple price',
        )
        if not vs_currency:
            return Price(ZERO)

        try:
            from_coingecko_id = from_asset.to_coingecko()
        except UnsupportedAsset:
            log.warning(
                f'Tried to query coingecko simple price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But from_asset is not supported in coingecko',
            )
            return Price(ZERO)

        result = self._query(
            module='simple/price',
            options={
                'ids': from_coingecko_id,
                'vs_currencies': vs_currency,
            })

        try:
            return Price(FVal(result[from_coingecko_id][vs_currency]))
        except KeyError as e:
            log.warning(
                f'Queried coingecko simple price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But got key error for {str(e)} when '
                f'processing the result.',
            )
            return Price(ZERO)

    def can_query_history(  # pylint: disable=no-self-use
            self,
            from_asset: Asset,  # pylint: disable=unused-argument
            to_asset: Asset,  # pylint: disable=unused-argument
            timestamp: Timestamp,  # pylint: disable=unused-argument
            seconds: Optional[int] = None,  # pylint: disable=unused-argument
    ) -> bool:
        return True  # noop for coingecko

    def rate_limited_in_last(  # pylint: disable=no-self-use
            self,
            seconds: Optional[int] = None,  # pylint: disable=unused-argument
    ) -> bool:
        return False  # noop for coingecko

    def _get_cached_price(self, from_asset: Asset, to_asset: Asset, date: str) -> Optional[Price]:
        price_history_dir = get_or_make_price_history_dir(self.data_directory)
        filename = (
            price_history_dir /
            f'{PRICE_HISTORY_FILE_PREFIX }{from_asset.identifier}_{to_asset.identifier}.json'
        )
        if not filename.is_file():
            return None

        with open(filename, 'r') as f:
            try:
                data: Dict[str, Price] = jsonloads_dict(f.read())
            except JSONDecodeError:
                return None

        if not isinstance(data, dict):
            return None

        return data.get(date, None)

    def _save_cached_price(
            self,
            from_asset: Asset,
            to_asset: Asset,
            date: str,
            price: Price,
    ) -> None:
        price_history_dir = get_or_make_price_history_dir(self.data_directory)
        filename = (
            price_history_dir /
            f'{PRICE_HISTORY_FILE_PREFIX }{from_asset.identifier}_{to_asset.identifier}.json'
        )
        data: Dict[str, Price] = {}
        if filename.is_file():
            with open(filename, 'r') as f:
                try:
                    data = jsonloads_dict(f.read())
                except JSONDecodeError:
                    data = {}

        if not isinstance(data, dict):
            data = {}

        data[date] = price
        with open(filename, 'w') as outfile:
            outfile.write(rlk_jsondumps(data))

    def query_historical_price(
            self,
            from_asset: Asset,
            to_asset: Asset,
            timestamp: Timestamp,
    ) -> Price:
        vs_currency = Coingecko.check_vs_currencies(
            from_asset=from_asset,
            to_asset=to_asset,
            location='historical price',
        )
        if not vs_currency:
            return Price(ZERO)

        try:
            from_coingecko_id = from_asset.to_coingecko()
        except UnsupportedAsset:
            log.warning(
                f'Tried to query coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But from_asset is not supported in coingecko',
            )
            return Price(ZERO)

        date = timestamp_to_date(timestamp, formatstr='%d-%m-%Y')
        cached_price = self._get_cached_price(from_asset=from_asset, to_asset=to_asset, date=date)
        if cached_price is not None:
            return cached_price

        result = self._query(
            module='coins',
            subpath=f'{from_coingecko_id}/history',
            options={
                'date': date,
                'localization': False,
            },
        )

        try:
            price = Price(FVal(result['market_data']['current_price'][vs_currency]))
        except KeyError as e:
            log.warning(
                f'Queried coingecko historical price from {from_asset.identifier} '
                f'to {to_asset.identifier}. But got key error for {str(e)} when '
                f'processing the result.',
            )
            return Price(ZERO)

        self._save_cached_price(from_asset, to_asset, date, price)
        return price