#!/usr/bin/env python3

"""Work Log
Record work activities and store to a sqlite database

Created: 2018
Last Update: 2018-06-05
Author: Alex Koumparos
"""
import datetime
import re

# from csv_manager import CsvManager
from db_manager import DBManager
import wl_settings as settings

class Menu:
    """The user-facing class that handles all interaction with the user and
    interfaces with the database manager.
    """
    # STATUS VARIABLES
    # ----------------
    quit = False

    # INITIALIZER
    # -----------
    def __init__(self, load_menu=True):
        """Instantiates the app, applies default settings and launches the
        main menu.
        """
        print("\nWORK LOG")
        print("========")
        self.OPTIONS = {
            'date format': settings.DATE_FORMATS['iso 8601'],
            'save format (date)': settings.DATE_FORMATS['iso 8601'],
            'case sensitive search': False,
            'entries per page': 10,
            'allow future dates': False,
            'earliest allowed date': datetime.datetime(1900, 1, 1),
        }
        self.current_record = 0
        self.current_page_start = 0
        if load_menu:
            menu = self.main_menu()
            while not self.quit:
                menu = menu()

    # MENU METHODS
    # ------------
    def main_menu(self):
        """This is the root menu. The user selects which activity to perform
        and then the method returns the function for the activity.
        """
        inputs = {
            'a': {'text': 'Add new entry',
                  'function': self.add_entry},
            's': {'text': 'Search in existing entries',
                  'function': self.search_entries},
            'o': {'text': 'Options',
                  'function': self.options},
            'q': {'text': 'Quit program',
                  'function': self.quit_program}
        }
        while True:
            print("\nMAIN MENU")
            print("What would you like to do?")
            for key, value in inputs.items():
                print("{}) {}".format(key, value['text']))
            user_entry = input("> ").lower()
            print(user_entry)
            if user_entry not in inputs.keys():
                continue
            return inputs[user_entry]['function']

    def add_entry(self):
        """This is the menu where the user can add a task that was completed
        """
        while True:
            print("\nADD ENTRY")
            print("Username")
            input_text = input("Enter your name > ")
            username = input_text
            date = None
            while date is None:
                print("Date of the Task")
                user_entry = self.date_entry()
                if user_entry[0] is not None:  # error
                    print(user_entry[0])
                    continue
                else:
                    date = user_entry[1]
                    date_string = self.date_to_string(date, target='file')
            print("Name of the Task")
            input_text = input("Enter the name of the task > ")
            task_name = input_text
            time_spent = None
            while time_spent is None:
                print("Time spent")
                print("Enter a whole number of minutes (rounded)")
                input_text = input("> ")
                try:
                    time_spent = int(input_text)
                except ValueError:
                    print("Invalid value, please try again")
                    continue
                if time_spent < 0:
                    print("Invalid value, please try again")
                    continue
            print("Notes")
            input_text = input("(Optional, leave blank for none) ")
            notes = input_text
            # call method to write data to file
            dbm = DBManager()
            file_data = {
                settings.HEADERS['user']: username,
                settings.HEADERS['date']: date_string,
                settings.HEADERS['task_name']: task_name,
                settings.HEADERS['duration']: time_spent,
                settings.HEADERS['notes']: notes
            }
            dbm.add_entry(file_data)
            return self.main_menu

    def options(self):
        """This is the menu where the user can specify user-configurable
        options
        """
        print('OPTIONS')

        print("Choose a display date format")
        menu_choices = list(settings.DATE_FORMATS.keys())
        menu_size = len(menu_choices)
        for i in range(len(menu_choices)):
            print("({}) - {}".format(i + 1, menu_choices[i]))
        input_text = input("> ")
        if input_text in [str(x) for x in range(1, menu_size + 1)]:
            choice = int(input_text) - 1
            choice = menu_choices[choice]
            print("You chose: {}".format(choice))
            self.OPTIONS['date format'] = settings.DATE_FORMATS[choice]
            print('going back to main menu')
        else:
            print("Invalid entry, returning to main menu")

        return self.main_menu

    def search_entries(self):
        """This is the search menu. The user selects how they want to search.
        """
        inputs = {
            'l': {'text': 'employee names List',
                  'function': self.search_employee},
            'e': {'text': 'Employee name Search',
                  'function': self.search_employee_text},
            'd': {'text': 'single Date',
                  'function': self.search_exact_date},
            'r': {'text': 'date Range',
                  'function': self.search_date_range},
            't': {'text': 'Time spent',
                  'function': self.search_time_spent},
            's': {'text': 'text Search',
                  'function': self.search_text_search},
            'b': {'text': 'Back to main menu',
                  'function': self.main_menu}
        }
        while True:
            print("\nSEARCH ENTRIES")
            print("How would you like to search?")
            for key, value in inputs.items():
                print("{}) {}".format(key, value['text']))
            user_entry = input("> ").lower()

            if user_entry not in inputs.keys():
                continue
            return inputs[user_entry]['function']

    def quit_program(self):
        print("Quitting")
        self.quit = True

    def present_results(self):
        """Show all the results from the search and then provide interaction
        choices
        """
        inputs = {
            'n': {'text': 'Next page',
                  'function': self.next_page},
            'p': {'text': 'Previous page',
                  'function': self.previous_page},
            'v': {'text': 'View detail',
                  'function': self.select_detail},
            'e': {'text': 'Edit',
                  'function': self.edit_record},
            'd': {'text': 'Delete',
                  'function': self.delete_record},
            'm': {'text': 'go back to Main menu',
                  'function': self.main_menu},
            'q': {'text': 'quit',
                  'function': self.quit_program},
        }
        if self.current_page_start == 0:
            del(inputs['p'])
        next_start = self.current_page_start + self.OPTIONS['entries per page']
        if next_start >= len(self.records):
            del(inputs['n'])
        print("\nSearch Results")
        if len(self.records) > next_start:
            current_page_end = next_start
        else:
            current_page_end = len(self.records) - 1
        for index in range(self.current_page_start, current_page_end + 1):
            value = self.records[index]
            short_form = self.display_entry(value, return_only=True)
            print("{}) {}".format(index + 1, short_form))

        print("\nAvailable actions:")
        for key, value in inputs.items():
            print('{}) {}'.format(key, value['text']))

        while True:
            user_entry = input("> ").lower()

            if user_entry not in inputs.keys():
                continue
            return inputs[user_entry]['function']

    def present_next_result(self):
        """Show the next available result"""
        inputs = {
            'p': {'text': 'Previous',
                  'function': self.previous_result},
            'n': {'text': 'Next',
                  'function': self.next_result},
            'b': {'text': 'Back to list view',
                  'function': self.present_results},
            'e': {'text': 'Edit',
                  'function': self.edit_current_record},
            'd': {'text': 'Delete',
                  'function': self.delete_current_record},
            'm': {'text': 'go back to Main menu',
                  'function': self.main_menu},
            'q': {'text': 'quit',
                  'function': self.quit_program},
        }
        if self.current_record == 0:
            del(inputs['p'])
        if self.current_record == len(self.records) - 1:
            del(inputs['n'])
        record = self.records[self.current_record]
        self.display_entry(record, verbose=True)

        print("\nAvailable actions:")
        for key, value in inputs.items():
            print('{}) {}'.format(key, value['text']))

        while True:
            user_entry = input("> ").lower()

            if user_entry not in inputs.keys():
                continue
            return inputs[user_entry]['function']

    # Specific Search Menus
    # .....................
    def search_employee(self):
        """This is the menu where the user is given a list of all employees
        who have entries, and can select a particular employee to see
        all their entries
        """
        print("\nSEARCH BY EMPLOYEE")
        # load the db manager
        dbm = DBManager()
        employee_names = dbm.view_employees()
        for i, value in enumerate(employee_names):
            print("{}) {}".format(i + 1, value['name']))
        selected_employee = None
        while selected_employee is None:
            user_input = input("> ")
            # perform input validation
            try:
                user_input = int(user_input) - 1
            except ValueError:
                print("Invalid value, try again")
                continue
            if user_input < 0:
                print("Value out of range. Try again.")
                continue
            try:
                selected_employee = employee_names[user_input]['name']
            except IndexError:
                print("Value out of range. Try again.")
                continue
            # when an employee is selected, show all the entries with that e'ee
            matching_records = dbm.view_everything(employee=selected_employee)
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    def search_employee_text(self):
        """This is the menu where the user enters a text string and is presented
        with all employee names containing that string
        """
        print('FIND EMPLOYEE NAME USING TEXT STRING')
        print("Enter the text string to search on")
        input_text = input("> ")
        text_string = input_text
        # load db
        dbm = DBManager()
        employee_names = dbm.view_names_with_text(text_string)
        for i, value in enumerate(employee_names):
            print("{}) {}".format(i + 1, value['name']))
        selected_employee = None
        while selected_employee is None:
            user_input = input("> ")
            # perform input validation
            try:
                user_input = int(user_input) - 1
            except ValueError:
                print("Invalid value, try again")
                continue
            if user_input < 0:
                print("Value out of range. Try again.")
                continue
            try:
                selected_employee = employee_names[user_input]['name']
            except IndexError:
                print("Value out of range. Try again.")
                continue
            # when an employee is selected, show all the entries with that e'ee
            matching_records = dbm.view_everything(employee=selected_employee)
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    def search_exact_date(self):
        """This is the menu where the user browses dates and entries and picks
        the date from a list
        """
        print("\nSEARCH EXACT DATE")
        # load the db manager
        dbm = DBManager()
        date_records = dbm.view_dates()
        for i, value in enumerate(date_records):
            value = self.date_to_string(value['date'])
            print("{}) {}".format(i + 1, value))
        selected_date = None
        while selected_date is None:
            user_input = input("> ")
            # perform input validation
            try:
                user_input = int(user_input) - 1
            except ValueError:
                print("Invalid value, try again")
                continue
            if user_input < 0:
                print("Value out of range. Try again.")
                continue
            try:
                selected_date = date_records[user_input]['date']
            except IndexError:
                print("Value out of range. Try again.")
                continue

            # when a date is selected, show all the entries with that date
            matching_records = dbm.view_entries_for_date(selected_date)
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    def search_date_range(self):
        """This is the menu where the user can enter a from date and to date
        and get back every entry from within that range
        """
        print('SEARCH DATE RANGE')
        start_date = None
        end_date = None
        # get start_date
        while start_date is None:
            print("Start Date:")
            user_entry = self.date_entry()
            if user_entry[0] is not None:  # error
                print(user_entry[0])
                continue
            else:
                start_date = user_entry[1]
        # get end_date
        while end_date is None:
            print("End Date:")
            user_entry = self.date_entry()
            if user_entry[0] is not None:  # error
                print(user_entry[0])
                continue
            else:
                end_date = user_entry[1]
        # load db
        dbm = DBManager()
        # switch start and end dates if end < start
        if end_date < start_date:
            current_date = end_date
            end_date = start_date
            start_date = end_date
        else:
            current_date = start_date
        # get all records in date range
        matching_records = dbm.view_entries_for_date_range(start_date,
                                                           end_date)

        print("\nShowing entries:")
        if len(matching_records) == 0:
            print("\nNo matches, returning to search menu")
            return self.search_entries
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    def search_time_spent(self):
        """This is the menu where the user enters the number of minutes a task
        took and be able to choose one to see entries from
        """
        print('SEARCH BY TIME SPENT')
        print("Time spent")
        time_spent = None
        while time_spent is None:
            input_text = input("Enter a whole number of minutes (rounded) ")
            try:
                time_spent = int(input_text)
            except ValueError:
                print("Invalid value")
                continue
        # load db
        dbm = DBManager()
        matching_records = dbm.view_entries_for_duration(time_spent)
        if len(matching_records) == 0:
            print("\nNo matches, returning to search menu")
            return self.search_entries
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    def search_text_search(self):
        """This is the menu where the user enters a text string and is presented
        with all entries containing that string in the task name or notes
        """
        print('SEARCH USING TEXT STRING')
        print("Enter the text string to search on")
        input_text = input("> ")
        text_string = input_text
        # load db
        dbm = DBManager()
        matching_records = dbm.view_entries_with_text(text_string)
        if len(matching_records) == 0:
            print("\nNo matches, returning to search menu")
            return self.search_entries
        self.records = matching_records
        self.current_record = 0
        return self.present_next_result

    # Modification Methods
    # --------------------
    def edit_record(self):
        print("edit record")
        print('enter the record number to edit')
        user_input = input("> ")
        match_index = int(user_input) - 1
        record = self.records[match_index]
        # get the new values for the record
        print("New Username")
        input_text = input("Enter the username > ")
        username = input_text
        date = None
        while date is None:
            print("New date of the Task")
            user_entry = self.date_entry()
            if user_entry[0] is not None:  # error
                print(user_entry[0])
                continue
            else:
                date = user_entry[1]
                date_string = self.date_to_string(date, target='file')
        print("New name of the Task")
        input_text = input("Enter the name of the task > ")
        task_name = input_text
        time_spent = None
        while time_spent is None:
            print("New time spent")
            input_text = input("Enter a whole number of minutes (rounded) ")
            try:
                time_spent = int(input_text)
            except ValueError:
                print("Invalid value")
                continue
        print("New notes")
        input_text = input("(Optional, leave blank for none) ")
        notes = input_text
        # load the db
        dbm = DBManager()
        # old_entry = dbm.view_entries
        new_values = {
            'name': username,
            'date': date_string,
            'task_name': task_name,
            'duration': time_spent,
            'notes': notes
        }
        dbm.edit_entry(record, new_values)
        return self.main_menu

    def edit_current_record(self):
        print("edit record")
        match_index = self.current_record
        record = self.records[match_index]
        # get the new values for the record
        print("New Username")
        input_text = input("Enter the username > ")
        username = input_text
        date = None
        while date is None:
            print("New date of the Task")
            user_entry = self.date_entry()
            if user_entry[0] is not None:  # error
                print(user_entry[0])
                continue
            else:
                date = user_entry[1]
        print("New name of the Task")
        input_text = input("Enter the name of the task > ")
        task_name = input_text
        time_spent = None
        while time_spent is None:
            print("New time spent")
            input_text = input("Enter a whole number of minutes (rounded) ")
            try:
                time_spent = int(input_text)
            except ValueError:
                print("Invalid value")
                continue
        print("New notes")
        input_text = input("(Optional, leave blank for none) ")
        notes = input_text
        # load the db
        dbm = DBManager()
        new_values = {
            'name': username,
            'date': date,
            'task_name': task_name,
            'duration': time_spent,
            'notes': notes
        }
        dbm.edit_entry(record, new_values)
        return self.main_menu

    def select_detail(self):
        print("View record")
        print('enter the record number to view')
        user_input = input("> ")
        match_index = int(user_input) - 1
        self.current_record = match_index
        return self.present_next_result

    def delete_record(self):
        print("delete record")
        print('enter the record number to delete')
        user_input = input("> ")
        match_index = int(user_input) - 1
        record = self.records[match_index]
        print(record)
        # load db
        dbm = DBManager()
        dbm.delete_entry(record)
        print("Entry deleted")
        return self.main_menu

    def delete_current_record(self):
        print("delete record")
        match_index = self.current_record
        record = self.records[match_index]
        # load db
        dbm = DBManager()
        dbm.delete_entry(record)
        print("Entry deleted")
        return self.main_menu

    # Other UI Methods
    # ----------------
    def display_entry(self, entry, verbose=False, return_only=False):
        """This method displays a selected entry, showing:
        - date (read from file in iso 8601 and displayed in whatever is set in
          options)
        - task name
        - time taken
        - any notes
        """
        username = entry[settings.HEADERS['user']]
        date_object = entry[settings.HEADERS['date']]
        date = self.date_to_string(date_object, target="display")
        task_name = entry[settings.HEADERS['task_name']]
        time_taken = entry[settings.HEADERS['duration']]
        notes = entry[settings.HEADERS['notes']]
        if verbose:
            line0 = username
            print(line0)
            line1 = "{}: {}".format(date, task_name)
            print(line1)
            print("-" * len(line1))
            print("{} minutes".format(time_taken))
            print("{}".format(notes))
        else:
            short_form = "{}: {} ({}m): {} | {}".format(username,
                                                        date,
                                                        time_taken,
                                                        task_name,
                                                        notes)
            if return_only:
                return short_form
            else:
                print(short_form)

    def previous_result(self):
        """load previous result"""
        self.current_record -= 1
        return self.present_next_result

    def next_result(self):
        """load next result"""
        self.current_record += 1
        return self.present_next_result

    def previous_page(self):
        """load previous page of results"""
        self.current_page_start -= self.OPTIONS['entries per page']
        return self.present_results

    def next_page(self):
        """load next page of results"""
        self.current_page_start += self.OPTIONS['entries per page']
        return self.present_results

    # Helper Methods
    # --------------
    def validate_date_entry(self, date_string, date_format):
        """Takes a date_string and date_format and attempts to create
        a valid datetime object with those imports.
        Returns a tuple in the form (error, datetime) where:
        - `error` is None if valid and a description of the error text if
          invalid;
        - `datetime` is a datetime object if valid and None if invalid
        """
        try:
            naive_datetime = datetime.datetime.strptime(
                date_string,
                date_format['datetime format']
            )
        except ValueError:
            error_text = "{date_string} is not valid in format {date_format}"
            error_args = {"date_string": date_string,
                          "date_format": date_format['UI format']}
            return (error_text.format(**error_args), None)
        else:
            if not self.OPTIONS['allow future dates']:
                if naive_datetime > datetime.datetime.now():
                    error_text = "dates in the future are not permitted"
                    error_args = {"date_string": date_string,
                                  "date_format": date_format['UI format']}
                    return (error_text.format(**error_args), None)
                if naive_datetime < self.OPTIONS['earliest allowed date']:
                    bad_date = self.OPTIONS['earliest allowed date'].strftime(
                        self.OPTIONS['date format']['datetime format']
                    )
                    error_text = "dates before {} are not permitted".format(
                        bad_date
                    )
                    error_args = {"date_string": date_string,
                                  "date_format": date_format['UI format']}
                    return (error_text.format(**error_args), None)
            return (None, naive_datetime)

    def date_entry(self):
        """This helper function asks for a date input in the user's preferred
        format and then returns that date as a naive datetime object
        """
        date_format = self.OPTIONS['date format']
        input_text = "Please use the '{}' date format: "
        user_entry = input(input_text.format(date_format['UI format']))
        # validate date entry
        validated = self.validate_date_entry(user_entry, date_format)
        return validated

    def date_to_string(self, date_object, target='display'):
        """This helper function takes a naive date object and returns a
        string representation in:
        - `target='display'`: the user's preferred display format
        - `target='file': the save format
        """
        if target == 'display':
            option = self.OPTIONS['date format']
            string_format = option['datetime format']
        else:  # 'file' or unrecognised target, fallback to write mode
            option = self.OPTIONS['save format (date)']
            string_format = option['datetime format']
        return date_object.strftime(string_format)

