import datetime
import numpy as np
import pkg_resources
import pandas as pd
from .base import BaseConnection, BaseReader
from tensorflow.keras.callbacks import Callback

class ExperimentNamer:
    '''Class methods for naming experiments.'''

    def __init__(self):
        '''Get a list of experiment names.'''
        path = '/names.csv'
        filepath = pkg_resources.resource_filename(__name__, path)
        self.name_pool = list(pd.read_csv(filepath)['name'].values)

    def get_used_names(self, df):
        '''Get a list of all names in the field 'name' in a pandas dataframe.

        Parameters:
        -----------
        df (pd.DataFrame): dataframe where each row is an experiment

        '''

        # There are no previous experiment names
        if len(df) == 0:
            self.used_names = []
        else:
            self.used_names = list(df.experiment_name.values)

    def get_unused_names(self):
        '''
        Get a list a names from the name pool that aren't in the used names.
        '''

        self.unused_names = [name for name in self.name_pool if name not in self.used_names]

        # If all names are used, add a suffix to create an additional 1000 names
        if len(self.unused_names) == 0:
            for i in range(10):
                suffix = '_' + str(i)
                self.unused_names = [name + suffix for name in self.name_pool]

    def get_random_unused_name(self):
        ''' Draw a random name that isn't already used, and pop it from the list.'''

        rand_idx = np.random.choice(len(self.unused_names), 1)[0]
        random_name = self.unused_names.pop(rand_idx)
        return random_name

class ExperimentRecorder(ExperimentNamer, BaseReader, BaseConnection, Callback):
    '''Class methods for recording experiments in mongo.'''

    def __init__(self, config_path, experiment=None):

        # Instantiate the experiment namer and name pool
        ExperimentNamer.__init__(self)

        # Get the experiment configuration inherited from BaseReader
        self.get_config(config_path)

        if experiment != None:
            self.experiment = experiment

        # Establish connection with mongoDB
        self.establish_db_connection('manager')

        # Establish which epoch to start recording values
        assert 'start_recording' in list(self.experiment.keys()), 'A start_recording value must be included in the experiment yaml.'
        self.start_recording = self.experiment['start_recording']

        self.results = {}
        for name in self.experiment['train_metrics']:
            self.results[name] = []
            self.results['val_' + name] = []

        # Get any previous experiment names
        previous_experiments = list(self.col.find())
        df = pd.DataFrame(previous_experiments)

        # Determine which names have been used
        self.get_used_names(df)
        self.get_unused_names()

        # Draw a random name to call this experiment
        this_experiment_name = self.get_random_unused_name()
        self.experiment['experiment_name'] = this_experiment_name
        print('{:12} {} {}'.format('', 'this experiment is called: ', this_experiment_name))
        print('{:12} {} {} {}'.format('', 'WARNING', len(self.unused_names), 'experiment names remaining'))

        # Initalize empty lists for loss values
        self.loss = []
        self.val_loss = []

    def _merge_two_dicts(self, x, y):
        z = x.copy()
        z.update(y)
        return z

    def create_experiment_entry(self):
        date = {'date_created' : datetime.datetime.utcnow()}
        experiment = self._merge_two_dicts(self.experiment, date)
        self.entry_id = self.col.insert_one(experiment)
        print('Created experiment entry: ', self.entry_id.inserted_id)

    def update_experiment_results(self, results):
        self.col.update_one({'_id' : self.entry_id.inserted_id}, {'$set' : results})
        print('Experiment results succesfully recorded.')

    def on_epoch_begin(self, epoch, logs=None):
        if epoch == self.start_recording:
            self.create_experiment_entry()

    def on_epoch_end(self, epoch, logs={}):
        for name in self.experiment['train_metrics']:
            self.results[name].append(np.float64(logs[name]))
            self.results['val_'+name].append(np.float64(logs['val_'+name]))
        if epoch >= self.start_recording:
            self.update_experiment_results(self.results)


import xml.etree.ElementTree as ET
import os
import shutil

import xmltodict
from zipfile import ZipFile

import tableau_utilities.tableau_file.tableau_file_objects as tfo
from tableau_utilities.general.funcs import transform_tableau_object

class TableauFileError(Exception):
    """ A minimum viable exception. """

    def __init__(self, message):
        self.message = message

class TableauFile:
    """ The base class for a Tableau file, i.e. Datasource or Workbook. """

    def __init__(self, file_path):
        """
        Args:
            file_path (str): Path to a Tableau file

        """
        self.file_path = os.path.abspath(file_path)
        self.file_directory = os.path.dirname(self.file_path)
        self.file_basename = os.path.basename(self.file_path)
        self.extension = file_path.split('.')[-1]
        self.file_name = self.file_basename.replace(f'.{self.extension}', '')
        ''' Set on init '''
        self._tree: ET.ElementTree
        self._root: ET.Element
        self.has_extract_data: bool = False
        self.__extract_xml()

    def __extract_xml(self, path=None):
        """ Extracts the XML from a Tableau file.

        Args:
            path (str): The path to the zipped Tableau file

        Returns: The contents of the Tableau File
        """
        if not path:
            path = self.file_path

        if self.extension in ['tdsx', 'twbx']:
            with ZipFile(path) as zip_file:
                for z in zip_file.filelist:
                    if z.filename.split('.')[-1] not in ['tds', 'twb']:
                        self.has_extract_data = True
                        continue
                    self._tree = ET.parse(zip_file.open(z.filename))
                    self._root = self._tree.getroot()
        else:
            self._tree = ET.parse(path)
            self._root = self._tree.getroot()

    def unzip(self, unzip_all=False, extract_to=None):
        """ Unzips the Tableau File.

        Args:
            unzip_all (bool): True to unzip all zipped files
            extract_to: Override the source file directory and save the file to another location

        Returns: The path to the unzipped Tableau File
        """

        if extract_to is not None:
            file_dir = extract_to
        else:
            file_dir = self.file_directory

        tableau_file_path = None
        with ZipFile(self.file_path) as zip_file:
            for z in zip_file.filelist:
                ext = z.filename.split('.')[-1]
                if unzip_all:
                    zip_file.extract(member=z, path=file_dir)
                    if ext in ['tds', 'twb']:
                        tableau_file_path = os.path.join(file_dir, z.filename)
                elif not unzip_all and ext in ['tds', 'twb']:
                    zip_file.extract(member=z, path=file_dir)
                    tableau_file_path = os.path.join(file_dir, z.filename)
        return tableau_file_path

    def save(self):
        """ Save/Update the Tableau file with the XML changes made """
        if self.extension in ['tdsx', 'twbx']:
            # Rebuild the TDSX / TWBX archive file, with the updated archived TDS / TWB
            # Move the file into a temporary folder while updating
            temp_folder = os.path.join(self.file_directory, f'__TEMP_{self.file_name}')
            os.makedirs(temp_folder, exist_ok=False)
            temp_path = os.path.join(temp_folder, self.file_basename)
            shutil.move(self.file_path, temp_path)
            # Unzip the zipped files
            extracted_files = list()
            with ZipFile(temp_path) as z:
                for f in z.filelist:
                    ext = f.filename.split('.')[-1]
                    path = z.extract(member=f, path=temp_folder)
                    extracted_files.append(path)
                    if ext in ['tds', 'twb']:
                        xml_path = path
            # Update XML file
            self._tree.write(xml_path, encoding="utf-8", xml_declaration=True)
            # Repack the unzipped file
            with ZipFile(temp_path, 'w') as z:
                for file in extracted_files:
                    arcname = file.split(temp_folder)[-1]
                    z.write(file, arcname=arcname)
            # Move file back to the original folder and remove any unpacked contents
            shutil.move(temp_path, self.file_path)
            shutil.rmtree(temp_folder)
        else:
            # Update the Tableau file's contents
            self._tree.write(self.file_path, encoding="utf-8", xml_declaration=True)