# ---------------------------

if __name__ == "__main__":

    menu = Menu()


import traceback
from types import MethodType
#
# class MyClass(object):
#     pass
#
# def set_name(self,name):
#     self.name=name
#
# cls = MyClass()
# cls.name="kevin"
# print(cls.name)
#
# cls.set_name = MethodType(set_name,cls)
# cls.set_name("lara")
# print(cls.name)

#第二部分：可以看到上面的类可以被随便添加方法和属性，那么怎么实现只能添加指定的属性和方法？用魔术__slot__
class MyClass2(object):
    __slots__ = ['name1','set_name1']

def set_name(self,name):
    self.name=name

# cls = MyClass2()
# cls.name="kevin"
# print(cls.name)
#
# try:
#     cls.set_name = MethodType(set_name,cls)  #报错 AttributeError
# except AttributeError:
#     traceback.print_exc()

#第三部分，如果继承了第二部分的类，那么slots就不起作用
class ExtMyClass(MyClass2):
    pass

# ext_cls = ExtMyClass()
# ext_cls.age=20
#
# print(ext_cls.age)

#第四部分：使用@property实现get 和 set方法
class Student:
    @property
    def score(self):
        return self._score

    @score.setter
    def score(self,value):
        if not isinstance(value,int):
            raise  ValueError("not int")
        elif (value<0) or (value>100):
            raise ValueError("not between 0 and 100")
        self._score = value

    #只读属性，就是不要设置setter 就行
    @property
    def double_socre(self):
        return self._score*2

# s = Student()
# s.score=100
# print(s.score) # 100
# print(s.double_socre) # 200

#第五部分：用描述器模拟生成property功能(不想学，下次再说)

#第六部分 类的默认行为与定制
class defaultAction:
    def __init__(self,name):
        self.name = name

    def __str__(self):
        return "hello "+self.name

# t = defaultAction("bitch")  #如果没有__str__重写，就是标准的输出  否则就是自定义的hello bitch
# print(t)

#把类做成迭代器实现 斐波拉契
class Fib100:
    def __init__(self):
        self._1,self._2 = 0,1

    def __iter__(self):
        return self

    def __next__(self):
        self._1,self._2 =self._2,self._2+self._1
        if self._1 > 100:
            raise  StopIteration
        return self._1

# for i in Fib100():
#     print(i)

# 实现下标访问 需要重写__getitem__选项
class Fib2:
    def __getitem__(self, n):
        a,b = 1,1
        for i in range(n):
            a,b = b, a+b
        return a
# f = Fib2()
# # print(f[1])
# # print(f[5])
# # print(f[10])

#第七部分  枚举
from enum import Enum

Month = Enum("Month",('Jan','Feb','Mar','Apr'))
# print(Month) #<enum 'Month'>
# print(Month.__members__.items()) ('Jan','Feb','Mar','Apr')
# for name , member in  Month.__members__.items():
#     print(name,"=>",member,',',member.value)
# print(Month.Jan) # Month.Jan