class Datasource(TableauFile):
    """
        A class representation of a Tableau Datasource.
        Used to update a Tableau Datasource by interacting with various elements,
        such as Columns, Folders, Connections, Metadata, etc.
    """

    def __init__(self, file_path):
        """
        Args:
            file_path (str): Path to a Tableau Datasource file; tds or tdsx
        """
        super().__init__(file_path)
        # Validate the file on initialization
        if self.extension not in ['tds', 'tdsx']:
            raise TableauFileError('File must be TDS or TDSX')

        self.connection: tfo.ParentConnection = self.__get_section(tfo.ParentConnection)
        self.aliases: tfo.Aliases = self.__get_section(tfo.Aliases)
        self.columns: tfo.TableauFileObjects[tfo.Column] = self.__get_section(tfo.Column, enforce_list=True)
        self.column_instance: tfo.ColumnInstance = self.__get_section(tfo.ColumnInstance)
        self.drill_paths: tfo.DrillPaths = self.__get_section(tfo.DrillPaths)
        self.folders_common: tfo.FoldersCommon = self.__get_section(tfo.FoldersCommon)
        self.date_options: tfo.DateOptions = self.__get_section(tfo.DateOptions)
        self.extract: tfo.Extract = self.__get_section(tfo.Extract)

    def __delattr__(self, attr):
        section = getattr(self, attr)
        if not section:
            return None
        # Remove the section from the parent Element
        parent = self._root.find('.')
        self.__remove_section_from_parent(parent, section.tag)
        # Set the section to None
        setattr(self, attr, None)

    def sections(self):
        """ Yields each section defined in the class, for iteration """
        yield self.connection
        yield self.aliases
        yield self.columns
        yield self.column_instance
        yield self.drill_paths
        yield self.folders_common
        yield self.date_options
        yield self.extract

    @staticmethod
    def __remove_section_from_parent(parent, tag) -> list[tuple[int, ET.Element]]:
        """ Removes all elements of a section from the parent, and returns those elements
        Args:
            parent (ET.Element): The parent element to remove the section from
            tag (str): The tag of the section element(s)

        Returns: A list of (index, Element) for the elements removed from the parent Element
        """
        # A section can be multiple elements within the parent element
        elements = [(i, e) for i, e in enumerate(parent) if e.tag.endswith(f'true...{tag}') or e.tag == tag]
        for _, e in elements:
            parent.remove(e)
        return elements

    def __get_section(self, obj, enforce_list=False):
        """ Sets DatasourceItems for each section

        Args:
            obj (type[tfo.TableauFileObject]): A Tableau File Object; ParentConnection, Column, etc
            enforce_list (bool): True if the section should be a TableauFileObjects list
        """
        parent = self._root.find('.')
        # Gets elements within the parent element, with the appropriate section.tag
        section: list[dict] = list()
        for element in parent:
            if element.tag.endswith(f'true...{obj.tag}') or element.tag == obj.tag:
                item = xmltodict.parse(ET.tostring(element))[element.tag]
                if not item:
                    continue
                new_item = transform_tableau_object(item)
                try:
                    section.append(obj(**new_item))
                except TypeError as err:
                    raise TableauFileError(f'{err}\n\nPre-transform {obj.tag} attributes: {item}') from err
        if len(section) > 1 or len(section) == 1 and enforce_list:
            return tfo.TableauFileObjects(section, item_class=obj, tag=obj.tag)
        if len(section) == 1:
            return section[0]
        if enforce_list:
            return tfo.TableauFileObjects(item_class=obj, tag=obj.tag)
        return obj()

    def enforce_column(self, column, folder_name=None, remote_name=None):
        """
            Enforces a column by:
                - Adding the column if it doesn't exist, otherwise updating it to match the column
                - Adding the column's corresponding folder-item to the appropriate folder, if it doesn't exist
                    - Create the folder if it doesn't exist
                - Updating the metadata local-name to map to the column name
                - Adding the column mapping to the mapping cols, if it doesn't exist

        Args:
            column (tfo.Column): The TableFile Column object
            remote_name (str): The name of the column from the connection (not required for Tableau Calculations),
             i.e. the SQL alias if the connection is a SQL query
            folder_name (str): The name of the folder that the column should be in

        """
        # Add Column
        if column not in self.columns:
            self.columns.add(column)
        # Update the Column
        else:
            self.columns.update(column)

        # Add Folder / FolderItem for the column, if folder_name was provided
        if folder_name:
            # Remove the column's folder-item for preview folder, if it will be moved to a new folder
            current_folder = [f for f in self.folders_common.folder if f.folder_item.get(column.name)]
            if current_folder and current_folder[0].name != folder_name:
                current_folder[0].folder_item.delete(column.name)
                self.folders_common.folder.update(current_folder[0])
            # Add column to the specified folder
            folder = self.folders_common.folder.get(folder_name)
            folder_item = tfo.FolderItem(name=column.name)
            if folder and folder_item not in folder.folder_item:
                folder.folder_item.append(folder_item)
                self.folders_common.folder.update(folder)
            elif not folder:
                self.folders_common.folder.add(tfo.Folder(name=folder_name, folder_item=[folder_item]))

        # If a remote_name was provided, and the column is not a Tableau Calculation - enforce metadata
        if not remote_name or column.calculation:
            return None

        # Update Connection MetadataRecords & MappingCols
        connection_record = self.connection.metadata_records.get(remote_name)
        if not connection_record:
            raise TableauFileError(f'Remote name provided is not in the metadata of the connection: {remote_name}')
        connection_record.local_name = column.name
        self.connection.metadata_records.update(connection_record)
        conn_col = tfo.MappingCol(key=column.name, value=f'{connection_record.parent_name}.[{remote_name}]')
        # Update Connection MappingCols
        if conn_col not in self.connection.cols:
            # Update the Connection MappingCol if key or value is different
            found = False
            for col in self.connection.cols:
                if conn_col.key == col.key or conn_col.value == col.value:
                    found = True
                    self.connection.cols[col].key = conn_col.key
                    self.connection.cols[col].value = conn_col.value
            # Otherwise, Add Connection MappingCol
            if not found:
                self.connection.cols.append(conn_col)

        # Update Extract MetadataRecords & MappingCols
        if not self.extract:
            return None
        extract_record = self.extract.connection.metadata_records.get(remote_name)
        if not extract_record:
            raise TableauFileError(f'Remote name provided is not in the metadata of the extract: {remote_name}')
        extract_record.local_name = column.name
        self.extract.connection.metadata_records.update(extract_record)
        extract_col = tfo.MappingCol(key=column.name, value=f'{extract_record.parent_name}.[{remote_name}]')
        # Update Extract MappingCols
        if extract_col not in self.extract.connection.cols:
            # Update the Extract MappingCol if key or value is different
            found = False
            for col in self.extract.connection.cols:
                if extract_col.key == col.key or extract_col.value == col.value:
                    found = True
                    self.extract.connection.cols[col].key = extract_col.key
                    self.extract.connection.cols[col].value = extract_col.value
            # Otherwise, Add Extract MappingCol
            if not found:
                self.extract.connection.cols.append(extract_col)

    def save(self):
        """ Save all changes made to each section of the Datasource """
        parent = self._root.find('.')
        ending_index = -1
        for section in self.sections():
            if not section:
                continue
            # Remove and get all elements of the section from the parent element
            elements = self.__remove_section_from_parent(parent, section.tag)
            # If there are no existing element(s), the index will be for the previous ending_index (default == -1)
            starting_index = elements[0][0] if elements else ending_index
            ending_index = elements[-1][0] + 1 if elements else starting_index
            # Insert the new / updated items
            if isinstance(section, tfo.TableauFileObjects):
                section.reverse()
                for idx, item in enumerate(section, 1):
                    parent.insert(starting_index, item.xml())
                    ending_index = starting_index + idx
            else:
                parent.insert(starting_index, section.xml())
        super().save()

if __name__ == '__main__':
    # Params
    ds_path = 'downloads/Users + Orgs.tdsx'

    unzip = False
    unzip_all_files = False

    ds = Datasource(ds_path)
    if unzip:
        ds.unzip(unzip_all=unzip_all_files)

    print(ds.columns.get('[USER_ID]'))


import os
import pprint
import shlex
import subprocess
from collections import OrderedDict
from contextlib import contextmanager

from ruamel.yaml import YAML
from cookiecutter.utils import rmtree

yaml = YAML()
dir_path = os.path.dirname(os.path.realpath(__file__))
travis_config_samples_path = os.path.join(dir_path, "sample_travis_configs")

@contextmanager
def inside_dir(dirpath):
    """
    Execute code from inside the given directory
    :param dirpath: String, path of the directory the command is being run.
    """
    old_path = os.getcwd()
    try:
        os.chdir(str(dirpath))
        yield
    finally:
        os.chdir(old_path)

@contextmanager
def bake_in_temp_dir(cookies, *args, **kwargs):
    """
    Delete the temporal directory that is created when executing the tests
    :param cookies: pytest_cookies.Cookies,
        cookie to be baked and its temporal files will be removed
    """
    result = cookies.bake(*args, **kwargs)
    try:
        yield result
    finally:
        rmtree(str(result.project))

def run_inside_dir(command, dirpath):
    """
    Run a command from inside a given directory, returning the exit status
    :param command: Command that will be executed
    :param dirpath: String, path of the directory the command is being run.
    """
    with inside_dir(dirpath):
        try:
            return int(subprocess.check_call(shlex.split(command)))
        except subprocess.CalledProcessError as e:
            print("Error")
            print(e.output)
            return int(e.returncode)

def sort_dict(d):
    res = OrderedDict()
    for k, v in sorted(d.items()):
        if isinstance(v, dict):
            res[k] = sort_dict(v)
        else:
            res[k] = v
    return res

class Fix:
    def __init__(self, config_file, project_dir, force=True):
        self.config_file = config_file
        self.project_dir = project_dir
        self.project_config_file = project_dir.join(".travis.yml")
        self.force = force
        self.before, self.after = self.parse_configs()

    def run_fix(self, force=None):
        if force is None:
            force = self.force
        # Replace with test .travis config
        command = "cp {} {}".format(self.config_file, self.project_config_file)
        subprocess.check_call(shlex.split(command))
        result = run_inside_dir(
            "invoke fix-token --no-verify {}".format("--force" if force else ""),
            self.project_dir,
        )
        self.before, self.after = self.parse_configs()
        print(result, type(result))
        return result

    def parse_configs(self):
        with open(self.config_file, "r") as _source_file:
            with open(self.project_config_file, "r") as _destination_file:
                before = sort_dict(yaml.load(_source_file))
                after = sort_dict(yaml.load(_destination_file))
                return before, after

    def get(self):
        return self.before, self.after

    def debug(self):
        print("##################################")
        [pprint.pprint(p) for p in self.get()]
        print("##################################")

    def was_modified(self):
        return self.before != self.after

    def get_final_token(self):
        try:
            releases_stages = [
                stage
                for stage in self.after["jobs"]["include"]
                if stage.get("deploy", dict()).get("provider") == "releases"
            ]
            return releases_stages[0]["deploy"]["token"]["secure"]
        except (TypeError, KeyError):
            return None

def _test_fix_travis___after_travis_cli_equally_modified(project_dir):
    after_travis_cli_equally_modified = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "after_travis_cli.equally.modified.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert after_travis_cli_equally_modified.run_fix(force=False) == 0
    assert after_travis_cli_equally_modified.was_modified()

def _test_fix_travis___after_travis_cli_modified(project_dir):
    after_travis_cli_modified = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "after_travis_cli.modified.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert after_travis_cli_modified.run_fix(force=False) == 1
    assert not after_travis_cli_modified.was_modified()

    assert after_travis_cli_modified.run_fix(force=True) == 0
    assert after_travis_cli_modified.was_modified()
    assert after_travis_cli_modified.get_final_token() == "NEW_GH_TOKEN"