#第八部分 元类 元编程
def init(self,name):
    self.name = name

def say_hello(self):
    print('Hello! %s!' % self.name)

Hello = type('Hello',(object,),dict(__init__ = init,hello=say_hello)) #这里等同于创建了一个类，然后有初始化方法 和 hello方法
# h = Hello("name")
# h.hello() #Hello! name!

#第九部分 元类（控制类的创建）
class ListMetaClass(type):  #元类一定要继承自type
    def __new__(cls,name,bases,attrs):
        # print(cls)
        # print(name)
        # print(bases)
        # print(type(attrs))
        attrs['add'] = lambda self,value: self.append(value)
        return type.__new__(cls,name,bases,attrs)

    #新建一个类指定继承自list，然后指定元类是ListMetaClass， 在上面元类中
    #通过__new__新建一个一个add方法，方法体通过lambda方式实现了
class  MyList(list,metaclass=ListMetaClass):
    pass

# mli = MyList()
# mli.add(1)
# mli.add(2)
# mli.add(3)
# print(mli)

#第十部分 ORM框架实例实现
    #背景：假如我们要自己写model,怎么保证save方法能获取到继承类User的id，name，然后保存呢？
# class User(Model): #都是伪代码
#     id = IntegerField('id')
#     name = StringField('name')
#
# u = User()
# u.id =100
# u.name = 'tome'
# u.save()

    #用元类解决上面的问题
class Field:
    def __init__(self, name, col_type):
        self.name = name
        self.col_type = col_type

class IntegerField(Field):
    def __init__(self, name):
        super(IntegerField, self).__init__(name, 'integer')

class StringField(Field):
    def __init__(self, name):
        super(StringField, self).__init__(name, 'varchar(1024)')

class ModelMetaclass(type):
    def __new__(cls, name, bases, attrs):
        if name == 'Model':
            return type.__new__(cls, name, bases, attrs)
        print('Model name: %s' % name)
        mappings = {}
        for k, v in attrs.items():
            if isinstance(v, Field):
                print('Field name: %s' % k)
                mappings[k] = v
        for k in mappings.keys():
            attrs.pop(k)
        attrs['__mappings__'] = mappings
        attrs['__table__'] = name
        return type.__new__(cls, name, bases, attrs)

class Model(dict, metaclass = ModelMetaclass):
    def __init__(self, **kvs):
        super(Model, self).__init__(**kvs)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError("'Model' object has no attribute '%s'." % key)

    def __setattr__(self, key, value):
        print('__setattr__')
        self[key] = value

    def save(self):
        fields = []
        params = []
        args = []
        for k, v in self.__mappings__.items():
            fields.append(v.name)
            params.append('?')
            args.append(getattr(self, k, None))
        sql = 'insert into %s(%s) values(%s)' % (self.__table__, ','.join(fields), ','.join(params))
        print('sql:', sql)
        print('args:', args)

class User(Model):
    id = IntegerField('id')
    name = StringField('name')

# u = User(id = 100, name = 'Tom')
# u = User()
# u.id = 100
# u.name = 'Tom'
# u.save()

#第十一部分：测试
import unittest
class MyDict(dict):
    pass

class TestMyDict(unittest.TestCase):
    def setUp(self):
        print("测试前")

    def tearDown(self):
        print("测试后清理")
    def test_init(self):
        md = MyDict(one = 1,two=2)
        self.assertEqual(md['one'],1)
        self.assertEqual(md['two'],2)

# if __name__ == '__main__':
#     unittest.main()

#第十一部分：日志
import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s", datefmt="%d-%M-%Y %H:%M:%S", level=logging.DEBUG)
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')


import re

def safe_code1(equation):

    for i in range(0,10):
        if str(i) in equation:
            continue
        eq = equation.replace('#',str(i))

        left_expression = eq.split("=")[0]
        right_expression = eq.split("=")[1]
        try:
            if eval(left_expression) == eval(right_expression):
                all_elements = [i for i in re.split("(\D)", eq) if i]
                if any(i[0] == '0' for i in all_elements):
                    continue
                return i
        except Exception as err:
            pass

    return -1

def safe_code(equation):

    for i in range(0,10):
        if str(i) in equation:
            continue
        eq = equation.replace('#',str(i))
        eq = eq.replace('=','==')

        try:
            if eval(eq):
                all_elements = [i for i in re.split("(\D)", eq) if i]
                if any(i[0] == '0' for i in all_elements):
                    continue
                return i
        except Exception as err:
            pass

    return -1

if __name__ == '__main__':
    print("Example:")
    print(safe_code("-5#*-1=5#"))
    print(safe_code("123*45#=5#088"))
    #These "asserts" using only for self-checking and not necessary for auto-testing
    assert safe_code("-5#*-1=5#") == 0
    assert safe_code("##*##=302#") == 5
    assert safe_code("19--45=5#") == -1
    assert safe_code("##--11=11") == -1
    assert safe_code("#9+3=22") == 1
    assert safe_code("11*#=##") == 2
    assert safe_code("#9+3=12") == -1
    print("Coding complete? Click 'Check' to earn cool rewards!")


"""
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水。
1. i的水量为min(i左右最大高度lmax[i], rmax[i]) - i高度
2. 遍历取lrmax
"""

def js(height):
    if not height:
        return 0

    n = len(height)
    lmax = [0] * n
    for i in range(n):
        if i == 0:
            lmax[i] = height[i]
            continue
        lmax[i] = max(lmax[i-1], height[i])
    print(lmax)

    rmax = [0] * n
    for i in range(n-1, -1, -1):
        if i == n - 1:
            rmax[i] = height[i]
            continue
        rmax[i] = max(rmax[i+1], height[i])
    print(rmax)

    ans = sum([min(lmax[i], rmax[i]) - height[i] for i in range(n) if min(lmax[i], rmax[i]) - height[i] > 0])
    return ans

if __name__ == "__main__":
    print(js([3, 2, 1, 2, 3]))


import pyaudio
import numpy as np
import math
import struct
import simpleaudio as sa
from imutils.video import FPS

#CHUNK = 1024
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
RECORD_SECONDS = 0.03
print(int(RATE / CHUNK * RECORD_SECONDS))

volume = 0.1     # range [0.0, 1.0]
fs = 44100       # sampling rate, Hz, must be integer
duration = 0.3   # in seconds, may be float
f = 750.0        # sine frequency, Hz, may be float

fps = FPS().start()

def rms(data):
    if data != []:
        count = len(data)/2
        format = "%dh"%(count)
        shorts = struct.unpack(format, data)
        sum_squares = 0.0
        for sample in shorts:
            n = sample * (1.0/32768)
            sum_squares += n*n
        return math.sqrt(sum_squares / count)
    return 0

p = pyaudio.PyAudio()
p2 = pyaudio.PyAudio()

stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)

# for paFloat32 sample values must be in range [-1.0, 1.0]
streamout = p2.open(format=pyaudio.paFloat32,
                   channels=1,
                   rate=fs,
                   output=True)