def _test_fix_travis___after_travis_cli_multiple_deploy(project_dir):
    after_travis_cli_multiple_deploy = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "after_travis_cli.multiple_deploy.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert after_travis_cli_multiple_deploy.run_fix(force=False) == 1
    assert not after_travis_cli_multiple_deploy.was_modified()
    assert after_travis_cli_multiple_deploy.run_fix(force=True) == 1
    assert not after_travis_cli_multiple_deploy.was_modified()

def _test_fix_travis___after_travis_cli_no_deploy(project_dir):
    after_travis_cli_no_deploy = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "after_travis_cli.no_deploy.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert after_travis_cli_no_deploy.run_fix(force=False) == 1
    assert not after_travis_cli_no_deploy.was_modified()
    assert after_travis_cli_no_deploy.run_fix(force=True) == 1
    assert not after_travis_cli_no_deploy.was_modified()

def _test_fix_travis___after_travis_cli_unmodified(project_dir):
    after_travis_cli_unmodified = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "after_travis_cli.unmodified.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert after_travis_cli_unmodified.run_fix(force=False) == 0
    assert after_travis_cli_unmodified.was_modified()
    assert after_travis_cli_unmodified.get_final_token() == "NEW_GH_TOKEN"
    assert after_travis_cli_unmodified.run_fix(force=True) == 0
    assert after_travis_cli_unmodified.was_modified()
    assert after_travis_cli_unmodified.get_final_token() == "NEW_GH_TOKEN"

def _test_fix_travis___before_travis_cli(project_dir):
    before_travis_cli = Fix(
        config_file=os.path.join(
            travis_config_samples_path, "before_travis_cli.travis.yml"
        ),
        project_dir=project_dir,
    )
    assert before_travis_cli.run_fix(force=False) == 1
    assert not before_travis_cli.was_modified()
    assert before_travis_cli.run_fix(force=True) == 1
    assert not before_travis_cli.was_modified()

def test_fix_travis(cookies):
    with bake_in_temp_dir(cookies) as result:
        _test_fix_travis___after_travis_cli_equally_modified(project_dir=result.project)
        _test_fix_travis___after_travis_cli_modified(project_dir=result.project)
        _test_fix_travis___after_travis_cli_multiple_deploy(project_dir=result.project)
        _test_fix_travis___after_travis_cli_no_deploy(project_dir=result.project)
        _test_fix_travis___after_travis_cli_unmodified(project_dir=result.project)
        _test_fix_travis___before_travis_cli(project_dir=result.project)


"""
Utilities for creating built-in modules
"""

from dataclasses import field, dataclass
from immutables import Map
from typing import Callable,  NoReturn, Optional, Sequence, TypeVar, Dict, Union
from . import vm_utils
from . import parser
from .types import Code, CodeFlags, Instruction, Scope, Stack, State, Value, NativeFunction, Vec

Z = TypeVar("Z", bound=Stack, contravariant=True)

def _fail(name: str, reason: str, stack: Stack):
    print("Failure in function", name)
    print("Reason:", reason)
    print("> Stack: ", "[" + " ".join(map(vm_utils.stringify_value, vm_utils.repr_stack(stack))) + "]")
    raise RuntimeError(name, reason)

Fail = Callable[[str], NoReturn]

@dataclass
class BuiltinModule:
    name: str
    members: Dict[str, Value] = field(init=False)

    @property
    def exports(self) -> Sequence[str]:
        return tuple(self.members)

    def __post_init__(self):
        self.members = {}

    def add(self, member_name: str, value: Value):
        self.members[member_name] = value

    def register_simple(self, name: Optional[str] = None):
        def inner(fn: Callable[[Z, Fail], Stack]) -> NativeFunction:
            native_fn = make_simple(name)(fn)  # type: ignore
            self.add(native_fn.name, native_fn)
            return native_fn
        return inner

    def register(self, name: Optional[str] = None):
        def inner(fn: Callable[[State, Fail], State]) -> NativeFunction:
            native_fn = make_function(name)(fn)  # type: ignore
            self.add(native_fn.name, native_fn)
            return native_fn
        return inner

    def make_scope(self, id: int):
        return Scope(parent=None, id=id, values=Map(self.members))

    def make_scope_with_existing_state(self, id: int, state: State):
        from . import vm
        scope = Scope(parent=vm.builtin_scope.id, id=id, values=Map(self.members))
        return scope, state.set_scope(id, scope)

@dataclass
class GurklangModule:
    name: str
    exports: Sequence[str]
    source_code: str

    def make_scope(self, id: int):
        from . import vm
        state = (
            State
            .make(vm.global_scope, vm.builtin_scope)
            .make_scope(vm.global_scope.id, id, persistent=True)
        )
        fn = Code(
            parser.parse(self.source_code),
            closure=None,
            flags=CodeFlags.PARENT_SCOPE,
            name=f"<module-{self.name}>"
        )
        state = vm.call(state, fn)
        return state.scopes[id]

    def make_scope_with_existing_state(self, id: int, state: State):
        scope = self.make_scope(id)
        return scope, state.set_scope(id, scope)

Module = Union[BuiltinModule, GurklangModule]

def make_simple(name: Optional[str] = None):
    def inner(fn: Callable[[Z, Fail], Stack]) -> NativeFunction:
        def new_fn(state: State, fail: Fail):
            stack = fn(state.stack, fail)
            return state.with_stack(stack)
        new_fn.__name__ = fn.__name__
        return make_function(name)(new_fn)
    return inner

def make_function(name: Optional[str] = None):
    def inner(fn: Callable[[State, Fail], State]) -> NativeFunction:
        fn_name = name or fn.__name__.replace("_", "-")
        def new_fn(state: State):
            local_fail: Fail = lambda reason: _fail(fn_name, reason, state.stack)
            try:
                return fn(state, local_fail)
            except Exception as e:
                local_fail(f"uncaught exception {type(e).__name__}: {' '.join(map(str, e.args))}")
        native_fn = NativeFunction(new_fn, fn_name)
        return native_fn
    return inner

def raw_function(*instructions: Instruction, name: str = "<raw>", source_code: Optional[str] = None):
    """
    Create `Code` with no closure and flags set to PARENT_SCORE
    """
    return Code(
        instructions,
        closure=None,
        flags=CodeFlags.PARENT_SCOPE,
        name=name,
        source_code=source_code
    )

def vec_to_stack(t: Value, fail: Fail) -> Stack:
    stack = None
    if t.tag != "vec":
        fail(f"expected tuple, got {t}")
    while True:
        if len(t.values) == 0:
            return stack
        if len(t.values) != 2:
            fail(f"got tuple of size {len(t.values)} {vm_utils.stringify_value(t)}, expected 2")
        head, rest = t.values
        if rest.tag != "vec":
            fail(f"expected tuple as second element, got {rest}")
        t = rest
        stack = (head, stack)

def stack_to_vec(stack: Stack) -> Vec:
    rv = Vec([])
    while stack is not None:
        head, stack = stack  # type: ignore
        rv = Vec([head, rv])
    return rv


import os
import json
import requests

# load the API token
API_TOKEN = os.getenv('RW_API_KEY')

# input dataset API ID of which you want to edit widget
dataset_id = ''

# the headers for all requests in this script
headers = {'Authorization': 'Bearer ' + API_TOKEN, 'Content-Type': 'application/json'}

# input the API id of the widget you want to edit
widget_id = ''

# include in the widget payload the fields you would like to edit
'''
for example:
    widget_payload = {"description": 'this is for a test'}
'''
widget_payload = {}

# send the request to edit the widget on the API
try:
    url = f'https://api.resourcewatch.org/v1/dataset/{dataset_id}/widget/{widget_id}'
    r = requests.patch(url, data=json.dumps(widget_payload), headers=headers)
    print(r.json())
except:
    print('Failed to edit a widget')
    print('Response from API request: {}'.format(r.content))


import numpy as np
import cv2

YOLO_input_size = 448
classes =  ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair",
                "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant",
                "sheep", "sofa", "train","tvmonitor"]

class Box:
    def __init__(self):
        self.x, self.y = float(), float()
        self.w, self.h = float(), float()
        self.c = float()
        self.prob = float()

def overlap(x1,w1,x2,w2):
    l1 = x1 - w1 / 2.;
    l2 = x2 - w2 / 2.;
    left = max(l1, l2)
    r1 = x1 + w1 / 2.;
    r2 = x2 + w2 / 2.;
    right = min(r1, r2)
    return right - left;

def box_intersection(a, b):
    w = overlap(a.x, a.w, b.x, b.w);
    h = overlap(a.y, a.h, b.y, b.h);
    if w < 0 or h < 0: return 0;
    area = w * h;
    return area;

def box_union(a, b):
    i = box_intersection(a, b);
    u = a.w * a.h + b.w * b.h - i;
    return u;

def box_iou(a, b):
    return box_intersection(a, b) / box_union(a, b);