# generate samples, note conversion to float32 array
samples = (np.sin(2*np.pi*np.arange(fs*duration)*f/fs)).astype(np.float32)

wave_obj = sa.WaveObject.from_wave_file("bumbo.wav")
ind = 0
count = 0
while ind != 500:
    frames = []
    energy = []
    for _ in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
        try:
            data = stream.read(CHUNK, exception_on_overflow=False)
        except IOError:
            #data = '\x00' * CHUNK
            data = []
        energy.append(rms(data))
    if np.mean(energy) > 0.01 and count > 10:
        #wave_obj.play()
        streamout.get_write_available()
        #streamout.write(samples * np.mean(energy)/0.2)
        streamout.write(samples * (volume + np.mean(energy) * 5))
        print(np.mean(energy))
        # energy = 0.02 - volume = 0.1
        # energy = 0.2 - volume = 1
        count = 0
    else:
        count = count + 1

    ind = ind + 1
    fps.update()

# stop the timer and display FPS information
fps.stop()
print("[INFO] elapsed time: {:.2f}".format(fps.elapsed()))
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))

#energy = np.mean(energy)
#print("Average energy in an execution of " + str(RECORD_SECONDS*ind) + " Seconds: " + str(energy))
stream.stop_stream()
stream.close()
streamout.stop_stream()
streamout.close()
p.terminate()
p2.terminate()


#!/usr/bin/python3
""" Rotation of a 2D matrix """

def rotate_2d_matrix(matrix):
    """
    Rotate a given matrix

    Args:
        matrix: List of lists
    """
    n = len(matrix)
    # for each row
    for x in range(n):
        reversedRow = []
        # Collect the values from back as a list
        for y in range((n - 1), -1, -1):
            reversedRow.append(matrix[y][x])
        matrix.append(reversedRow)
    # Remove the rows original matrix
    for i in range(n):
        matrix.pop(0)


import argparse
import time
import re
import os
import datetime

import numpy as np

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torch.utils.data import Dataset, DataLoader, random_split
from torch.nn.utils.rnn import pack_padded_sequence, pad_sequence

from vocab import Vocab

class Merger(nn.Module):
    def __init__(self, args, vocab):
        super().__init__()
        self.bigram_encoder = Merger.BigramCharRNN(
            args,
            output_vocab_size=2,
            input_vocab=vocab.src.char2id)

    def forward(self, bigrams, lengths_bigram):
        outputs_bigram = self.bigram_encoder(bigrams, lengths_bigram)
        return outputs_bigram

    class BigramCharRNN(nn.Module):
        def __init__(self,
                    args,
                    output_vocab_size,
                    input_vocab):
            super().__init__()
            self.input_vocab = input_vocab
            self.hidden_dim = args.rnn_dim_char
            self.num_layers = args.rnn_layers
            self.word_embeddings = nn.Embedding(len(input_vocab), args.ce_dim)
            self.lstm = nn.LSTM(args.ce_dim,
                            args.rnn_dim_char,
                            num_layers=args.rnn_layers,
                            bidirectional=True)
            self.fc0 = nn.Linear(args.rnn_dim_char, 64)
            self.fc1 = nn.Linear(64, output_vocab_size)
            self.relu = nn.ReLU()
            self.dropout = nn.Dropout(0.1)

        def forward(self, bigrams, lengths):
            embeds = self.dropout(self.word_embeddings(bigrams))
            packed_embeds = pack_padded_sequence(
                embeds, lengths, enforce_sorted=False)
            _, hidden = self.lstm(packed_embeds)
            hidden = sum(hidden[i][j, :, :]
                            for i in range(2) for j in range(self.num_layers))
            tag_space = self.relu(self.fc0(hidden))
            tag_space = self.fc1(tag_space)
            return tag_space