def YoloOut2Boxes(net_out, threshold = 0.2, sqrt=1.8,C=20, B=2, S=7):
    class_vehicle = 6
    boxes = []
    SS        =  S * S # number of grid cells
    prob_size = SS * C # class probabilities
    conf_size = SS * B # confidences for each grid cell

    probs = net_out[0 : prob_size]
    confs = net_out[prob_size : (prob_size + conf_size)]
    cords = net_out[(prob_size + conf_size) : ]
    probs = probs.reshape([SS, C])
    confs = confs.reshape([SS, B])
    cords = cords.reshape([SS, B, 4])

    for grid in range(SS):
        for b in range(B):
            bx = Box()
            # Formally we define confidence as Pr(Object) * IOU(truth, predicted).
            # If no object exists in that cell, the confidence scores should be
            # zero. Otherwise we want the confidence score to equal the
            # intersection over union (IOU) between the predicted box
            # and the ground truth.
            bx.c =  confs[grid, b]
            # The (x, y) coordinates represent the center of the box relative
            # to the bounds of the grid cell.
            bx.x = (cords[grid, b, 0] + grid %  S) / S
            bx.y = (cords[grid, b, 1] + grid // S) / S
            # Sum-squared error also equally weights errors in large
            # boxes and small boxes. Our error metric should reflect that
            # small deviations in large boxes matter less than in small
            # boxes. To partially address this we predict the square root
            # of the bounding box width and height instead of the width
            # and height directly.
            bx.w =  cords[grid, b, 2] ** sqrt
            bx.h =  cords[grid, b, 3] ** sqrt
            # Pr(Class(i) | Object) * Pr(Object) * IOU(truth, predicted) = Pr(Class(i)) * IOU(truth, predicted)
            # for more details, see equation(1) in the paper <You Only Look Once:
            # Unified, Real-Time Object Detection>
            p = probs[grid, :] * bx.c

            if p[class_vehicle] >= threshold:
                bx.prob = p[class_vehicle]
                boxes.append(bx)

    # combine boxes that are overlap
    boxes.sort(key=lambda b:b.prob,reverse=True)
    for i in range(len(boxes)):
        boxi = boxes[i]
        if boxi.prob == 0: continue
        for j in range(i + 1, len(boxes)):
            boxj = boxes[j]
            if box_iou(boxi, boxj) >= .4:
                boxes[j].prob = 0.
    boxes = [b for b in boxes if b.prob > 0.]

    return boxes

def Boxes2BB(boxes, h, w):
    if len(boxes)==0:
        return

    bb_box = np.zeros(shape=[len(boxes), 5])
    for i, b in enumerate(boxes):
        left  = int ((b.x - b.w/2.) * w)
        right = int ((b.x + b.w/2.) * w)
        top   = int ((b.y - b.h/2.) * h)
        bot   = int ((b.y + b.h/2.) * h)
        if left  < 0    :  left = 0
        if right > w - 1: right = w - 1
        if top   < 0    :   top = 0
        if bot   > h - 1:   bot = h - 1

        # left, right, top, bot = Square(left, right, top, bot, h, w)
        bb_box[i,0] = left
        bb_box[i,2] = right
        bb_box[i,1] = top
        bb_box[i,3] = bot
        bb_box[i,4] = b.prob
    return bb_box

# enlarge each box to a squared shape
def Square(left, right, top, bot, max_h, max_w):
    w = right - left
    h = bot - top
    diff1 = np.abs((w-h)//2)
    diff2 = np.abs(w-h) - diff1
    left = left if w > h else left - diff1
    right = right if w > h else right + diff2
    top = top if h > w else top - diff1
    bot = bot if h > w else bot + diff2

    #check if box is off the margin

    if left < 0:
        margin = 0 - left
        left +=margin
        right += margin

    if right > max_w:
        margin = right - max_w
        left -= margin
        right -= margin

    if top < 0:
        margin = 0 - top
        top += margin
        bot += margin

    if bot > max_h:
        margin = bot - max_h
        top -= margin
        bot -= margin

    return left, right, top, bot

# Crop the origin image so that the output is a square
def CropImage(image):
    assert len(image.shape)==3
    h, w, _ = image.shape
    diff1 = np.abs(w-h)//2
    diff2 = np.abs(w-h) - diff1

    w_begin = 0 if w < h else diff1
    w_end = w if w < h else w - diff2
    h_begin = 0 if h < w else diff1
    h_end = h if h < w else h - diff2

    image_cropped = image[h_begin:h_end, w_begin:w_end, :]
    offset_w = diff1 if w > h else 0
    offset_h = diff1 if h > w else 0
    return image_cropped, [offset_h, offset_w]

# Enlarge the origin image so that the output is a square
def EnlargeImage(image):
    assert len(image.shape)==3
    h, w, _ = image.shape
    diff1 = np.abs(w-h)//2
    diff2 = np.abs(w-h) - diff1

    if h > w:
        first = np.zeros(shape=[h, diff1, 3], dtype=np.uint8)
        second = image
        third = np.zeros(shape=[h, diff2, 3], dtype=np.uint8)
        enlarged = np.concatenate([first, second, third], axis=1)

    elif w > h:
        first = np.zeros(shape=[diff1, w, 3], dtype=np.uint8)
        second = image
        third = np.zeros(shape=[diff2, w, 3], dtype=np.uint8)
        enlarged = np.concatenate([first, second, third], axis=0)
    else:
        enlarged = image

    offset_w = -diff1 if h > w else 0
    offset_h = -diff1 if w > h else 0
    return enlarged, [offset_h, offset_w]

# Resize the image so that it fits the network inputs
# it is assumed that the input image is a square image
def ResizeImage(image, size_target=YOLO_input_size):
    assert len(image.shape)==3
    assert image.shape[0] == image.shape[1]
    size_origin = image.shape[0]
    ratio = size_origin/size_target
    image_resized = cv2.resize(image,(size_target,size_target))
    return image_resized, ratio

def DetectVehicle(image, model, threshold=0.17):
    # image_squared, [offset_h, offset_w] = EnlargeImage(image)
    image_squared, [offset_h, offset_w] = CropImage(image)
    image_resized, ratio = ResizeImage(image_squared)
    batch = np.transpose(image_resized,(2,0,1))
    batch = 2*(batch/255.) - 1
    batch = np.expand_dims(batch, axis=0)
    out = model.predict(batch)
    boxes = YoloOut2Boxes(out[0], threshold = threshold)

    if len(boxes)==0:
        return

    bb = Boxes2BB(boxes, YOLO_input_size, YOLO_input_size)
    # cordinates of bb is based on resized image of shape(448,448)
    # need to convert the cordinates so that it is based on original image
    bb[:,:-1] *= ratio
    bb[:,[0, 2]] += offset_w
    bb[:,[1, 3]] += offset_h

    for i in range(bb.shape[0]):
        left, right, top, bot = Square(bb[i,0], bb[i,2], bb[i,1], bb[i,3], image.shape[0], image.shape[1])
        bb[i,0] = left
        bb[i,2] = right
        bb[i,1] = top
        bb[i,3] = bot
    return bb

# if __name__=='__main__':
def Test():
    from model import GetModel
    from os.path import join
    import matplotlib.pyplot as plt

    model = GetModel()
    root = 'E:\\DM\\Udacity\\Public Security\\TieLing_tmp'
    name = '701214002172473803010830164_铁岭良工阀门门口北向南（铁抚街）.2018-02-12 133228.-.X99.Z.99.4.33.jpg'
    imagePath = join(root, name)
    image = plt.imread(imagePath)
    bb = DetectVehicle(image, model)
    bb = bb[0].astype(np.int)
    cropped = image[bb[1]:bb[3],bb[0]:bb[2],:]
    f,(ax1,ax2) = plt.subplots(1,2,figsize=(16,6))
    ax1.imshow(image)
    ax2.imshow(cropped)


#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Author(s):

#   Christian Kliche <chk@ebp.de>
#   Panu Lahtinen <panu.lahtinen@fmi.fi>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

'''This module defines functions to create images out of dataset messages
'''
from urlparse import urlparse
from functools import partial
import logging
import numpy as np
import scipy.ndimage as ndi
from fnmatch import fnmatch
from mpop.projector import get_area_def
from pyresample.geometry import AreaDefinition
from dwd_extensions.tools.image_io import read_image
from datetime import datetime

LOGGER = logging.getLogger(__name__)

def create_world_composite(msg, proc_func_params):
    """
    Creates a world composite images out of an dataset message
    """
    items = []
    for elem in msg.data['dataset']:
        url = urlparse(elem['uri'])
        if url.netloc != '':
            LOGGER.error('uri not supported: %s',
                         format(elem['uri']))
            return None

        area = get_area_def(msg.data['area']['name'])
        t_gatherer = msg.data['gatherer_time']
        if not isinstance(t_gatherer, datetime):
            try:
                t_gatherer = datetime.strptime(
                    t_gatherer, '%Y%m%d%H%M%S')
            except:
                t_gatherer = None
        items.append((url.path, area, t_gatherer))

    lon_limits = {}
    erosion_size = None
    smooth_width = None

    if proc_func_params:
        # order images
        if 'order' in proc_func_params:
            order_list = proc_func_params['order'].split('|')
            sort_key = partial(_match_order_index, order_list)
            items = sorted(items, key=sort_key, reverse=True)

        if 'lon_limits' in proc_func_params:
            sat_lon_list = proc_func_params['lon_limits'].split('|')
            for sat_lon in sat_lon_list:
                sat, min_lon, max_lon = sat_lon.split(',')
                lon_limits[sat] = (float(min_lon), float(max_lon))

        if 'erosion_size' in proc_func_params:
            erosion_size = float(proc_func_params['erosion_size'])

        if 'smooth_width' in proc_func_params:
            smooth_width = float(proc_func_params['smooth_width'])

    return _create_world_composite(items, lon_limits=lon_limits,
                                   erosion_size=erosion_size,
                                   smooth_width=smooth_width)

def _match_order_index(order_list, item):
    for idx, pattern in enumerate(order_list):
        if fnmatch(item[0], pattern):
            return idx
    return len(order_list)

def _create_world_composite(items, lon_limits=None,
                            erosion_size=20,
                            smooth_width=20):
    # smooth_sigma = 4

    img = None
    for (path, area, timeslot) in items:

        if not isinstance(area, AreaDefinition):
            area = get_area_def(area)

        next_img = read_image(path, area, timeslot)

        if img is None:
            img = next_img
        else:
            # scaled_smooth_sigma = smooth_sigma * (float(img.width) / 1000.0)

            img_mask = reduce(np.ma.mask_or,
                              [chn.mask for chn in img.channels])
            next_img_mask = reduce(np.ma.mask_or,
                                   [chn.mask for chn in next_img.channels])

            # Mask overlapping areas away
            if lon_limits:
                for sat in lon_limits:
                    if sat in path:
                        mask_limits = calc_pixel_mask_limits(area,
                                                             lon_limits[sat])
                        for lim in mask_limits:
                            next_img_mask[:, lim[0]:lim[1]] = 1
                        break

            alpha = np.ones(next_img_mask.shape, dtype='float')
            alpha[next_img_mask] = 0.0

            if erosion_size is not None and smooth_width is not None:
                scaled_erosion_size = erosion_size * (float(img.width) /
                                                      1000.0)
                scaled_smooth_width = smooth_width * (float(img.width) /
                                                      1000.0)

                # smooth_alpha = ndi.gaussian_filter(
                #     ndi.grey_erosion(alpha, size=(scaled_erosion_size,
                #                                   scaled_erosion_size)),
                #        scaled_smooth_sigma)
                smooth_alpha = ndi.uniform_filter(
                    ndi.grey_erosion(alpha, size=(scaled_erosion_size,
                                                  scaled_erosion_size)),
                    scaled_smooth_width)
                smooth_alpha[img_mask] = alpha[img_mask]
            else:
                smooth_alpha = alpha

            for i in range(0, min(len(img.channels), len(next_img.channels))):
                chdata = next_img.channels[i].data * smooth_alpha + \
                    img.channels[i].data * (1 - smooth_alpha)
                chmask = np.logical_and(img_mask, next_img_mask)
                img.channels[i] = \
                    np.ma.masked_where(chmask, chdata)

    return img

def calc_pixel_mask_limits(adef, lon_limits):
    """Calculate pixel intervals from longitude ranges."""
    # We'll assume global grid from -180 to 180 longitudes
    scale = 360. / adef.shape[1]  # degrees per pixel

    left_limit = int((lon_limits[0] + 180) / scale)
    right_limit = int((lon_limits[1] + 180) / scale)

    # Satellite data spans 180th meridian
    if right_limit < left_limit:
        return [[right_limit, left_limit]]
    else:
        return [[0, left_limit], [right_limit, adef.shape[1]]]


"""
MGF is a simple human-readable format for MS/MS data. It
allows storing MS/MS peak lists and exprimental parameters.

This module provides :class:`MGFLoader`, a :class:`~.RandomAccessScanSource`
implementation.

The parser is based on :mod:`pyteomics.mgf`.
"""

import os
from pyteomics import mgf
from pyteomics.auxiliary import OffsetIndex
import numpy as np

from six import string_types as basestring

from .scan import (
    ScanFileMetadataBase, RandomAccessScanSource, ScanDataSource,
    PrecursorInformation, _FakeGroupedScanIteratorImpl,
    ChargeNotProvided, Scan)

from .metadata.file_information import (
    FileInformation, MS_MSn_Spectrum)

from ._compression import test_if_file_has_fast_random_access

class _MGFParser(mgf.IndexedMGF):

    def parse_charge(self, charge_text, list_only=False):
        """
        Pyteomics _parse_charge is very general-purpose, and
        can't be sped up, so we specialize it here.
        """
        try:
            if not list_only:
                return int(charge_text.replace('+', ''))
            return list(map(self.parse_charge, charge_text.split(" ")))
        except Exception:
            if '-' in charge_text:
                return int(charge_text.replace("-", '')) * -1
            raise

    def parse_peak_charge(self, charge_text, list_only=False):
        return self.parse_charge(charge_text, list_only=list_only)

class _MGFMetadata(ScanFileMetadataBase):
    """
    Objects implementing this interface can describe the original source
    files, instrument configuration, and data processing parameters used to
    create the current spectral data file.

    Patterned after the provenance features of mzML that could also be mapped
    onto mzXML and other complete vendor readers.
    """

    def file_description(self):
        """
        Describe the file and its components, as well
        as any content types it has.

        Returns
        -------
        :class:`~.FileInformation`
        """
        finfo = FileInformation()
        finfo.add_content("centroid spectrum")
        finfo.add_content(MS_MSn_Spectrum)
        if isinstance(self.source_file, (basestring, os.PathLike)):
            finfo.add_file(self.source_file)
        elif hasattr(self.source_file, 'name'):
            finfo.add_file(self.source_file.name)
        return finfo

    def instrument_configuration(self):
        """
        Describe the different instrument components and configurations used
        to acquire scans in this run.

        Returns
        -------
        :class:`list` of :class:`~.InstrumentInformation`
        """
        return super(_MGFMetadata, self).instrument_configuration()

    def data_processing(self):
        """
        Describe any preprocessing steps applied to the data described by this
        instance.

        Returns
        -------
        :class:`list` of :class:`~.DataProcessingInformation`
        """
        return super(_MGFMetadata, self).data_processing()

class MGFInterface(ScanDataSource):
    """
    Provides a basic set of widely used MASCOT Generic File (MGF)
    data accessor mechanisms. Because MGF files lack any form of standardization,
    no strong guarantees of correctness can be made.

    This dialect does not know how to use the charge column of the peak data
    section, see :class:`~.ProcessedMGFLoader`.
    """

    def _scan_arrays(self, scan):
        """
        Returns raw data arrays for m/z and intensity

        Parameters
        ----------
        scan : Mapping
            The underlying scan information storage,
            usually a `dict`

        Returns
        -------
        mz: np.array
            An array of m/z values for this scan
        intensity: np.array
            An array of intensity values for this scan
        """
        try:
            return scan['m/z array'], scan["intensity array"]
        except KeyError:
            return np.array([]), np.array([])

    def _ms_level(self, scan):
        return 2

    def _scan_title(self, scan):
        """
        Returns a verbose name for this scan, if one
        were stored in the file. Usually includes both the
        scan's id string, as well as information about the
        original file and format.

        Parameters
        ----------
        scan : Mapping
            The underlying scan information storage,
            usually a `dict`

        Returns
        -------
        str
        """
        return scan['params']["title"].strip('.')

    def _scan_id(self, scan):
        """
        Returns the scan's id string, a unique
        identifier for this scan in the context of
        the data file it is recordered in

        Parameters
        ----------
        scan : Mapping
            The underlying scan information storage,
            usually a `dict`

        Returns
        -------
        str
        """
        return scan['params']["title"].strip('.')

    def _scan_time(self, scan):
        try:
            return float(scan['params']['rtinseconds']) / 60.0
        except KeyError:
            return -1

    def _is_profile(self, scan):
        return False

    def _precursor_information(self, scan):
        mz, intensity = scan['params']['pepmass']
        charge = scan['params'].get('charge', [ChargeNotProvided])[0]
        pinfo = PrecursorInformation(
            mz, intensity, charge, source=self,
            product_scan_id=self._scan_id(scan),
            defaulted=True, orphan=True)
        return pinfo

    def _polarity(self, scan):
        pinfo = self._precursor_information(scan)
        if pinfo is not None:
            if pinfo.charge:
                if pinfo.charge == ChargeNotProvided or pinfo.charge > 0:
                    return 1
                return -1
            return 1
        return 1

    def _activation(self, scan):
        return None

    def _scan_index(self, scan):
        """
        Returns the base 0 offset from the start
        of the data file in number of scans to reach
        this scan.

        If the original format does not natively include
        an index value, this value may be computed from
        the byte offset index.

        Parameters
        ----------
        scan : Mapping
            The underlying scan information storage,
            usually a `dict`

        Returns
        -------
        int
        """
        try:
            return self._title_to_index[self._scan_title(scan)]
        except KeyError:
            try:
                return self._title_to_index[self._scan_title(scan) + '.']
            except KeyError:
                return -1
        return -1

    def _annotations(self, scan):
        annots = dict()
        params = scan['params']
        for key, value in params.items():
            if key in ("pepmass", "charge", "title", "rtinseconds"):
                continue
            else:
                try:
                    if value[0].isdigit():
                        value = float(value)
                    elif value == 'None':
                        value = None
                except ValueError:
                    if value == 'None':
                        value = None
            annots[key] = value
        return annots

class MGFLoader(MGFInterface, RandomAccessScanSource[dict, Scan], _MGFMetadata):
    """
    Reads scans from MASCOT Generic File (MGF) Format files. Provides both iterative
    and random access.

    .. note::
        If the file is not sorted by retention time, :meth:`get_scan_by_time` and any
        other time-based accessors will fail.

    Attributes
    ----------
    source_file: str
        Path to file to read from.
    source: pyteomics.mgf.MGFBase
        Underlying scan data source
    header: dict
        Any top-of-the-file parameters
    """

    def __init__(self, source_file, encoding='utf-8', use_index=True, **kwargs):
        self.source_file = source_file
        self.encoding = encoding
        self._use_index = use_index
        self._source = self._create_parser()
        self.initialize_scan_cache()
        self.make_iterator()
        self._title_to_index = self._prepare_index_lookup()

    @property
    def has_fast_random_access(self):
        return test_if_file_has_fast_random_access(self.source.file)

    def _prepare_index_lookup(self):
        title_to_index = dict()
        for i, key in enumerate(self.index):
            title_to_index[key] = i
        return title_to_index

    @property
    def header(self):
        """
        Any top-of-the-file parameters

        Returns
        -------
        dict
        """
        return self._source.header

    def __reduce__(self):
        return self.__class__, (self.source_file, self.encoding, self._use_index, )

    def has_msn_scans(self):
        return True

    def has_ms1_scans(self):
        return False

    def _create_parser(self):
        if self._use_index:
            return _MGFParser(self.source_file, read_charges=False,
                              convert_arrays=1, encoding=self.encoding)
        simple_reader = mgf.MGF(
            self.source_file, read_charges=False,
            convert_arrays=1, encoding=self.encoding)
        simple_reader.index = OffsetIndex()
        return simple_reader

    def get_scan_by_id(self, scan_id):
        """
        Retrieve the scan object for the specified scan id.

        If the scan object is still bound and in memory somewhere,
        a reference to that same object will be returned. Otherwise,
        a new object will be created.

        Parameters
        ----------
        scan_id : str
            The unique scan id value to be retrieved

        Returns
        -------
        Scan
        """
        try:
            return self.scan_cache[scan_id]
        except KeyError:
            pass
        try:
            scan = self.source.get_spectrum(scan_id)
        except KeyError:
            scan = self.source.get_spectrum(scan_id + '.')
        scan = self._make_scan(scan)
        self.scan_cache[scan_id] = scan
        return scan

    def get_scan_by_index(self, index):
        """
        Retrieve the scan object for the specified scan index.

        This internally calls :meth:`get_scan_by_id` which will
        use its cache.

        Parameters
        ----------
        index: int
            The index to get the scan for

        Returns
        -------
        Scan
        """
        if not self._use_index:
            raise TypeError("This method requires the index. Please pass `use_index=True` during initialization")
        id_str = self.index.from_index(index)
        return self.get_scan_by_id(id_str)

    def get_scan_by_time(self, time):
        """
        Retrieve the scan object for the specified scan time.

        This internally calls :meth:`get_scan_by_id` which will
        use its cache.

        Parameters
        ----------
        time : float
            The time to get the nearest scan from

        Returns
        -------
        Scan
        """
        if not self._use_index:
            raise TypeError("This method requires the index. Please pass `use_index=True` during initialization")

        scan_ids = tuple(self.index)
        lo = 0
        hi = len(scan_ids)

        best_match = None
        best_error = float('inf')

        if time == float('inf'):
            return self.get_scan_by_id(scan_ids[-1])

        while hi != lo:
            mid = (hi + lo) // 2
            sid = scan_ids[mid]
            scan = self.get_scan_by_id(sid)
            scan_time = scan.scan_time
            err = abs(scan_time - time)
            if err < best_error:
                best_error = err
                best_match = scan
            if scan_time == time:
                return scan
            elif (hi - lo) == 1:
                return best_match
            elif scan_time > time:
                hi = mid
            else:
                lo = mid
        if hi == 0 and not self._use_index:
            raise TypeError("This method requires the index. Please pass `use_index=True` during initialization")

    @property
    def source(self):
        """The file parser that this reader consumes."""
        return self._source

    @property
    def index(self):
        """
        The byte offset index used to achieve fast random access.

        Maps :class:`~.ScanBase` IDs to the byte offsets, implying
        the order the scans reside in the file.

        Returns
        -------
        :class:`pyteomics.xml.ByteEncodingOrderedDict`
        """
        return self.source.index

    def __len__(self):
        return len(self.index)

    def close(self):
        """Close the underlying reader."""
        self._source.close()
        self._dispose()

    def reset(self):
        """
        Reset the object, clearing out any existing
        state.

        This resets the underlying file iterator, then
        calls :meth:`make_iterator`, and clears the scan
        cache.
        """
        self._source.reset()
        try:
            self.source.seek(0)
        except (IOError, AttributeError):
            pass
        self.make_iterator(None)
        self.initialize_scan_cache()

    def _make_default_iterator(self):
        return iter(self._source)

    def make_iterator(self, iterator=None, grouped=False):
        """
        Configure the iterator's behavior.

        Parameters
        ----------
        iterator : Iterator, optional
            The iterator to manipulate. If missing, the default
            iterator will be used.
        grouped : bool, optional
            Whether the iterator should be grouped and produce :class:`.ScanBunch` objects
            or single :class:`.Scan`. Defaults to False
        """
        return super(MGFLoader, self).make_iterator(iterator, grouped)

    def _yield_from_index(self, scan_source, start):
        offset_provider = self.index
        keys = list(offset_provider.keys())
        if start is not None:
            if isinstance(start, basestring):
                try:
                    start = keys.index(start)
                except ValueError:
                    start = keys.index(start + '.')
            elif isinstance(start, int):
                start = start
            else:
                raise TypeError("Cannot start from object %r" % start)
        else:
            start = 0
        for key in keys[start:]:
            yield scan_source.get_by_id(key)

    def start_from_scan(self, scan_id=None, rt=None, index=None, require_ms1=True, grouped=True):
        """
        Reconstruct an iterator which will start from the scan matching one of ``scan_id``,
        ``rt``, or ``index``. Only one may be provided.

        After invoking this method, the iterator this object wraps will be changed to begin
        yielding scan bunchs (or single scans if ``grouped`` is ``False``).

        This method will trigger several random-access operations, making it prohibitively
        expensive for normally compressed files.

        Arguments
        ---------
        scan_id: str, optional
            Start from the scan with the specified id.
        rt: float, optional
            Start from the scan nearest to specified time (in minutes) in the run. If no
            exact match is found, the nearest scan time will be found, rounded up.
        index: int, optional
            Start from the scan with the specified index.
        require_ms1: bool, optional
            Whether the iterator must start from an MS1 scan. True by default.
        grouped: bool, optional
            whether the iterator should yield scan bunches or single scans. True by default.
        """
        if scan_id is None:
            if rt is not None:
                scan = self.get_scan_by_time(rt)
            elif index is not None:
                try:
                    scan = self.get_scan_by_index(index)
                except IndexError:
                    if index > len(self.index):
                        index = len(self.index) - 1
                    else:
                        index = 0
                    scan = self.get_scan_by_index(index)

            else:
                raise ValueError("Must provide a scan locator, one of (scan_id, rt, index)")

            scan_id = scan.id
        else:
            scan = self.get_scan_by_id(scan_id)

        # MGF files do not contain MS1 scans
        if require_ms1:
            pass

        iterator = self._yield_from_index(self._source, scan_id)
        self.make_iterator(iterator, grouped=grouped)
        return self

    def _scan_group_iterator(self, iterator=None, mode=None):
        if iterator is None:
            iterator = self._make_default_iterator()

        impl = _FakeGroupedScanIteratorImpl(
            iterator, self._make_scan, self._validate, self._cache_scan)
        return impl

    def next(self):
        return next(self._producer)

    def _validate(self, scan):
        return True


from django.core.management import BaseCommand
from catalog.models import Category, Product

class Command(BaseCommand):

    def handle(self, *args, **options):
        category_list = [
            {'name': 'auto', 'description': 'vehicle'},
            {'name': 'food', 'description': 'essen'}
        ]

        category_objects = []
        for category_item in category_list:
            category_objects.append(
                Category(**category_item))

        Category.objects.bulk_create(category_objects)

        product_list = [
            {'name': 'potato', 'description': 'vegetable', 'category': 'food', 'purchase_price': 50, 'date_of_creation': '2023-05-01', 'last_modified_date': '2023-05-03'},
            {'name': 'strawberry', 'description': 'berry', 'category': 'food', 'purchase_price': 100, 'date_of_creation': '2023-05-01', 'last_modified_date': '2023-05-03'}
        ]

        product_objects = []
        for product_item in product_list:
            product_objects.append(
                Product(**product_item))

        Product.objects.bulk_create(product_objects)


import sys
import pathlib
import moviepy.editor as mpy

#Initialize fade in and out lengths in seconds. Adjust these to preference.
fadein_dur = 2
fadeout_dur = 2

# grab and initialize the argument directories
first_arg = sys.argv[1]
second_arg = sys.argv[2]
third_arg = sys.argv[3]

music_dir = pathlib.Path(first_arg)
photo_dir = pathlib.Path(second_arg)
save_dir = pathlib.Path(third_arg)

# check if save / exists, if not create; throw errors if photo or song dirs don't exist
if save_dir.exists() is False:
    save_dir.mkdir()

if photo_dir.exists() is False:
    raise RuntimeError (f'Error: Photo directory does not exist. Try again.')

if music_dir.exists() is False:
    raise RuntimeError (f'Error: Music directory does not exist. Try again.')

#load up the array of photos to merge
def photo_list():
    for photo_file in photo_dir.iterdir():
        pFilePath = pathlib.Path(photo_file)
        if pFilePath.suffixes == '.jpg' or '.png' or '.gif':
            print(photo_file)
            yield photo_file

def avMerge(song, photo):
    video = mpy.ImageClip(f"{photo}")
    audio = mpy.AudioFileClip(f"{song}")
    video = (video.set_audio(audio)
            .set_duration(audio.duration)
            .fadein(fadein_dur)
            .fadeout(fadeout_dur))
    video.write_videofile(str(pathlib.PurePath(str(save_dir), str(song.stem))) + ".mp4", fps=24)

# set up generator for the photos
gen = photo_list()

# loop through music folder
for music_file in music_dir.iterdir():
    print(music_file)
    mFilePath = pathlib.Path(music_file)
    if mFilePath.suffix == '.mp3':
        avMerge(music_file, next(gen))


from typing import Iterable, Any, Optional

__all__ = ("get",)

def get(iterable: Iterable[Any], **kwargs) -> Optional[Any]:
    """
    Returns the first object that matches the kwargs arguments.
    Used in caching.

    :param Iterable iterable: The iterable.
    :param kwargs: The key arguments.
    :return: The first object that matches the kwargs arguments.
    :rtype: Optional[Any]
    """

    for elem in iterable:
        for key, value in kwargs.items():
            if getattr(elem, key) == value:
                return elem


# run-demo.py
# simple example of gst-switch python api

# pass the path to the binaries:
#

import argparse

import time
import subprocess

from gstswitch.helpers import PreviewSinks
from gstswitch.server import Server
from gstswitch.helpers import TestSources

def wait(secs):
    print("sleeping {} secconds...".format(secs))
    time.sleep(secs)

def main(args):

    print("running server")
    serv = Server(path=args.path)
    serv.run()

    wait(5)

    print("running source pattern=1")
    sources = TestSources(video_port=3000, audio_port=4000)
    sources.new_test_video(pattern=1)

    wait(5)

    print("running gst-switch-ui")
    # the & will run this in the background so control returns
    # and we can bring up the 2nd source

    # Replaced call with subprocess.Popen.
    SwitchUi = subprocess.Popen("gst-switch-ui &",shell=True)

    wait(5)

    print("running source pattern=18")
    sources.new_test_video(pattern=18)

    raw_input("hit enter:")

    # Replaced pkill call with Popen.kill
    subprocess.Popen.kill(SwitchUi)
    serv.kill()

def pars_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("--path")
    args = parser.parse_args()
    return args

if __name__=='__main__':
    args=pars_args()
    main(args)


from pyrogram import Client, Filters, InlineKeyboardMarkup, InlineKeyboardButton

from ..config import Config
from ..screenshotbot import ScreenShotBot

UPDATES_CHANNEL = 'MediQBank'
from pyrogram.errors.exceptions.bad_request_400 import UserNotParticipant, UsernameNotOccupied, ChatAdminRequired, PeerIdInvalid

@ScreenShotBot.on_message(Filters.private & Filters.command("start"))
async def start(c, m):

    ## Doing Force Sub 🤣
    update_channel = UPDATES_CHANNEL
    if update_channel:
        try:
            user = await c.get_chat_member(update_channel, m.chat.id)
            if user.status == "kicked":
                await c.send_message(
                   chat_id=m.chat.id,
                   text="Sorry Sir, You are Banned to use me. Contact my [Support Group](https://t.me/safothebot).",
                   parse_mode="markdown",
                   disable_web_page_preview=True
                )
                return
        except UserNotParticipant:
            await c.send_message(
                chat_id=m.chat.id,
                text="**Hey, \nPlease Join My Updates Channel To Use This Bot!**",
                reply_markup=InlineKeyboardMarkup(
                    [
                        [
                            InlineKeyboardButton("Join Updates Channel", url=f"https://t.me/{update_channel}")
                        ]
                    ]
                ),
                parse_mode="markdown"
            )
            return
        except Exception:
            await c.send_message(
                chat_id=m.chat.id,
                text="Something went Wrong. Contact my [Support Group](https://t.me/safothebot).",
                parse_mode="markdown",
                disable_web_page_preview=True)
            return
    ##
    if not await c.db.is_user_exist(m.chat.id):
        await c.db.add_user(m.chat.id)
        await c.send_message(
            Config.LOG_CHANNEL,
            f"#PING_SS: \n\nNew User [{m.from_user.first_name}](tg://user?id={m.chat.id}) started!"
        )

    await m.reply_text(
        text=f"Hi, [{m.from_user.first_name}](tg://user?id={m.chat.id})! 😍\n\nI'm S1 Screenshot BOT. I Can Provide Screenshots From Your Video Files Without Downloading The Entire File. For More Details Check /help!",
        parse_mode="markdown",
        quote=True,
        reply_markup=InlineKeyboardMarkup(
            [
                [
                    InlineKeyboardButton('Updates', url='https://t.me/safoneyt'),
                    InlineKeyboardButton('Developer', url='https://t.me/I_Am_Only_One_1')
                ],
                [
                    InlineKeyboardButton('Support Group', url='https://t.me/safothebot')
                ]
            ]
        )
    )


import abc
import argparse
import os
from typing import Any, Union

class BaseOptions(abc.ABC):

    def __init__(self):
        """Instanciate a BaseOptions object by reading and parsing programm parameters
        """
        self.parser = argparse.ArgumentParser()
        self.read_parameters()
        self.gather_options()

    def __getattr__(self, name: str) -> Any:
        return getattr(self.options, name)

    def isnotebook(self) -> bool:
        try:
            shell = get_ipython().__class__.__name__  # Seems to be in the global name space when ipython is running
            if shell == 'ZMQInteractiveShell':
                return True   # Jupyter notebook or qtconsole
            elif shell == 'TerminalInteractiveShell':
                return True  # Terminal running IPython
            else:
                return False  # Other type (?)
        except NameError:
            return False      # Probably standard Python interpreter

    @abc.abstractmethod
    def read_parameters(self) -> None:
        """Abstract method that defines model options.
        """
        # General options
        self.parser.add_argument("--name", type=str, default="model", help="Set model name")
        self.parser.add_argument("--device", type=str, default="cpu", help="Set the device that pytorch will use for the model: (cpu, cuda:0, ...)")
        self.parser.add_argument("--saved_models_dir", type=str, default="./saved_models")

    def gather_options(self, force: bool = False) -> None:
        """Parse args to store them in the object
        """
        if self.isnotebook() or force:
            self.options = self.parser.parse_args("")
        else:
            self.options = self.parser.parse_args()

    def rewrite_option(self, option_name: str, value: Any) -> None:
        if hasattr(self.options, option_name):
            setattr(self.options, option_name, value)
        else:
            raise ValueError(f"Option {option_name} does not exist")

    def save_options(self, dir_path: str) -> None:

        save_path = os.path.join(dir_path, "options.txt")
        if not os.path.exists(dir_path):
            os.makedirs(dir_path)

        with open(save_path, "wt") as save_file:
            save_file.write(self.print_options(return_str=True))

    def load_options(self, path: str) -> None:

        with open(path, "r") as file:
            text = file.readlines()
            for i, line in enumerate(text):
                if i == 0:
                    continue
                argument, value = [word.strip() for word in line.split("---->")]
                if "(" in value:
                    value = eval(value)
                elif "." in value:
                    try:
                        value = float(value)
                    except ValueError:
                        pass
                else:
                    try:
                        value = int(value)
                    except ValueError:
                        pass

                self.rewrite_option(argument.lower(), value)

    def print_options(self, return_str: bool = False) -> Union[None, str]:

        text = "MODEL OPTIONS".center(60, "-") + "\n"
        for atribute, value in self.options.__dict__.items():
            text += f"{atribute.capitalize():<25}---->{str(value):>25}\n"

        if return_str:
            return text

        print(text)


from collections import defaultdict

from odoo import models

class EventRegistration(models.Model):
    _inherit = 'event.registration'

    def _get_lead_grouping(self, rules, rule_to_new_regs):
        """ Override to support sale-order based grouping and update.

        When checking for groups for rules, we search for existing leads linked
        to same group (based on sale_order_id) and rule. Each rule can therefore
        update an existing lead or create a new one, for each sale order that
        makes the group. """
        so_registrations = self.filtered(lambda reg: reg.sale_order_id)
        grouping_res = super(EventRegistration, self - so_registrations)._get_lead_grouping(rules, rule_to_new_regs)

        if so_registrations:
            # find existing leads in batch to put them in cache and avoid multiple search / queries
            related_registrations = self.env['event.registration'].search([
                ('sale_order_id', 'in', so_registrations.sale_order_id.ids)
            ])
            related_leads = self.env['crm.lead'].search([
                ('event_lead_rule_id', 'in', rules.ids),
                ('registration_ids', 'in', related_registrations.ids)
            ])

            for rule in rules:
                rule_new_regs = rule_to_new_regs[rule]

                # for each group (sale_order), find its linked registrations
                so_to_regs = defaultdict(lambda: self.env['event.registration'])
                for registration in rule_new_regs & so_registrations:
                    so_to_regs[registration.sale_order_id] |= registration

                # for each grouped registrations, prepare result with group and existing lead
                so_res = []
                for sale_order, registrations in so_to_regs.items():
                    registrations = registrations.sorted('id')  # as an OR was used, re-ensure order
                    leads = related_leads.filtered(lambda lead: lead.event_lead_rule_id == rule and lead.registration_ids.sale_order_id == sale_order)
                    so_res.append((leads, sale_order, registrations))
                if so_res:
                    grouping_res[rule] = grouping_res.get(rule, list()) + so_res

        return grouping_res


from shapely import Polygon
import trimesh

pts = ((100, 100), (400, 100), (400, 400), (100, 400))
hole = ((150, 150), (350, 150), (350, 350), (150, 350))
p = Polygon(pts, [hole])
mesh = trimesh.creation.extrude_polygon(p, 100)
#mesh = trimesh.primitives.Extrusion(p, height=100)
other = mesh.copy()
other.apply_translation((150, 50, 50))
mesh = mesh.union(other)
#other.visual = trimesh.visual.ColorVisuals(
#    other,
#    [[255, 0, 0] for _ in other.visual.face_colors]
#     )
for i, face in enumerate(mesh.faces):
    x0, y0, z0 = mesh.vertices[face[0]]
    x1, y1, z1 = mesh.vertices[face[1]]
    x2, y2, z2 = mesh.vertices[face[2]]
    if x0 == x1 and y0 != y1:
        mesh.visual.face_colors[i][:] = 255, 0, 0, 255
#mesh.show()
import py5

def setup():
    py5.size(700, 700, py5.P3D)

def draw():
    py5.background(100, 200, 100)
    py5.translate(350, 0)
    py5.rotate_y(py5.radians(py5.mouse_x))
    py5.translate(-350, 0)
    draw_mesh(mesh)

def draw_mesh(m):
    for i, face in enumerate(m.faces):
        r, g, b, a = m.visual.face_colors[i]
        py5.fill(r, g, b, a)
        with py5.begin_closed_shape():
            py5.vertices([m.vertices[v] for v in face])

py5.run_sketch()


from uuid import UUID
from typing import List, Optional

from app.models import ContactPerson, Organization
from app.schemas.contact_person import ContactPersonCreate, ContactPersonUpdate
from fastapi.encoders import jsonable_encoder
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession

class CRUDContactPerson:
    async def get(
        self,
        session: AsyncSession,
        *,
        organization: Organization,
        skip: int = 0,
        limit: int = 100,
    ) -> List[Organization]:
        result = await session.execute(
            select(ContactPerson)
            .offset(skip)
            .limit(limit)
            .where(ContactPerson.organization_id == organization.id)
        )

        return result.scalars().all()

    async def get_by_id(
        self, session: AsyncSession, *, id_: UUID
    ) -> Optional[ContactPerson]:
        result = await session.execute(
            select(ContactPerson).where(ContactPerson.id == id_)
        )

        return result.scalars().first()

    async def get_by_first_second_name_email(
        self,
        session: AsyncSession,
        *,
        first_name: str,
        second_name: str,
        email: str,
    ) -> Optional[Organization]:
        result = await session.execute(
            select(ContactPerson).where(
                and_(
                    ContactPerson.first_name == first_name,
                    ContactPerson.second_name == second_name,
                    ContactPerson.email == email,
                )
            )
        )

        return result.scalars().first()

    async def create(
        self,
        session: AsyncSession,
        *,
        contact_person_in: ContactPersonCreate,
        organization: Organization,
    ) -> ContactPerson:

        organization = ContactPerson(
            **contact_person_in.dict(exclude=["organization_name"]),
            organization_id=organization.id,
        )
        session.add(organization)

        await session.commit()
        await session.refresh(organization)

        return organization

    async def update(
        self,
        session: AsyncSession,
        *,
        contact_person: ContactPerson,
        contact_person_in: ContactPersonUpdate,
        organization: Optional[Organization] = None,
    ) -> ContactPerson:

        contact_person_data = jsonable_encoder(contact_person)
        update_data = contact_person_in.dict(skip_defaults=True)

        if organization:
            contact_person.organization_id = organization.id

        for field in contact_person_data:
            if field == "organization_name":
                continue

            if field in update_data:
                setattr(contact_person, field, update_data[field])

        session.add(contact_person)
        await session.commit()
        await session.refresh(contact_person)

        return contact_person

    async def delete(
        self,
        session: AsyncSession,
        *,
        contact_person: ContactPerson,
    ) -> None:
        session.delete(contact_person)
        await session.commit()

crud_contact_person = CRUDContactPerson()


from torch.optim import lr_scheduler
import torch.nn.init as init
import math

def get_scheduler(optimizer, hyperparameters, iterations=-1):
    if 'lr_policy' not in hyperparameters or hyperparameters['lr_policy'] == 'constant':
        scheduler = None # constant scheduler
    elif hyperparameters['lr_policy'] == 'step':
        scheduler = lr_scheduler.StepLR(optimizer, step_size=hyperparameters['step_size'],
                                        gamma=hyperparameters['gamma'], last_epoch=iterations)
    else:
        return NotImplementedError('learning rate policy [%s] is not implemented', hyperparameters['lr_policy'])
    return scheduler

def weights_init(init_type='gaussian'):
    def init_fun(m):
        classname = m.__class__.__name__
        if (classname.find('Conv') == 0 or classname.find('Linear') == 0) and hasattr(m, 'weight'):
            # print m.__class__.__name__
            if init_type == 'gaussian':
                init.normal_(m.weight.data, 0.0, 0.02)
            elif init_type == 'xavier':
                init.xavier_normal_(m.weight.data, gain=math.sqrt(2))
            elif init_type == 'kaiming':
                init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
            elif init_type == 'orthogonal':
                init.orthogonal_(m.weight.data, gain=math.sqrt(2))
            elif init_type == 'default':
                pass
            else:
                assert 0, "Unsupported initialization: {}".format(init_type)
            if hasattr(m, 'bias') and m.bias is not None:
                init.constant_(m.bias.data, 0.0)

    return init_fun


import pygame, json

from catan import NUMBER_FONT, BLACK

import pygame
import json

class Timer:
    """
    A Timer class to keep track of time and display it on a screen.

    Args:
        screen (Surface): The surface to render the time on.

    Attributes:
        screen (Surface): The surface to render the time on.
        time (int): The total time in milliseconds for the timer.
        paused (bool): A flag to indicate if the timer is currently paused.
        remaining_time (int): The remaining time in milliseconds on the timer.

    Methods:
        update(dt):
            Update the remaining time on the timer based on elapsed time.

            Args:
                dt (float): The elapsed time in milliseconds.

        pause():
            Pause the timer.

        unpause():
            Unpause the timer.

        save():
            Save the current state of the timer to a file.

        load():
            Load the saved state of the timer from a file.
    """
    def __init__(self, screen):
        """
        Initialize the Timer class.

        Args:
            screen (Surface): The surface to render the time on.
        """
        self.screen = screen
        self.time = 15 * 60 * 1000  # 15 minutes in milliseconds
        self.paused = False
        self.remaining_time = self.time

    def update(self, dt):
        """
        Update the remaining time on the timer based on elapsed time.

        Args:
            dt (float): The elapsed time in milliseconds.
        """
        if not self.paused:
            self.remaining_time -= dt
            if self.remaining_time <= 0:
                self.remaining_time = 0
                self.paused = True

        # Convert milliseconds to minutes, seconds, and milliseconds
        minutes = int(self.remaining_time / 60000)
        seconds = int((self.remaining_time % 60000) / 1000)
        milliseconds = int(self.remaining_time % 1000)

        # Format the time as a string
        time_str = "{:02d}:{:02d}.{:03d}".format(minutes, seconds, milliseconds)

        # Render the time as text
        text = NUMBER_FONT.render(time_str, True, BLACK)

        # Display the time on the screen
        self.screen.blit(text, (1550, 250))

    def pause(self):
        """
        Pause the timer.
        """
        self.paused = True

    def unpause(self):
        """
        Unpause the timer.
        """
        self.paused = False

    def save(self):
        """
        Save the current state of the timer to a file.
        """
        data = {"time": self.time, "paused": self.paused, "remaining_time": self.remaining_time}
        with open("timer_data.txt", "w") as f:
            json.dump(data, f)

    def load(self):
        """
        Load the saved state of the timer from a file.
        """
        with open("timer_data.txt", "r") as f:
            data = json.load(f)
        self.time = data["time"]
        self.paused = data["paused"]
        self.remaining_time = data["remaining_time"]


"""Файл содержит класс DataBaseInterface который объединяет методы для работы с базой данных"""

import json
import sqlite3
from datetime import datetime

from const import DB_NAME

class DataBaseInterface:
    """
    Класс для работы с базой данных.
    """

    def __init__(self):
        """
        1. Создаётся связь с бд sqlite3
        2. Вызывается функция для создания таблиц.
        """
        self.connection = sqlite3.connect(DB_NAME)
        self.init_tables()

    def init_tables(self):
        """
        Создаются таблицы в БД.

        1. С помощью контекстного менеджера открывается соединение с бд;
        2. Создаётся объект курсора, с помощью которого будут передаваться команды;
        3. Передаются две команды для создания таблиц, если они не созданы;
        4. С помощью метода self.connection.commit(), команды выполняются.
        """
        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS template
                (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    variables JSON NOT NULL,
                    date_added DATETIME NOT NULL
                )
                """
                )
            cursor.execute(
                """
                CREATE TABLE IF NOT EXISTS history
                (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    template_id INTEGER NOT NULL,
                    used_variables JSON NOT NULL,
                    use_date DATETIME NOT NULL,
                    FOREIGN KEY (template_id) REFERENCES template (id)
                )
                """
            )
            self.connection.commit()

    def save_templates(self, templates):
        """
        Сохраняет данные о шаблонах в таблицу template.

        :param templates: Словарь в котором ключом является название шаблона из папки шаблонов,
        а значением является список его переменных.
            Пример:
                {
                    "Заявление на отпуск": ["фио", "дни", "дата"],
                    "Командировка": ["фио", "должность", "город", "дата"]
                }

        """
        with self.connection:
            cursor = self.connection.cursor()
            date_added = datetime.now()
            for template_filename, variables in templates.items():
                json_variables = json.dumps(variables)
                if not self.template_exist(template_filename, json_variables):
                    cursor.execute(
                        "INSERT INTO template (name, variables, date_added)"
                        "VALUES (?, ?, ?)",
                        (template_filename, json_variables, date_added)
                    )
            self.connection.commit()

    def template_exist(self, name, variables):
        """
        Производит поиск шаблона в таблице template.

        :param name: Имя шаблона.
        :param variables: Переменные шаблона.
        :return: True, если шаблон найден, иначе False.
        """
        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                SELECT variables FROM template
                WHERE name=(?)
                """,
                (name,)
            )
            result = cursor.fetchall()
            for row in result:
                if row[0] == variables:
                    return True
            return False

    def save_history(self, template_id, used_variables, date_added):
        """
        Сохраняет в таблицу history, переменные которые использовал пользователь при генерации нового документа.

        :param template_id: Номер шаблона из таблицы template.
        :param used_variables: Список используемых переменных.
        :param date_added: Дата создания документа.
        """

        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                INSERT INTO history (template_id, used_variables, use_date)
                VALUES (?, ?, ?)
                """,
                (template_id, json.dumps(used_variables), date_added)
            )
            self.connection.commit()

    def read_templates(self):
        """
        Запрашивает все данные из таблицы template.

        :return: Список кортежей с данными из таблицы.
        """
        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute("SELECT * FROM template")
            rows = cursor.fetchall()
        return rows

    def read_template_by_id(self, template_id):
        """
        Запрашивает данные о шаблоне по его id.

        :param template_id: Номер шаблона в таблице template.
        :return: Кортеж с данными о шаблоне.
        """
        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                SELECT * FROM template
                WHERE id = (?)
                """,
                (template_id,)
            )
            template = cursor.fetchone()
        return template

    def read_template_history(self, template_id):
        """
        Запрашивает строки из таблицы history с заданным template_id

        :param template_id: Номер шаблона в таблице template.
        :return: Список кортежей с данными об истории использования шаблона.
        """

        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                SELECT * FROM history
                WHERE template_id = (?)
                """,
                (template_id,)
            )
            history = cursor.fetchall()
        return history

    def get_template_id(self, template_name):
        """
        Запрашивает последний существующий номер шаблона с определённым именем.

        :param template_name: Имя шаблона.
        :return: Номер шаблона.
        """

        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(
                """
                SELECT id FROM template
                WHERE name = (?)
                ORDER BY id DESC
                """,
                (template_name,)
            )
            template_id = cursor.fetchone()
        return template_id[0]