class Network:
    def __init__(self, args) -> None:
        self.device = torch.device(
            f'cuda:{args.gpu_index}' if torch.cuda.is_available() else 'cpu')
        self.vocab = Vocab.load(args.vocab_path)
        self.load_data(args)
        self.model = Merger(args, vocab=self.vocab).to(self.device)
        self.loss_function = nn.CrossEntropyLoss()
        self.optimizer = optim.Adam(self.model.parameters())
        self.scheduler = ReduceLROnPlateau(self.optimizer, 'min', patience=3)

    class BigramData(Dataset):
        def __init__(self, data) -> None:
            self.data = data

        def __getitem__(self, index):
            src = torch.tensor(self.data[index][0], dtype=torch.long)
            label = torch.tensor(self.data[index][1], dtype=torch.long)
            return src, label

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

    def load_data(self, args, train_split=0.8):
        with open(args.data) as f:
            data = [line.split('\t') for line in f.readlines()]
            src = self.vocab.src.words2indices([list(line[0].strip()) for line in data])
            labels = [0 if bigram[1].strip() == '0' else 1 for bigram in data]
            src, labels = src[:args.data_size], labels[:args.data_size]
            data = list(zip(src, labels))
            lengths = [int(len(src)*train_split), int(len(src)*(1-train_split))]
            if sum(lengths) != len(src):
                lengths[0] += len(src) - sum(lengths)
            train_data, dev_data = random_split(data, lengths)

            train_data = Network.BigramData(train_data)
            dev_data = Network.BigramData(dev_data)

        def generate_batch(data_batch):
            src_batch, labels_batch = [], []
            lengths_bigram = []
            for src_item, label_item in data_batch:
                lengths_bigram.append(len(src_item))
                src_batch.append(src_item)
                labels_batch.append(label_item)

            src_batch = pad_sequence(
                src_batch, padding_value=self.vocab.src.word2id['<pad>'])
            labels_batch = torch.stack(labels_batch)
            return (src_batch, labels_batch), lengths_bigram

        self.train_iter = DataLoader(train_data, batch_size=args.batch_size,
                                     shuffle=True, collate_fn=generate_batch)
        self.dev_iter = DataLoader(train_data, batch_size=args.batch_size,
                                     collate_fn=generate_batch)

    @staticmethod
    def epoch_time(start_time: int,
                   end_time: int):
        elapsed_time = end_time - start_time
        elapsed_mins = int(elapsed_time / 60)
        elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
        return elapsed_mins, elapsed_secs

    def train(self, args):
        metrics = {'train_loss': [], 'dev_loss': [], 'dev_acc': []}
        for epoch in range(args.epochs):
            self.model.train()
            print(f'Epoch {epoch+1}/{args.epochs}')
            epoch_loss = 0
            start_time = time.time()
            for iteration, train_batch in enumerate(self.train_iter):
                (bigrams, labels), lengths = train_batch
                self.model.zero_grad()
                bigrams, labels = bigrams.to(self.device), labels.to(self.device)
                tag_scores = self.model(bigrams, lengths)
                tag_scores = tag_scores.view(-1, tag_scores.shape[-1])
                labels = labels.view(-1)
                loss = self.loss_function(tag_scores, labels)
                loss.backward()
                self.optimizer.step()
                epoch_loss += loss.item()
                if iteration and iteration % 100 == 0 and len(self.train_iter) - iteration > 10 \
                        or iteration + 1 == len(self.train_iter):
                    for param_group in self.optimizer.param_groups:
                        lr = param_group['lr']
                    print(
                        f'Batch {iteration}/{len(self.train_iter)-1}\t| Loss {loss.item():.7f} | lr {lr}')
                    metrics['train_loss'].append(epoch_loss / iteration)
            end_time = time.time()
            epoch_mins, epoch_secs = Network.epoch_time(start_time, end_time)
            val_metrics = self.evaluate()
            for m in metrics:
                if m != 'train_loss':
                    metrics[m].append(val_metrics[m])
            print(
                f'Epoch {epoch+1}/{args.epochs} | Time: {epoch_mins}m {epoch_secs}s')
            print(
                f"\tTrain Loss: {metrics['train_loss'][-1]:.7f} | Dev. Loss: {metrics['dev_loss'][-1]:.7f} | Dev. Acc.: {metrics['dev_acc'][-1]:.1%}")
            print()
            self.scheduler.step(metrics['dev_loss'][-1])

        self.save_model(args)
        return metrics

    def evaluate(self):
        self.model.eval()
        with torch.no_grad():
            correct, total = 0, 0
            epoch_loss = 0
            for (bigrams, labels), lengths in self.dev_iter:
                # Loss
                bigrams, labels = bigrams.to(self.device), labels.to(self.device)
                output = self.model(bigrams, lengths)
                loss = self.loss_function(output, labels)
                epoch_loss += loss.item()

                # Accuracy
                output = output.argmax(-1)
                correct += torch.sum(output == labels)
                total += labels.shape[0]
        metrics = {}
        metrics['dev_acc'] = correct / total
        metrics['dev_loss'] = epoch_loss / len(self.dev_iter)
        return metrics

    def predict(self):
        self.model.eval()
        pred, gold = [], []
        inputs_bigram = []
        with torch.no_grad():
            for (bigrams, labels), lengths in self.dev_iter:
                bigrams, labels = bigrams.to(self.device), labels.to(self.device)
                output = self.model(bigrams, lengths)
                output = output.argmax(-1)
                pred += list(output.detach().cpu().numpy())
                gold += list(labels.detach().cpu().numpy())
                inputs_bigram += list(bigrams.permute(1, 0).detach().cpu().numpy())

        return inputs_bigram, gold, pred

    @staticmethod
    def load_model(model_path: str):
        params = torch.load(model_path)
        args = params['args']
        network = Network(args)
        network.model.load_state_dict(params['state_dict'])
        return network

    def save_model(self, args):
        save_path = os.path.join(args.cpt, args.logdir) + '.pt'
        print('Saving model parameters to [%s]\n' % save_path)
        params = {
            'args': args,
            'state_dict': self.model.state_dict()
        }
        torch.save(params, save_path)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--batch_size", default=1048,
                        type=int, help="Batch size.")
    parser.add_argument("--epochs", default=50, type=int,
                        help="Number of epochs.")
    parser.add_argument("--ce_dim", default=32, type=int,
                        help="Word embedding dimension.")
    parser.add_argument("--we_dim", default=64, type=int,
                        help="Word embedding dimension.")
    parser.add_argument("--rnn_dim_char", default=64,
                        type=int, help="RNN cell dimension.")
    parser.add_argument("--rnn_dim", default=128,
                        type=int, help="RNN cell dimension.")
    parser.add_argument("--rnn_layers", default=1,
                        type=int, help="Number of RNN layers.")
    parser.add_argument("--data_size", default=10000, type=int,
                        help="Maximum number of examples to load.")
    parser.add_argument("--gpu_index", default=0, type=int,
                        help="Index of GPU to be used.")
    parser.add_argument("--vocab", dest='vocab_path',
                        default="/Users/chriscay/Library/Mobile Documents/com~apple~CloudDocs/NYUAD/camel_morph/sandbox_files/root_generator/data_nn_vocab.json", type=str,
                        help="Path to vocab JSON file.")
    parser.add_argument("--data",
                        default="/Users/chriscay/Library/Mobile Documents/com~apple~CloudDocs/NYUAD/camel_morph/sandbox_files/root_generator/data_nn.tsv", type=str,
                        help="Path to file with bigrams dataset.")
    parser.add_argument("--cpt", default='/Users/chriscay/Library/Mobile Documents/com~apple~CloudDocs/NYUAD/camel_morph/sandbox_files/root_generator/model_weights', type=str,
                        help="Directory to save the model checkpoints to.")
    parser.add_argument("--logs", default='/Users/chriscay/Library/Mobile Documents/com~apple~CloudDocs/NYUAD/camel_morph/sandbox_files/root_generator/logs', type=str,
                        help="Directory to save the model checkpoints to.")
    parser.add_argument("--load", default='', type=str,
                        help="Directory to save the model checkpoints to.")

    parser.add_argument("--seed", default=42, type=int, help="Random seed.")
    args = parser.parse_args([] if "__file__" not in globals() else None)

    torch.manual_seed(args.seed)
    np.random.seed(args.seed)

    args.logdir = "{}-{}-{}".format(
        os.path.basename(globals().get("__file__", "notebook").split('.')[0]),
        datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S"),
        ",".join(("{}={}".format(re.sub(
            "(.)[^_]*_?", r"\1", key), value) for key, value in sorted(vars(args).items()) if isinstance(value, int)))
    )

    if not args.load:
        network = Network(args)
        metrics = network.train(args)
        print(metrics)
    else:
        network = Network.load(args.load)
        inputs, gold, pred = network.predict()
        with open(os.path.join(args.logs, args.logdir), 'w') as f:
            for i, result in enumerate(pred):
                bigram = ''.join(
                    list(map(lambda x: network.vocab.src.id2char[x], inputs[0][i])))
                print(re.sub(r'(<pad>)+?', r'', bigram), file=f, end=' | ')
                print(result, 'gold:', gold[i], file=f, end= ' | ')

if __name__ == '__main__':
    main()


import pytest
import sacrebleu

EPSILON = 1e-4

test_sentence_level_chrf = [
    (
        'Co nás nejvíc trápí, protože lékaři si vybírají, kdo bude žít a kdo zemře.',
        ['Nejvíce smutní jsme z toho, že musíme rozhodovat o tom, kdo bude žít a kdo zemře.'],
        39.14078509,
    ),
    (
        'Nebo prostě nemají vybavení, které by jim pomohlo, uvedli lékaři.',
        ['A někdy nemáme ani potřebný materiál, abychom jim pomohli, popsali lékaři.'],
        31.22557079,
    ),
    (
        'Lapali po dechu, jejich životy skončily dřív, než skutečně začaly.',
        ['Lapali po dechu a pak jejich život skončil - dřív, než skutečně mohl začít, připomněli.'],
        57.15704367,
    ),
]

# hypothesis, reference, expected score
# >= 2.0.0: some orders are not fulfilled in epsilon smoothing (chrF++.py and NLTK)
test_cases = [
    (["abcdefg"], ["hijklmnop"], 0.0),
    (["a"], ["b"], 0.0),
    ([""], ["b"], 0.0),
    ([""], ["ref"], 0.0),
    ([""], ["reference"], 0.0),
    (["aa"], ["ab"], 8.3333),
    (["a", "b"], ["a", "c"], 8.3333),
    (["a"], ["a"], 16.6667),
    (["a b c"], ["a b c"], 50.0),
    (["a b c"], ["abc"], 50.0),
    ([" risk assessment must be made of those who are qualified and expertise in the sector - these are the scientists ."],
     ["risk assessment has to be undertaken by those who are qualified and expert in that area - that is the scientists ."], 63.361730),
    ([" Die    Beziehung zwischen  Obama und Netanjahu ist nicht gerade  freundlich. "],
     ["Das Verhältnis zwischen Obama und Netanyahu ist nicht gerade freundschaftlich."], 64.1302698),
    (["Niemand hat die Absicht, eine Mauer zu errichten"], ["Niemand hat die Absicht, eine Mauer zu errichten"], 100.0),
]

# sacreBLEU < 2.0.0 mode
# hypothesis, reference, expected score
test_cases_effective_order = [
    (["a"], ["a"], 100.0),
    ([""], ["reference"], 0.0),
    (["a b c"], ["a b c"], 100.0),
    (["a b c"], ["abc"], 100.0),
    ([""], ["c"], 0.0),
    (["a", "b"], ["a", "c"], 50.0),
    (["aa"], ["ab"], 25.0),
]

test_cases_keep_whitespace = [
    (
        ["Die Beziehung zwischen Obama und Netanjahu ist nicht gerade freundlich."],
        ["Das Verhältnis zwischen Obama und Netanyahu ist nicht gerade freundschaftlich."],
        67.3481606,
    ),
    (
        ["risk assessment must be made of those who are qualified and expertise in the sector - these are the scientists ."],
        ["risk assessment has to be undertaken by those who are qualified and expert in that area - that is the scientists ."],
        65.2414427,
    ),
]

@pytest.mark.parametrize("hypotheses, references, expected_score", test_cases)
def test_chrf(hypotheses, references, expected_score):
    score = sacrebleu.corpus_chrf(
        hypotheses, [references], char_order=6, word_order=0, beta=3,
        eps_smoothing=True).score
    assert abs(score - expected_score) < EPSILON

@pytest.mark.parametrize("hypotheses, references, expected_score", test_cases_effective_order)
def test_chrf_eff_order(hypotheses, references, expected_score):
    score = sacrebleu.corpus_chrf(
        hypotheses, [references], char_order=6, word_order=0, beta=3,
        eps_smoothing=False).score
    assert abs(score - expected_score) < EPSILON

@pytest.mark.parametrize("hypotheses, references, expected_score", test_cases_keep_whitespace)
def test_chrf_keep_whitespace(hypotheses, references, expected_score):
    score = sacrebleu.corpus_chrf(
        hypotheses, [references], char_order=6, word_order=0, beta=3,
        remove_whitespace=False).score
    assert abs(score - expected_score) < EPSILON

@pytest.mark.parametrize("hypothesis, references, expected_score", test_sentence_level_chrf)
def test_chrf_sentence_level(hypothesis, references, expected_score):
    score = sacrebleu.sentence_chrf(hypothesis, references, eps_smoothing=True).score
    assert abs(score - expected_score) < EPSILON