personal organizing tool
from .address_book import main


if __name__ == '__main__':
    main()

orgapp/address_book/__init__.py
Vskesha/orgApp
from .address_book import main


if __name__ == '__main__':
    main()

orgapp/address_book/__main__.py
Vskesha/orgApp
from .classes_ab import AddressBook, Record, AddressBookFileHandler, Phone, Email, Birthday, Name
from functools import wraps
from colorama import init as init_colorama, Fore, Style
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles.named_colors import NAMED_COLORS
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit import prompt
from pathlib import Path


ADDRESSBOOK_LOGO = """
                .o8        .o8                                       
               "888       "888                                       
 .oooo.    .oooo888   .oooo888  oooo d8b  .ooooo.   .oooo.o  .oooo.o 
`P  )88b  d88' `888  d88' `888  `888""8P d88' `88b d88(  "8 d88(  "8 
 .oP"888  888   888  888   888   888     888ooo888 `"Y88b.  `"Y88b.  
d8(  888  888   888  888   888   888     888    .o o.  )88b o.  )88b 
`Y888""8o `Y8bod88P" `Y8bod88P" d888b    `Y8bod8P' 8""888P' 8""888P' 

             .o8                           oooo        
            "888                           `888        
             888oooo.   .ooooo.   .ooooo.   888  oooo  
             d88' `88b d88' `88b d88' `88b  888 .8P'   
             888   888 888   888 888   888  888888.    
             888   888 888   888 888   888  888 `88b.  
             `Y8bod8P' `Y8bod8P' `Y8bod8P' o888o o888o 
"""

# Команди та описи для користувача
COMMANDS = {
    'add_email': ['add_email'],
    'add_phone_number': ['add_phone'],
    'add_record': ['add'],
    'change_email': ['change_email'],
    'change_phone_number': ['change_phone'],
    'days_to_birthday': ['when_birthday'],
    'exit': ['exit'],
    'find_records': ['find'],
    'get_all_records': ['all'],
    'get_birthdays_per_week': ['get_list'],
    'load_from_file': ['load'],
    'remove_email': ['remove_email'],
    'remove_phone_number': ['remove_phone'],
    'remove_record': ['remove'],
    'save_to_file': ['save'],
    'help': ['help']
}

COMMAND_DESCRIPTIONS = {
    'add an email': ['add_email'],
    'add a phone number': ['add_phone'],
    'add contact to AdressBook ': ['add'],
    'change an email ': ['change_email'],
    'change phone number': ['change_phone'],
    'return days until birthday': ['when_birthday'],
    'exit from AdressBook ': ['exit'],
    'find contact in AdressBook': ['find'],
    'display all contacts': ['all'],
    'return list of birthdays': ['get_list'],
    'load information about contacts from file': ['load'],
    'remove an email': ['remove_email'],
    'remove phone number': ['remove_phone'],
    'remove contact from AdressBook': ['remove'],
    'save information about contacts to file': ['save'],
    'display help': ['help']
}

FILE_PATH = Path.home() / "orgApp" / "address_book.json"  # for working on different filesystems
FILE_PATH.parent.mkdir(parents=True, exist_ok=True)


class RainbowLexer(Lexer):
    """
    Lexer for rainbow syntax highlighting.

    This lexer assigns colors to characters based on the rainbow spectrum.
    """

    def lex_document(self, document):
        colors = list(sorted({"Teal": "#028000"}, key=NAMED_COLORS.get))

        def get_line(lineno):
            return [
                (colors[i % len(colors)], c)
                for i, c in enumerate(document.lines[lineno])
            ]

        return get_line


def print_command_list():
    """
    Generate a formatted list of available commands with their descriptions.
    This function formats the list of available commands and their descriptions
    in a user-friendly way for display.
    """
    print(Fore.GREEN, "Available commands:")
    separator = '|----------------------|--------------------------------------------|'
    print(separator, f'\n|  Commands            |  Description {" ":30}|\n', separator, sep='')
    for description, commands in COMMAND_DESCRIPTIONS.items():
        print(f"| {Fore.WHITE} {', '.join(commands):<20}{Fore.GREEN}| {description:<43}|")
    print(separator, '\n')


def command_parser(user_input: str) -> tuple[callable, str]:
    """
    Parse user input to determine the corresponding command and data.
    This function parses the user's input to identify the command they want to
    execute and the associated data, if any..
    """
    if not user_input:
        raise IndexError("Nothing was entered ...")
    func, data = None, []
    lower_input_end_spaced = user_input.lower() + ' '
    for command, aliases in COMMANDS.items():
        for alias in aliases:
            if lower_input_end_spaced.startswith(alias + ' '):
                func = globals()[f'handle_{command}']
                data = user_input[len(alias) + 1:].strip()
                break
        if func:
            break
    if not func:
        raise ValueError(f"There is no such command {user_input.split()[0]}")
    return func, data


def validate_input(prompt, validator=None, error_message=None, final_error_message=None, transform_func=None):
    """
    Prompt the user for input and validate it using a specified validator function.
    This function prompts the user for input, checks it, and if it doesn't pass
    the validation check, it prompts the user for input again. If no validator is
    provided, any non-empty input is considered valid.
    """
    user_input = input(prompt).strip()
    if not user_input:
        return None
    if validator:
        attempts = 2
        while not validator(user_input):
            if error_message:
                print(error_message)
            attempts -= 1
            if attempts == 0:
                if final_error_message:
                    print(final_error_message)
                return None
            user_input = input(prompt).strip()  # Перепитати користувача
    if transform_func:
        user_input = transform_func(user_input)
    return user_input


validation_info = {
    "name": {
        "prompt": "Enter a contact name: ",
        "validator": lambda x: Name(x).validate(x),
        "error_message": "Invalid name. Please use only letters and more than one. Try again!!!",
        "final_error_message": "Attempts ended when entering a name.",
        "transform_func": lambda x: x.lower().strip()

    },
    "phone": {
        "prompt": "Enter the phone number (+380________): ",
        "validator": lambda x: not x or Phone(x).validate(x),
        "error_message": "The phone does not match the format (+380________). Try again!!!",
        "final_error_message": "The attempts ended when the phone was entered.",
        "transform_func": lambda x: x.strip()
    },
    "email": {
        "prompt": "Enter an email in an acceptable format: ",
        "validator": lambda x: not x or Email(x).validate(x),
        "error_message": "The mail format is not acceptable. Try again!!!",
        "final_error_message": "Attempts ended when email was entered.",
        "transform_func": lambda x: x.strip()
    },
    "birthday": {
        "prompt": "Enter the contact's birthday in the format (dd.mm.yyyy): ",
        "validator": lambda x: not x or Birthday(x).validate(x),
        "error_message": "Date of birth format should be (dd.mm.yyyy) or another error. Try again!!!",
        "final_error_message": "Attempts ended when birthday was entered.",
        "transform_func": lambda x: x.strip()
    },
    }


# Функції-обробники команд
def handle_add_email(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'add_email' command. Adds an email
    address to a contact in the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Failed to enter name to add mail."
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        if contact.email:
            return f"An email address for {name} already exists: {contact.email.value}"
        else:
            print('Add email:')
            new_email = validate_input(**validation_info["email"])
            if contact.add_email(new_email):
                return f"Email added successfully. \n{address_book.get_all_records()}"
            else:
                return "Email is not valid."
    else:
        return f"The contact with the name {name} was not found in the address book."


def handle_add_phone_number(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'add_phone' command. Adds phone
    to a contact in the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Failed to enter name to add phone."
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        print('Add phone:')
        new_phone = validate_input(**validation_info["phone"])
        if new_phone not in [record.value for record in contact.phones]:
            if contact.add_phone_number(new_phone):
                return f"Phone number successfully added. \n{address_book.get_all_records()}"
            else:
                return "The phone number is not valid."
        else:
            return f"The number {new_phone} has not been added because it already exists in the contact {name}!!!"
    else:
        return f"The contact with the name {name} was not found in the address book."


def handle_add_record(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'add' command. Adds a new contact to the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return f"Failed to add contact!!!"
    if name.lower().strip() in [record.name.value.lower().strip() for record in address_book.data.values()]:
        return f"A contact with that name already exists!!!"
    else:
        phone = validate_input(**validation_info["phone"])
        email = validate_input(**validation_info["email"])
        birthday = validate_input(**validation_info["birthday"])
    new_record = Record(name)
    if phone:
        new_record.phones = [Phone(phone)]
    if birthday:
        new_record.birthday = Birthday(birthday)
    if email:
        new_record.email = Email(email)
    if address_book.add_record(new_record):
        return f"The contact has been successfully added to the address book. \n{address_book.get_all_records()}"
    else:
        return "The data is not valid."


def handle_change_email(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'change_email' command. Changes the email address of a contact.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Failed to enter name to change email."
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        print("The old email:")
        old_email = validate_input(**validation_info["email"])
        if not old_email:
            return f'The old email address was not entered or is not valid'
        else:
            if old_email != contact.email.value:
                return f'Unable to change the email address.' \
                       f'There is no email address {old_email} in the contact {name}.\n{address_book.get_all_records()}'
            else:
                 print("The new email:")
                 new_email = validate_input(**validation_info["email"])
                 if new_email:
                    contact.change_email(old_email, new_email)
                    return f"The email has been successfully changed from {old_email} to {new_email}.\n{address_book.get_all_records()}"
                 else:
                    return f'New email is not entered or is not valid'
    else:
        return f"The contact with the name {name} was not found in the address book."


def handle_change_phone_number(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'change_phone' command. Changes the phone number of a contact.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "You failed to enter a name to change the contact's phone number."
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        print("The old phone number:")
        old_phone = validate_input(**validation_info["phone"])
        if old_phone in [record.value for record in contact.phones]:
            print("The new phone number:")
            new_phone = validate_input(**validation_info["phone"])
            if new_phone:
                if new_phone not in [record.value for record in contact.phones]:
                    contact.change_phone_number(old_phone, new_phone)
                    return f"The phone number {old_phone} has been successfully updated to {new_phone}.\n{address_book.get_all_records()}"
                else:
                    return f'Failed to replace the phone {old_phone} with the new one {new_phone}. You entered the same phone number.'
            else:
                return 'Failed to replace the phone number with a new one. The new phone number is missing or not valid'
        else:
            return 'The old phone number is not found in the address book or is not valid.'
    else:
        return f"The contact with the name {name} was not found in the address book."


def handle_days_to_birthday(arg: str, address_book: AddressBook) -> str:
    """
     Command handler for 'when_birthday' command. Calculates the
     days until the birthday of a contact
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "You failed to enter a name to find out the contact's birthday."
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        if contact.birthday:
            days = contact.days_to_birthday()
            return f"Name {name} has {days} days left until their birthday."
        else:
            return f"The contact {name} does not have a birth date specified."
    else:
        return f"Contact with the name {name} was not found in the address book."


def handle_exit(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'exit' command. Exits the address book application.
    """
    return handle_save_to_file(arg, address_book) + '\nGoodbye!'


def handle_find_records(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'find' command. Searches for contacts
    in the address book based on user-specified criteria.
    """
    search_criteria = {}
    print("What criteria would you like to search by?\n1. Search by name\n2. Search by phone number.")
    search_option = input("Select an option (1 or 2): ")
    if search_option == "1":
        search_name = input("Enter a name to search for (minimum 2 characters): ").strip().lower()
        if len(search_name) >= 2:
            search_criteria['name'] = search_name
        else:
            return "You entered too few characters for the name. Name search canceled."
    elif search_option == "2":
        search_phones = input("Please enter a part of the phone number for the search (minimum 5 characters): ").strip()
        if len(search_phones) >= 5:
            search_criteria['phones'] = search_phones
        else:
            return "You entered too few characters for a phone number. Phone number search canceled."
    else:
        return "Invalid option selected. Search canceled."
    results = address_book.find_records(**search_criteria)
    if results:
        return f"Search results: \n{address_book.get_all_records()}"
    else:
        return "The contact meeting the specified criteria was not found."


def handle_get_all_records(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'all' command. Retrieves and
    returns all records in the address book.
    """
    return address_book.get_all_records()


def handle_get_birthdays_per_week(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'get_list' command. Retrieves birthdays
    for the specified number of days ahead.
    """
    num_str = input("Enter the number of days: ")
    if num_str.isdigit():
        num = int(num_str)
        birthdays_list = address_book.get_birthdays_per_week(num)
        if birthdays_list:
            return "\n".join(birthdays_list)
        else:
            return "No birthdays today."
    else:
        return "You entered a non-number. Please try running the command again!!!"


def handle_load_from_file(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'load_from_file' command. Loads the address
    book data from a file.
    """
    arg = arg.strip()
    if arg and (not Path(arg).exists() or not Path(arg).is_file()):
        return f"Шлях до файлу не існує"
    arg = arg if arg else str(FILE_PATH)
    file_handler = AddressBookFileHandler(arg)
    loaded_address_book = file_handler.load_from_file()
    address_book.data.update(loaded_address_book.data)
    return f"Адресну книгу завантажено з файлу {arg}"


def handle_remove_email(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'remove_email' command. Removes an email
    address from a contact in the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Failed to enter a name for email removal!!!"
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        print('To delete')
        email_to_remove = validate_input(**validation_info["email"])
        if email_to_remove and email_to_remove != contact.email.value:
            return f"The email {email_to_remove} is not present in the contact {name}!!!\n{address_book.get_all_records()}"
        if contact.remove_email(email_to_remove):
            return f"The email {email_to_remove} has been successfully deleted. \n{address_book.get_all_records()}"
        else:
            return "Failed to delete the email."
    else:
        return f"The contact with the name {name} was not found in the address book."


def handle_remove_phone_number(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'remove_phone' command. Removes a phone
    number from a contact in the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Failed to enter a name for phone number deletion!!!"
    else:
        contact = address_book.get_record_by_name(name)
    if contact is not None:
        print('To delete')
        phone_to_remove = validate_input(**validation_info["phone"])
        if phone_to_remove and phone_to_remove not in [phone.value for phone in contact.phones]:
            return f"The phone number {phone_to_remove} is not found in the contact {name}!!!\n{address_book.get_all_records()}"
        if contact.remove_phone_number(phone_to_remove):
            return f"The phone number {phone_to_remove} has been successfully deleted.\n{address_book.get_all_records()}"
        else:
            return "Failed to delete the phone number."
    else:
        return f"The contact {name} was not found in the address book or the address book has not been created yet."


def handle_remove_record(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'remove' command. Removes a contact from
    the address book.
    """
    name = validate_input(**validation_info["name"])
    if name is None:
        return "Could not enter name to delete contact!!!"
    if address_book.remove_record(name):
        return f"Contact {name} has been successfully removed from the address book. \n{address_book.get_all_records()}"
    else:
        return f"The contact {name} is not found in the address book or the book has not yet been added."


def handle_help(arg: str, address_book: AddressBook) -> str:
    """Outputs the command menu"""
    print_command_list()
    return ''


def handle_save_to_file(arg: str, address_book: AddressBook) -> str:
    """
    Command handler for 'save' command. Saves the address
    book data to a file.
    """
    FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
    file_handler = AddressBookFileHandler(str(FILE_PATH))
    file_handler.save_to_file(address_book)
    return f"The address book has been saved at the following path {str(FILE_PATH)}"


def input_error(func):
    """
    A decorator wrapper for error handling.

    Args:
        func (callable): The function to wrap with error handling.

    Returns:
        callable: The wrapped function with error handling.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            result = func(*args, **kwargs)
            return result
        except IndexError as e:
            print(Fore.RED, 'Not enough data.', str(e))
        except ValueError as e:
            print(Fore.RED, 'Wrong value.', str(e))
        except KeyError as e:
            print(Fore.RED, 'Wrong key.', str(e)[1:-1])
        except TypeError as e:
            print(Fore.RED, 'Wrong type of value.', str(e))
        except FileNotFoundError as e:
            print(Fore.RED, e)
        except Exception as e:
            print(Fore.RED, e)
    return wrapper


@input_error
def main_cycle(address_book: AddressBook) -> bool:
    """
    Return True if it needs to stop the program. False otherwise.
    """
    completer = NestedCompleter.from_nested_dict({command[0]: None for command in COMMANDS.values()})
    user_input = prompt('>>> ', completer=completer, lexer=RainbowLexer()).strip().lower()
    func, argument = command_parser(user_input)
    result = func(argument, address_book)
    print(Fore.WHITE, result)
    return result.endswith('Goodbye!')


def prepare() -> None:
    """
    Prints initial information to the user
    :return: None
    """
    init_colorama()
    print(Fore.BLUE + Style.BRIGHT + ADDRESSBOOK_LOGO)
    print(Fore.CYAN + "Welcome to your ADDRESS BOOK!")
    print()
    print_command_list()


def main():
    """
    Main entry point for the address book program.
    This function initializes the address book, prepares
    the environment, and enters the main program loop.
    """
    address_book = AddressBook()
    print(handle_load_from_file('', address_book))
    prepare()

    while True:
        if main_cycle(address_book):
            break


if __name__ == '__main__':
    main()

orgapp/address_book/address_book.py
Vskesha/orgApp
from collections import UserDict
from datetime import datetime, timedelta
import calendar
import re
import json


class Field:
    """
    A base class for representing fields with validation.
    Args:
        value: The initial value of the field.
    """
    def __init__(self, value: str):
        self.__value = None
        self.value = value

    def validate(self, new_value: str) -> bool:
        """
        Validates a new value for the field.
        """
        return True

    @property
    def value(self) -> str:
        """
        Property representing the field's value.
        """
        return self.__value

    @value.setter
    def value(self, new_value: str) -> None:
        """
        Setter for the field's value.
        """
        self.__value = new_value

    def __str__(self)-> str:
        """
        Returns a string representation of the field's value.
        """
        return str(self.value)


class Phone(Field):
    """
    A class representing a phone number field with validation.
    Args:
        value: The initial value of the phone number.
    """
    def __init__(self, value: str = None):
        super().__init__(value)

    @Field.value.setter
    def value(self, new_value: str) -> str:
        """
        Setter method for the phone number field.
         """
        if self.validate(new_value):
            Field.value.fset(self, new_value)
        else:
            return f'The phone number {new_value} cannot be assigned as it is not valid.'

    def validate(self, number: str) -> bool:
        """
        Validates a new phone number value.
        """
        if number is None:
            return False
        try:
            phone_format = r'\+380\d{9}'
            if not re.match(phone_format, number):
                return False
            return True
        except ValueError:
            return False


class Email(Field):
    """
    A class representing an email address field with validation.
    Args:
        value: The initial value of the email address..
    """
    def __init__(self, value: str = None):
        super().__init__(value)

    @Field.value.setter
    def value(self, new_value: str) -> str:
        """
        Setter method for the email field.
        """
        if self.validate(new_value):
            Field.value.fset(self, new_value)
        else:
            return f'The email {new_value} cannot be assigned as it is not valid.'

    def validate(self, email: str) -> bool:
        """
        Validates a email value.
        """
        if email is None:
            return False
        try:
            email_format = r'\b(?![A-Za-z0-9._%+-]*a@t[A-Za-z0-9._%+-]*)\b(?![0-9]{2})[A-Za-z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b'
            if not re.match(email_format, email):
                return False
            return True
        except ValueError:
            return False


class Name(Field):
    """
    A class representing a name field with validation.
    Args:
        value: The initial value of the name.
    """
    def __init__(self, value: str):
        super().__init__(value)

    @Field.value.setter
    def value(self, new_value: str) -> str:
        """
        Setter method for the new name value.
        """
        if self.validate(new_value):
            Field.value.fset(self, new_value)
        else:
            return f'The name {new_value} cannot be assigned as it is not valid.'

    def validate(self, name: str) -> bool:
        """
        Validates a new name value.
        """
        try:
            name_format = r'^[A-Za-zА-Яа-я\s]+$'
            if re.match(name_format, name) and len(name) > 1:
                return True
            return False
        except ValueError:
            return False


class Birthday(Field):
    """
    A class representing a birthday field with validation.
    Args:
        value: The initial value of the birthday.
    """
    def __init__(self, value: str = None):
        super().__init__(value)

    @Field.value.setter
    def value(self, new_value: str) -> str:
        """
        Setter method for the new birthday value.
        """
        if self.validate(new_value):
            Field.value.fset(self, new_value)
        else:
            return f'The date of birth {new_value} cannot be assigned as it is not valid.'

    def validate(self, new_value: str) -> bool:
        """
        Validates a new birthday value.
        """
        try:
            parsed_date = datetime.strptime(new_value, '%d.%m.%Y').date()
            today = datetime.now().date()
            if parsed_date > today:
                return False
            return True
        except ValueError:
            return False
        except TypeError:
            return False


class Record:
    """
    A class representing a contact record in an address book.

    Args:
        name (str): The name of the contact.
        phone (str): The phone number of the contact. Default is None.
        birthday (str, optional): The birthday of the contact in 'dd.mm.yyyy' format. Default is None.
        email (str, optional): The email address of the contact. Default is None.
    """
    def __init__(self, name: str, phone: str = None, birthday: str = None, email: str = None):
        self.birthday = Birthday(birthday) if birthday is not None else None
        self.email = Email(email) if email is not None else None
        self.name = Name(name)
        self.phones = [Phone(phone)] if phone is not None else []

    def add_email(self, email_value: str) -> bool:
        """
        Adds an email address to the contact's record.
        """
        if email_value:
            self.email = Email(email_value)
            return True
        return False

    def change_email(self, email: str, new_email_value: str) -> bool:
        """
        Changes the email address of the contact.
        """
        if self.email is not None and self.email.value == email:
            if self.email.validate(new_email_value):
                self.email = Email(new_email_value)
                return True
        return False

    def remove_email(self, del_email: str) -> bool:
        """
        Removes the email address from the contact's record.
        """
        if self.email and self.email.value == del_email:
            self.email = None
            return True
        return False

    def add_phone_number(self, number: str) -> bool:
        """
        Adds a phone number to the contact's record.
        """
        phone = Phone(number)
        if phone.validate(number) and number not in self.phones:
            self.phones.append(phone)
            return True
        else:
            return False

    def change_phone_number(self, number: str, new_number: str) -> bool:
        """
         Changes a phone number in the contact's record.
        """
        for index, phone in enumerate(self.phones):
            if phone.value == number:
                self.phones[index] = Phone(new_number)
                return True
        return False

    def remove_phone_number(self, number: str) -> bool:
        """
        Removes a phone number from the contact's record.
        """
        if any(phone.value == number for phone in self.phones):
            new_phones = [phone for phone in self.phones if phone.value != number]
            self.phones = new_phones
            return True
        return False

    def days_to_birthday(self) -> int | None:
        """
         Calculates the number of days remaining until the contact's next birthday.
        """
        if self.birthday and self.birthday.validate(self.birthday.value):
            parsed_date = datetime.strptime(self.birthday.value, '%d.%m.%Y').date()
            date_now = datetime.now().date()
            date_now_year = date_now.year
            next_year = date_now.year + 1
            parsed_date = parsed_date.replace(year=date_now_year)

            if parsed_date <= date_now:
                if calendar.isleap(next_year):
                    time_delta = (parsed_date - date_now + timedelta(days=366)).days
                else:
                    time_delta = (parsed_date - date_now + timedelta(days=365)).days
            else:
                time_delta = (parsed_date - date_now).days
            return time_delta
        else:
            return None

    def __str__(self) -> str:
        """
        Returns a string representation of the contact record.
        """
        phones_str = ', '.join(str(phone) for phone in self.phones)
        if not phones_str:
            phones_str = "None"
        if not self.birthday:
            birthday_str = "None"
        else:
            birthday_str = self.birthday.value
        if not self.email:
            email_str = "None"
        else:
            email_str = self.email.value
        return f"Name: {self.name.value}, Phones: {phones_str}, Email: {email_str}, Birthday: {birthday_str}"


class AddressBook(UserDict):
    """
    A class representing an address book that stores and
    manages contact records.
    """
    def add_record(self, record: Record) -> bool:
        """
        Adds a contact record to the address book
        if it passes validation.
        """
        if self.validate_record(record):
            key = record.name.value
            self.data[key] = record
            return True
        else:
            return False

    def find_records(self, **search_criteria: dict) -> list:
        """
        Finds and returns a list of contact records that
        match the given search criteria.
        """
        result = []
        found = False
        for record in self.data.values():
            if 'name' in search_criteria and len(search_criteria['name']) >= 2 and search_criteria[
                'name'] in record.name.value:
                result.append(record)
                found = True
            if 'phones' in search_criteria and len(search_criteria['phones']) >= 5:
                for phone in record.phones:
                    if search_criteria['phones'] in phone.value:
                        result.append(record)
                        found = True
                        break
        if not found:
            print("There is no contact that matches the specified search criteria.")
        return result

    def get_all_records(self) -> str:
        """
        Retrieves all contact records in the address
        book and returns them.
        """
        all_contacts = []
        header = '{:<20} {:<30} {:<20} {:<20}'.format('Name', 'Phone', 'Birthday', 'Email')
        separator = '-' * len(header)
        all_contacts.append(header)
        all_contacts.append(separator)

        if self.data:
            for record in self.data.values():
                phones = ', '.join([f'{phone.value}' for phone in record.phones])
                birthday_str = record.birthday.value if record.birthday else '-'
                email_str = record.email.value if record.email else '-'
                record_str = '{:<20} {:<30} {:<20} {:<20}'.format(
                    record.name.value,
                    phones,
                    birthday_str,
                    email_str
                )
                all_contacts.append(record_str)
        else:
            all_contacts.append("The address book is empty.")
        return '\n'.join(all_contacts)

    def get_birthdays_per_week(self, num: int) -> list:
        """
        Finds and returns a list of names whose birthdaуs
        begin after a given number of days.
        """
        to_day = datetime.now().date()
        new_date = to_day + timedelta(days=num)

        happy_birthday = []
        for record in self.data.values():
            if record.birthday:
                birthdate = datetime.strptime(record.birthday.value, '%d.%m.%Y').date()
                if birthdate.day == new_date.day and birthdate.month == new_date.month:
                    happy_birthday.append(record.name.value)

        print(f'List of birthday celebrants to greet in {num} day(s): {happy_birthday}')
        return happy_birthday

    def get_record_by_name(self, name: str) -> 'Record':
        """
        Retrieves a contact record by searching for a name.
        """
        for record in self.data.values():
            if record.name.value == name:
                return record
        return None

    def remove_record(self, name: str) -> bool:
        """
        Removes a contact record by name.
        """
        if name in self.data:
            del self.data[name]
            return True
        else:
            return False

    def validate_record(self, record: Record) -> bool:
        """
        Validates a contact record, including name,
        phone numbers, birthday, and email.
        """
        valid_phones = all(isinstance(phone, Phone) and phone.validate(phone.value) for phone in record.phones)
        valid_name = isinstance(record.name, Name)

        if record.birthday:
            valid_birthday = isinstance(record.birthday, Birthday) and record.birthday.validate(record.birthday.value)
        else:
            valid_birthday = True

        if record.email:
            valid_email = isinstance(record.email, Email) and record.email.validate(record.email.value)
        else:
            valid_email = True

        if not valid_phones:
            print("Phone numbers are not valid.")
        if not valid_name:
            print("Name is not valid.")
        if not valid_birthday:
            print("Date of birth is not valid.")
        if not valid_email:
            print("Email is not valid.")
        return valid_phones and valid_name and valid_birthday and valid_email

    def iterator(self, n: int) -> Record:
        """
        Splits the address book into iterators with 'n'
        records per iteration.
        """
        return (list(self.data.values())[i:i + n] for i in range(0, len(self.data), n))

    def __iter__(self):
        """
        Initializes the iterator for the address book.
        """
        self.index = 0
        return self

    def __next__(self) -> 'Record':
        """
        Iterates through the address book, returning one record at a time.
        """
        if self.index < len(self.data):
            record = list(self.data.values())[self.index]
            self.index += 1
            return record
        else:
            raise StopIteration

    def __str__(self) -> str:
        """
        Returns a string representation of the address book.
        """
        book_str = "\n".join(f"{name}: {record}" for name, record in self.data.items())
        return book_str


class AddressBookFileHandler:
    """
    A class for handling the serialization and deserialization of an AddressBook to/from a file.
    Args:
        file_name (str): The name of the file to read from or write to.
    """
    def __init__(self, file_name: str):
        self.file_name = file_name

    def save_to_file(self, address_book: AddressBook) -> None:
        """
        Serializes and saves an AddressBook to a file.
        """
        with open(self.file_name, 'w') as file:
            json.dump(address_book.data, file, default=self._serialize_record, indent=4)

    def _deserialize_record(self, contact_data: dict) -> Record:
        """
        Deserializes a contact record from a dictionary.
        """
        if isinstance(contact_data, str):
            return None
        name = contact_data.get('name')
        phones = contact_data.get('phones', [])
        birthday = contact_data.get('birthday')
        email = contact_data.get('email')
        record = Record(name, None, birthday, email)
        for phone in phones:
            record.add_phone_number(phone)
        return record

    def load_from_file(self) -> AddressBook:
        """
        Loads and deserializes an AddressBook from a file.
        """
        addressbook = AddressBook()
        try:
            with open(self.file_name, 'r') as file:
                records_data = json.load(file)
                for contact_data in records_data.values():
                    addressbook.add_record(self._deserialize_record(contact_data))
        except FileNotFoundError:
            pass
        except Exception:
            pass
        return addressbook

    def _serialize_record(self, record: Record) -> dict:
        """
       Serializes a contact record to a dictionary.
        """
        return {
            'name': record.name.value,
            'phones': [phone.value for phone in record.phones],
            'birthday': record.birthday.value if record.birthday else None,
            'email': record.email.value if record.email else None
        }

orgapp/address_book/classes_ab.py
Vskesha/orgApp
from .bandergoose import main


if __name__ == '__main__':
    main()

orgapp/bandergoose/__init__.py
Vskesha/orgApp
from .bandergoose import main


if __name__ == '__main__':
    main()

orgapp/bandergoose/__main__.py
Vskesha/orgApp
import os
import random
import pygame
from pygame.constants import QUIT, K_DOWN, K_UP, K_LEFT, K_RIGHT


HEIGHT = 700
WIDTH = 1200
COLOR_WHITE = (255, 255, 255)
COLOR_BLACK = (0, 0, 0)
COLOR_BLUE = (0, 0, 255)
COLOR_GREEN = (0, 255, 0)
COLOR_RED = (255, 0, 0)
PLAYER_IMAGES = [f'orgapp/bandergoose/1-{i}.png' for i in range(1, 6)]


def create_enemy():
    enemy = pygame.image.load('orgapp/bandergoose/enemy.png').convert_alpha()
    enemy_size = (enemy.get_width(), enemy.get_height())
    enemy_rect = pygame.Rect(WIDTH, random.randint(enemy_size[1], HEIGHT-2*enemy_size[1]), *enemy_size)
    enemy_move = [random.randint(-8, -4), 0]
    return [enemy, enemy_rect, enemy_move]


def create_bonus():
    bonus = pygame.image.load('orgapp/bandergoose/bonus.png').convert_alpha()
    bonus_size = (bonus.get_width(), bonus.get_height())
    bonus_rect = pygame.Rect(random.randint(bonus_size[0], WIDTH-2*bonus_size[0]), -bonus_size[1], *bonus_size)
    bonus_move = [0, random.randint(4, 8)]
    return [bonus, bonus_rect, bonus_move]


def main():
    print('bi', end=' ')
    pygame.init()
    print('ai', end=' ')
    FONT = pygame.font.SysFont('Verdana', 25)
    GAME_OVER_FONT = pygame.font.SysFont('Verdana_bold', 200)
    print('af')
    main_display = pygame.display.set_mode((WIDTH, HEIGHT))
    bg_image = pygame.image.load('orgapp/bandergoose/background.png')
    bg = pygame.transform.scale(bg_image, (WIDTH, HEIGHT))
    bg_x1, bg_x2 = 0, bg.get_width()
    bg_move = 3

    player = pygame.image.load('orgapp/bandergoose/player.png').convert_alpha()
    player_size = (player.get_width(), player.get_height())
    player_rect = pygame.Rect(0, (HEIGHT - player_size[1]) // 2, *player_size)
    player_move_down = [0, 5]
    player_move_right = [5, 0]
    player_move_up = [0, -5]
    player_move_left = [-5, 0]

    CREATE_ENEMY = pygame.USEREVENT + 1
    pygame.time.set_timer(CREATE_ENEMY, 2500)
    CREATE_BONUS = pygame.USEREVENT + 2
    pygame.time.set_timer(CREATE_BONUS, 4000)
    CHANGE_IMAGE = pygame.USEREVENT + 3
    pygame.time.set_timer(CHANGE_IMAGE, 150)

    enemies = []
    bonuses = []
    playing = True
    quit_game = False
    score = 0
    image_index = 0

    while playing:
        FPS = pygame.time.Clock()
        FPS.tick(150)

        for event in pygame.event.get():
            if event.type == QUIT:
                playing = False
                quit_game = True
            if event.type == CREATE_ENEMY:
                enemies.append(create_enemy())
            if event.type == CREATE_BONUS:
                bonuses.append(create_bonus())
            if event.type == CHANGE_IMAGE:
                player = pygame.image.load(os.path.join(PLAYER_IMAGES[image_index]))
                image_index += 1
                if image_index >= len(PLAYER_IMAGES):
                    image_index = 0

        bg_x1 -= bg_move
        bg_x2 -= bg_move
        if bg_x1 < -bg.get_width():
            bg_x1, bg_x2 = bg_x2, bg_x2 + bg.get_width()
        main_display.blit(bg, (bg_x1, 0))
        main_display.blit(bg, (bg_x2, 0))
        main_display.blit(player, player_rect)

        keys = pygame.key.get_pressed()

        if keys[K_DOWN] and player_rect.bottom < HEIGHT:
            player_rect = player_rect.move(player_move_down)

        if keys[K_RIGHT] and player_rect.right < WIDTH:
            player_rect = player_rect.move(player_move_right)

        if keys[K_UP] and player_rect.top > 0:
            player_rect = player_rect.move(player_move_up)

        if keys[K_LEFT] and player_rect.left > 0:
            player_rect = player_rect.move(player_move_left)

        for enemy in enemies:
            enemy[1] = enemy[1].move(enemy[2])
            if player_rect.colliderect(enemy[1]):
                playing = False
                explosion = pygame.image.load('orgapp/bandergoose/explosion.png').convert_alpha()
                enemy[1] = enemy[1].move(-150,-150)
                main_display.blit(explosion, enemy[1])
            else:
                main_display.blit(enemy[0], enemy[1])

        for bonus in bonuses:
            bonus[1] = bonus[1].move(bonus[2])
            main_display.blit(bonus[0], bonus[1])
            if player_rect.colliderect(bonus[1]):
                score += 1
                bonuses.pop(bonuses.index(bonus))
        main_display.blit(FONT.render(f'Рахунок: {score}', True, COLOR_BLACK), (WIDTH-150, 20))
        pygame.display.flip()

        for enemy in enemies:
            if enemy[1].right < 0:
                enemies.pop(enemies.index(enemy))

        for bonus in bonuses:
            if bonus[1].top > HEIGHT:
                bonuses.pop(bonuses.index(bonus))

    while not quit_game:
        for event in pygame.event.get():
            if event.type == QUIT:
                quit_game = True
        main_display.blit(GAME_OVER_FONT.render('GAME OVER', True, COLOR_RED), (200, 300))
        pygame.display.flip()


if __name__ == '__main__':
    main()

orgapp/bandergoose/bandergoose.py
Vskesha/orgApp
from .hannoitower import main


if __name__ == '__main__':
    main()

orgapp/hannoitower/__init__.py
Vskesha/orgApp
from .hannoitower import main


if __name__ == '__main__':
    main()

orgapp/hannoitower/__main__.py
Vskesha/orgApp
from tkinter import *
from time import sleep


class HanoiTower(Canvas):

    def __init__(self):
        super().__init__(bg='grey40')
        self.n_game = None
        self.text_info = None
        self.previous_column = 0
        self._from = None
        self._to = None
        self.end_position = None
        self.end_position_rect = None
        self.choose_button = None
        self.choose_text = None
        self.selected = False
        self.master.title('Hanoi Tower')
        self.pack(fill=BOTH, expand=TRUE)
        self.update()
        self.width = self.winfo_width()
        self.height = self.winfo_height()
        self.bagels = [[], [], []]
        self.min_bagel = 10
        self.draw_towers()
        self.choosing_start_position()
        self.colors = ('grey20', 'grey30', 'grey40', 'purple', 'blue', 'cyan', 'green', 'yellow', 'orange', 'red')

    def draw_towers(self):
        radius = (self.width / 60)
        height = (self.height * 0.7)
        base_radius = (self.width / 7)
        base_height = (self.height / 30)
        for i in range(3):
            down_point = (self.width * (i / 3 + 1 / 6), self.height * 0.85)
            self.create_rectangle(down_point[0] - radius,
                                  down_point[1] - height,
                                  down_point[0] + radius,
                                  down_point[1],
                                  fill='white', outline='black')
            self.create_rectangle(down_point[0] - base_radius,
                                  down_point[1],
                                  down_point[0] + base_radius,
                                  down_point[1] + base_height,
                                  fill='white', outline='black')
        self.text_info = self.create_text(self.width / 2, self.height / 2, anchor=CENTER,
                                          font=f'Arial {self.height//8} bold')
        self.update()

    def choosing_start_position(self):
        # print('Choosing start position')
        self.choose_text = self.create_text(self.width / 2, self.height / 20,
                                            font=f'Arial {self.height // 40} bold',
                                            anchor=CENTER,
                                            text='Choose start position and press "OK"')
        self.choose_button = Button(self, anchor=CENTER, background='cyan',
                                    font=f'Arial {self.height // 40} bold',
                                    text='OK', padx=70)
        self.choose_button.place(x=self.width / 2, y=self.height * 19 / 20, anchor=CENTER)
        self.bind('<Button-1>', self.add_bagel)
        self.n_game = Button(self, anchor=CENTER, background='cyan',
                             font=f'Arial {self.height // 40} bold',
                             text='New game', command=self.new_game,
                             padx=40)
        self.n_game.place(x=self.width * 5 // 6, y=self.height * 19 / 20, anchor=CENTER)

    def new_game(self):
        self.destroy()
        HanoiTower()

    def add_bagel(self, event):
        self.choose_button.configure(command=self.choosing_end_position)
        if self.min_bagel < 1:
            return
        i = event.x // (self.width // 3)
        bagel = self.draw_bagel(bagel_column=i, column_amount=len(self.bagels[i]), bagel_size=self.min_bagel)
        self.bagels[i].append([self.min_bagel, bagel, i])
        self.previous_column = i
        self.min_bagel -= 1

    def choosing_end_position(self):
        # print('Start position selected. Choosing end position')
        # print(self.bagels)
        self.itemconfigure(self.choose_text, text='Choose end position and press "OK"')
        self.update()
        self.unbind('<Button-1>')
        self.bind('<Button-1>', self.draw_end_position)

    def automatically(self):
        # print('automatically')
        self.delete(self.choose_text)
        self.choose_button.destroy()
        self.unbind('<Button-1>')
        for n in range(1, 11-self.min_bagel):
            maximum = self.min_bagel + n
            max_column = 0
            for i, column in enumerate(self.bagels):
                for bagel in column:
                    if bagel[0] == maximum:
                        max_column = i
            if max_column == self.previous_column:
                continue
            temp_column = 3 - max_column - self.previous_column
            if ((self.bagels[temp_column] and self.bagels[temp_column][-1][0] - self.bagels[max_column][-1][0] == 1)
                    or (n == 10 and not self.bagels[temp_column]) and temp_column == self.end_position):
                self._from = max_column
                self._to = temp_column
                # print('Moving without recursion')
                self.move_bagel()
                sleep(0.05)
                max_column = temp_column
            self.auto_moving(fr=self.previous_column, to=max_column, n=n - 1)
            self.previous_column = max_column
        if self.previous_column != self.end_position:
            self.auto_moving(fr=self.previous_column, to=self.end_position, n=10 - self.min_bagel)

    def auto_moving(self, fr, to, n):
        temp = 3 - fr - to
        if n <= 1:
            self._from = fr
            self._to = to
            self.move_bagel()
            return
        self.auto_moving(fr=fr, to=temp, n=n - 1)
        sleep(0.05)
        self._from = fr
        self._to = to
        self.move_bagel()
        sleep(0.05)
        self.auto_moving(fr=temp, to=to, n=n - 1)
        sleep(0.05)

    def gaming(self):
        # print(f"End position - {self.end_position}. It is gaming")
        self.itemconfigure(self.choose_text, text='Move rings from one tower to another. Try to solve this')
        self.unbind('<Button-1>')
        self.bind('<Button-1>', self.get_from_to)
        self.choose_button.configure(command=self.automatically, text='Auto')

    def get_from_to(self, event):
        i = event.x // (self.width // 3)
        if self._from is None:
            if self.bagels[i]:
                self._from = i
                self.itemconfigure(self.bagels[i][-1][1], width=5)
        else:
            self._to = i
            self.move_bagel()

    def move_bagel(self):
        # print(f'Try to move from {self._from} to {self._to}.  ', end='')
        ba = self.bagels
        to = self._to
        fr = self._from
        if fr == to:
            self.itemconfigure(ba[fr][-1][1], width=1)
        elif ba[fr]:
            if not ba[to] or ba[fr][-1][0] < ba[to][-1][0]:
                # moving bagel to respective position
                # print('-->  Ok')
                self.itemconfigure(ba[fr][-1][1], width=1)
                self.coords(ba[fr][-1][1], *self.bagel_coords(to, len(ba[to]), ba[fr][-1][0]))
                self.update()
                ab = ba[fr].pop()
                ab[2] = to
                ba[to].append(ab)
            else:
                # print('-->  Bagel is bigger')
                self.itemconfigure(ba[fr][-1][1], width=1)
                self.itemconfigure(self.text_info, text='Bagel is bigger', fill='red')
                self.update()
                sleep(1)
                self.itemconfigure(self.text_info, text='')
                self.update()
        else:
            # print('-->  There is no bagels')
            self.itemconfigure(self.text_info, text='There is no bagels', fill='red')
            self.update()
            sleep(1)
            self.itemconfigure(self.text_info, text='')
            self.update()
        self.winner()
        self._from = self._to = None

    def winner(self):
        bagels = self.bagels
        i = self.end_position
        if bagels[i] and not bagels[(i + 1) % 3] and not bagels[(i + 2) % 3]:
            # print('You win!!!')
            self.itemconfigure(self.text_info, text='YOU WIN!!!', fill='green')
            self.update()

    def draw_end_position(self, event):
        self.choose_button.configure(command=self.gaming)
        self.end_position = event.x // (self.width // 3)
        # print(self.end_position)
        xc = self.width * (1 / 6 + self.end_position / 3)
        yc = self.height / 2
        w = self.width / 6.1
        h = self.height * 0.4
        if self.end_position_rect is None:
            self.end_position_rect = self.create_rectangle(xc-w, yc-h, xc+w, yc+h, outline='green', width=3)
        else:
            self.coords(self.end_position_rect, xc-w, yc-h, xc+w, yc+h)
        self.update()

    def draw_bagel(self, bagel_column, column_amount, bagel_size):
        coords = self.bagel_coords(bagel_column, column_amount, bagel_size)
        return self.create_rectangle(*coords,
                                     fill=self.colors[bagel_size - 1],
                                     outline='black')

    def bagel_coords(self, bagel_column, column_amount, bagel_size):
        bagel_height = self.height * 0.7 / 10
        bagel_radius = self.width / 30 + bagel_size * self.width / 80
        down_position = self.height * 0.85 - column_amount * bagel_height
        bagel_x_position = self.width * (bagel_column / 3 + 1 / 6)
        return (bagel_x_position - bagel_radius,
                down_position - bagel_height,
                bagel_x_position + bagel_radius,
                down_position)


def main():
    win = Tk()
    win.update()
    width = win.winfo_screenwidth() // 10
    height = win.winfo_screenheight() // 10
    win.geometry(f'{width * 8}x{height * 8}+{width}+{height}')
    HanoiTower()
    win.mainloop()


if __name__ == '__main__':
    main()

orgapp/hannoitower/hannoitower.py
Vskesha/orgApp
ORGAPP_LOGO = """
                                    .o.                             
                                   .888.                            
 .ooooo.  oooo d8b  .oooooooo     .8"888.     oo.ooooo.  oo.ooooo.  
d88' `88b `888""8P 888' `88b     .8' `888.     888' `88b  888' `88b 
888   888  888     888   888    .88ooo8888.    888   888  888   888 
888   888  888     `88bod8P'   .8'     `888.   888   888  888   888 
`Y8bod8P' d888b    `8oooooo.  o88o     o8888o  888bod8P'  888bod8P' 
================== d"     YD ================= 888 ====== 888 ===== 
                   "Y88888P'                  o888o      o888o      
"""

NOTEBOOK_LOGO = """
                          .                     
                        .o8                     
ooo. .oo.    .ooooo.  .o888oo  .ooooo.   
`888P"Y88b  d88' `88b   888   d88' `88b   
 888   888  888   888   888   888ooo888    
 888   888  888   888   888 . 888    .o  
o888o o888o `Y8bod8P'   "888" `Y8bod8P'  
 .o8                           oooo
"888                           `888
 888oooo.   .ooooo.   .ooooo.   888  oooo
 d88' `88b d88' `88b d88' `88b  888 .8P'
 888   888 888   888 888   888  888888.
 888   888 888   888 888   888  888 `88b.
 `Y8bod8P' `Y8bod8P' `Y8bod8P' o888o o888o
"""

ADDRESSBOOK_LOGO = """
                .o8        .o8                                       
               "888       "888                                       
 .oooo.    .oooo888   .oooo888  oooo d8b  .ooooo.   .oooo.o  .oooo.o 
`P  )88b  d88' `888  d88' `888  `888""8P d88' `88b d88(  "8 d88(  "8 
 .oP"888  888   888  888   888   888     888ooo888 `"Y88b.  `"Y88b.  
d8(  888  888   888  888   888   888     888    .o o.  )88b o.  )88b 
`Y888""8o `Y8bod88P" `Y8bod88P" d888b    `Y8bod8P' 8""888P' 8""888P' 

             .o8                           oooo        
            "888                           `888        
             888oooo.   .ooooo.   .ooooo.   888  oooo  
             d88' `88b d88' `88b d88' `88b  888 .8P'   
             888   888 888   888 888   888  888888.    
             888   888 888   888 888   888  888 `88b.  
             `Y8bod8P' `Y8bod8P' `Y8bod8P' o888o o888o 
"""

SORTER_LOGO = """
 .oooooo..o                        .                      
d8P'    `Y8                      .o8                      
Y88bo.       .ooooo.  oooo d8b .o888oo  .ooooo.  oooo d8b 
 `"Y8888o.  d88' `88b `888""8P   888   d88' `88b `888""8P 
     `"Y88b 888   888  888       888   888ooo888  888     
oo     .d8P 888   888  888       888 . 888    .o  888     
8""88888P'  `Y8bod8P' d888b      "888" `Y8bod8P' d888b    
"""
orgapp/logos.py
Vskesha/orgApp
from .address_book import main as address_main
from .bandergoose import main as bandergoose_main
from .hannoitower import main as hannoi_main
from .note_book import main as note_main
from .sorter import clean_folder
from .snake import main as snake_main
from .tictactoe import main as tictactoe_main
from colorama import init as init_colorama, Fore, Back, Style
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles.named_colors import NAMED_COLORS
from prompt_toolkit import prompt
from prompt_toolkit.completion import NestedCompleter


COMMAND_COLOR = Fore.WHITE
TEXT_COLOR = Fore.BLUE
ERROR_COLOR = Fore.RED
ORGAPP_LOGO = """
                                    .o.                             
                                   .888.                            
 .ooooo.  oooo d8b  .oooooooo     .8"888.     oo.ooooo.  oo.ooooo.  
d88' `88b `888""8P 888' `88b     .8' `888.     888' `88b  888' `88b 
888   888  888     888   888    .88ooo8888.    888   888  888   888 
888   888  888     `88bod8P'   .8'     `888.   888   888  888   888 
`Y8bod8P' d888b    `8oooooo.  o88o     o8888o  888bod8P'  888bod8P' 
================== d"     YD ================= 888 ====== 888 ===== 
                   "Y88888P'                  o888o      o888o      
"""


def close_program() -> str:
    print(f'{Fore.GREEN}Goodbye!')
    return 'exiting'


COMMANDS = {
    'abk': address_main,
    'addressbook': address_main,
    'bandergoose': bandergoose_main,
    'exit': close_program,
    'hannoitower': hannoi_main,
    'nbk': note_main,
    'notebook': note_main,
    'quit': close_program,
    'sorter': clean_folder,
    'snake': snake_main,
    'tictactoe': tictactoe_main,
}


def get_command() -> str:
    """Gets and returns str command from user"""
    completer = NestedCompleter.from_nested_dict({command: None for command in COMMANDS.keys()}) 
    while True:
        command = prompt('>>>', completer=completer, lexer=RainbowLexer()).strip().lower()
        if command in COMMANDS:
            return command
        else:
            print(f'{ERROR_COLOR}There is no such command: {COMMAND_COLOR + command}\n'
                  f'{TEXT_COLOR}Try again:{COMMAND_COLOR}')


def main():
    """
    This is start point of our orgApp
    """
    init_colorama()
    while True:
        print_menu()
        try:
            subprogram = COMMANDS[get_command()]
            if subprogram() == 'exiting':
                break
        except Exception:
            print('Something wrong. Sorry ...')


def print_menu():
    """
    Prints initial menu of the program
    """
    print(Fore.GREEN+ Style.BRIGHT + ORGAPP_LOGO)
    print(f'{Fore.CYAN}{"":>35}YOUR FAVORITE ORGANIZER PROGRAM\n')
    print(Style.BRIGHT)
    print(TEXT_COLOR + 'With this app You can deal with your notes and contacts')
    print(f'Use {COMMAND_COLOR}nbk {TEXT_COLOR}or {COMMAND_COLOR}notebook '
          f'{TEXT_COLOR}command to open your notebook manager.')
    print(f'Type {COMMAND_COLOR}abk {TEXT_COLOR}or {COMMAND_COLOR}addressbook '
          f'{TEXT_COLOR}below to manage your contacts.')
    print(f'Also You are able to sort your files with {COMMAND_COLOR}sorter.')
    print(f'{TEXT_COLOR}And if you are tired you can play simple games:')
    print(f'{COMMAND_COLOR}tictactoe, bandergoose, hannoitower '
          f'{TEXT_COLOR}or {COMMAND_COLOR}snake.')
    print(f'exit, quit {TEXT_COLOR}to close the program')


class RainbowLexer(Lexer):
    """
    Lexer for rainbow syntax highlighting.

    This lexer assigns colors to characters based on the rainbow spectrum.
    """

    def lex_document(self, document):
        colors = list(sorted({"Teal": "#028000"}, key=NAMED_COLORS.get))

        def get_line(lineno):
            return [
                (colors[i % len(colors)], c)
                for i, c in enumerate(document.lines[lineno])
            ]

        return get_line


if __name__ == '__main__':
    main()

orgapp/main.py
Vskesha/orgApp
from .note_book import main


if __name__ == '__main__':
    main()

orgapp/note_book/__init__.py
Vskesha/orgApp
from .note_book import main


if __name__ == '__main__':
    main()

orgapp/note_book/__main__.py
Vskesha/orgApp
import json


class Note:
    """Represents a note with a title and content."""

    def __init__(self, title: str, content: str, tags: set = None):
        """
        Initializes a new Note.

        Args:
            title (str): The title of the note.
            content (str): The content of the note.
            tags (list):  The tags of the note.
        """
        self.__title = None
        self.__content = None
        self.__tags = None
        self.title = title
        self.content = content
        self.tags = tags
    
    @property
    def title(self):
        return self.__title

    @title.setter
    def title(self, new_title):
        if new_title.strip():   
            self.__title = new_title
        else:
            raise ValueError("Title cannot be empty.")

    @property
    def content(self):
        return self.__content

    @content.setter
    def content(self, new_content):
        if new_content.strip():   
            self.__content = new_content
        else:
            raise ValueError("Content cannot be empty.")

    @property
    def tags(self):
        return self.__tags

    @tags.setter
    def tags(self, new_tags):
        self.__tags = new_tags if len(new_tags) else set()


class NoteManager:
    """Manages a collection of notes."""

    def __init__(self):
        """Initializes a new NoteManager with an empty list of notes."""
        self.notes = []

    def add_note(self, title: str, content: str, tags: set = None) -> None:
        """
        Adds a new note to the collection.

        Args:
            title (str): The title of the note.
            content (str): The content of the note.
            tags(set(str)): list with tag words
        """
        note = Note(title, content, tags)
        self.notes.append(note)

    def add_notes(self, other) -> None:
        """
        Extends self.notes with notes from other NoteManager object
        """
        titles = set(note.title for note in self.notes)
        for note in other.notes:
            if note.title not in titles:
                self.notes.append(note)

    def add_tag_to_note(self, title: str, tag: str) -> bool:
        """
        Adds the tag to the note found by title.

        Args:
            title (str): The title of the note.
            tag (str): The tag of the note.
        """        
        
        for note in self.notes:
            if note.title == title:
                note.tags.add(tag)


    def delete_note(self, title: str) -> bool:
        """
        Deletes the note found by title.

        Args:
            title (str): The title of the note.
        """

        for note in self.notes:
            if note.title == title:
                self.notes.remove(note)


    def delete_tag_from_note(self, title: str, tag: str) -> bool:
        """
        Deletes tag from the note found by title.

        Args:
            title (str): The title of the note.
            tag (str): The tag of the note.
        """

        for note in self.notes:
            if note.title == title:
                note.tags.discard(tag)
   

    def edit_note(self, title: str, content: str) -> bool:
        """
        Edits the note found by title.

        Args:
            title (str): The title of the note.
            content (str): New content.
        """

        for note in self.notes:
            if note.title == title:
                note.content = content


    @classmethod
    def load_notes_from_json(cls, filename: str):
        """
        Loads notes from a JSON file and replaces the current collection.
        returns NoteManager object
        Args:
            filename (str): The name of the JSON file to load notes from.
        """
        new_note_book = NoteManager()
        try:
            with open(filename, "r", encoding="utf-8") as file:
                data = json.load(file)
                for note_data in data:
                    new_note_book.add_note(note_data["title"], note_data["content"], set(note_data["tags"]))
        except (FileNotFoundError, KeyError):
            pass

        return new_note_book

    def get_all_notes(self) -> list[Note]:
        """ Returns a list of all notes."""
        return self.notes


    def get_titles(self) -> list[str]:
        """Returns a list of all titles"""
        return [note.title for note in self.notes]


    def save_notes_to_json(self, filename: str) -> None:
        """
        Saves the notes to a JSON file.

        Args:
            filename (str): The name of the JSON file to save the notes to.
        """
        data = []
        for note in self.notes:
            data.append({"title": note.title, "content": note.content, "tags": list(note.tags)})

        with open(filename, "w", encoding="utf-8") as file:
            json.dump(data, file, ensure_ascii=False, indent=4)

    def search_by_tag(self, tag: str) -> list[Note]:
        """
        Searches for notes by tag.

        Args:
            tag (str): The tag to search for.

        Returns:
            list: A list of notes having the tag.
        """
        results = []
        for note in self.notes:
            if tag in note.tags:
                results.append(note)
        return results    
    
    def search_notes(self, keyword: str) -> list[Note]:
        """
        Searches for notes containing a specific keyword.

        Args:
            keyword (str): The keyword to search for.

        Returns:
            list: A list of notes containing the keyword in their title or content.
        """
        results = []
        for note in self.notes:
            if keyword in note.title or keyword in note.content:
                results.append(note)
        return results

    @classmethod
    def string_from_list(cls, notes: list[Note]) -> str:
        """makes multiline string from given list of Notes"""
        result = ""
        for i, note in enumerate(notes, 1):
            result += f"{i:>3}. Title: {note.title}\n"
            result += f"     Content: {note.content}\n"
            if note.tags:
                result += f"     Tags: {', '.join(note.tags)}\n"
            result += "\n"
        return result

orgapp/note_book/classes_nb.py
Vskesha/orgApp
from .classes_nb import NoteManager
from colorama import init as init_colorama, Fore, Back, Style
from functools import wraps
from pathlib import Path
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles.named_colors import NAMED_COLORS
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit import prompt
from faker import Faker


FILE_PATH = Path.home() / "orgApp" / "notes.json"  # for working on different filesystems
FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
NOTE_MANAGER = NoteManager.load_notes_from_json(str(FILE_PATH))  # Create an object to manage notes from file
NOTEBOOK_LOGO = """
                          .                     
                        .o8                     
ooo. .oo.    .ooooo.  .o888oo  .ooooo.   
`888P"Y88b  d88' `88b   888   d88' `88b   
 888   888  888   888   888   888ooo888    
 888   888  888   888   888 . 888    .o  
o888o o888o `Y8bod8P'   "888" `Y8bod8P'  
 .o8                           oooo
"888                           `888
 888oooo.   .ooooo.   .ooooo.   888  oooo
 d88' `88b d88' `88b d88' `88b  888 .8P'
 888   888 888   888 888   888  888888.
 888   888 888   888 888   888  888 `88b.
 `Y8bod8P' `Y8bod8P' `Y8bod8P' o888o o888o
"""


def command_parser(user_input: str) -> tuple[callable, str]:
    """
    fetches and returns proper handler and argument of this handler
    from user_input accordingly to the COMMANDS
    :param user_input: str which must start with command followed by name and phone number if needed
    :return: tuple of function and str argument
    """
    if not user_input:
        raise IndexError("Nothing was entered ...")

    func, data = None, []
    lower_input_end_spaced = user_input.lower() + " "
    for command in COMMANDS:
        if lower_input_end_spaced.startswith(command + " "):
            func = COMMANDS[command]
            data = user_input[len(command) + 1:].strip()

    if not func:
        raise ValueError(f"There is no such command {user_input.split()[0]}")

    return func, data


def handle_add_note(args: str) -> str:
    """adds note to your NoteBook"""
    title = input("Enter note title: ")
    if not title.strip():
        raise ValueError('Title cannot be empty')
    if title in NOTE_MANAGER.get_titles():
        raise ValueError(f'Note with title "{title}" already exist')
    content = input("Enter note text: ")
    tags = input("Enter note tags: ")
    NOTE_MANAGER.add_note(title, content, set(tags.split()))
    return "Note added successfully."


def handle_add_tags(agrs: str) -> str:
    """add tag to note"""
    title = input("Enter note title: ")
    if title not in NOTE_MANAGER.get_titles():
        raise KeyError(f'Note with title "{title}" not found')
    tags = input("Enter tag: ").split()
    result = False
    for tag in tags:
        result = NOTE_MANAGER.add_tag_to_note(title, tag)
    return "Tags added successfully."
    

def handle_delete_note(args: str) -> str:
    """deletes note from NoteBook"""
    title = input("Enter note title: ")
    if title not in NOTE_MANAGER.get_titles():
        raise KeyError(f'Note with title "{title}" not found')
    NOTE_MANAGER.delete_note(title)
    return "Note deleted successfully."


def handle_delete_tag_from_note(args: str) -> str:
    """deletes tag from note in NoteBook"""
    title = input("Enter note title: ")
    if title not in NOTE_MANAGER.get_titles():
        raise KeyError(f'Note with title "{title}" not found')
    tag = input("Enter tag: ")
    NOTE_MANAGER.delete_tag_from_note(title, tag)
    return "Tag deleted successfully."


def handle_edit_note(args: str) -> str:
    """edit a note content in the NoteBook"""
    title = input("Enter note title: ")
    if title not in NOTE_MANAGER.get_titles():
        raise KeyError(f'Note with title "{title}" not found')
    content = input("Enter new note text: ")
    NOTE_MANAGER.edit_note(title, content)
    return "Note edited successfully."


def handle_exit(args: str) -> str:
    """exits the program"""
    return handle_save_notes("") + "\nGoodbye!"


def handle_fill_with_random_notes(args: str) -> str:
    """adds fdke data to the notebook"""
    count_notes = int(input("Enter the number of notes: "))
    faker = Faker()
    for i in range(count_notes):
        NOTE_MANAGER.add_note(title=faker.name(), content=faker.sentence(nb_words=15), tags=set(faker.words(2 + i % 2)))
    return "Random notes added successfully."     


def handle_help(args: str) -> str:
    """outputs this list of commands"""
    print_menu()
    return ''


def handle_load_notes(args: str) -> str:
    """loads notes from the given file"""
    filename = args if args else input("Enter the filename to load: ")
    new_notes = NoteManager.load_notes_from_json(filename)
    NOTE_MANAGER.add_notes(new_notes)
    return f"Notes loaded from {filename}"


def handle_save_notes(args: str) -> str:
    """saves notes to file"""
    FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
    NOTE_MANAGER.save_notes_to_json(str(FILE_PATH))
    return f"Notes saved to {str(FILE_PATH)}"


def handle_find_by_tag(args: str) -> str:
    """returns notes with given tags"""
    tag = args.split()[0] if args else input("Enter a tag to search: ")
    search_results = NOTE_MANAGER.search_by_tag(tag)
    if search_results:
        result_str = "Search results:\n\n"
        result_str += NoteManager.string_from_list(search_results)
        return result_str
    else:
        return "No notes found for this tag." 
    

def handle_search_notes(args: str) -> str:
    """returns notes with given keyword"""
    keyword = args[0] if args else input("Enter a keyword to search: ")
    search_results = NOTE_MANAGER.search_notes(keyword)
    if search_results:
        result_str = "Search results:\n\n"
        result_str += NoteManager.string_from_list(search_results)
        return result_str
    else:
        return "No notes found for this keyword."


def handle_view_all_notes(args: str) -> str:
    """displays all notes."""
    all_notes = NOTE_MANAGER.get_all_notes()
    if all_notes:
        result_str = "All notes:\n\n"
        result_str += NoteManager.string_from_list(all_notes)
        return result_str
    else:
        return " There are no notes."


def input_error(func):
    """
    A decorator wrapper for error handling.

    Args:
        func (callable): The function to wrap with error handling.

    Returns:
        callable: The wrapped function with error handling.
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            result = func(*args, **kwargs)
            return result
        except IndexError as e:
            print(Fore.RED, 'Not enough data.', str(e))
        except ValueError as e:
            print(Fore.RED, 'Wrong value.', str(e))
        except KeyError as e:
            print(Fore.RED, 'Wrong key.', str(e)[1:-1])
        except TypeError as e:
            print(Fore.RED, 'Wrong type of value.', str(e))
        except FileNotFoundError as e:
            print(Fore.RED, e)
        except Exception as e:
            print(Fore.RED, e)
    return wrapper


@input_error
def main_cycle() -> bool:
    """
    return True if it needs to stop program. False otherwise.
    """
    completer = NestedCompleter.from_nested_dict({command: None for command in COMMANDS.keys()})
    user_input = prompt('>>> ', completer=completer, lexer=RainbowLexer())
    func, argument = command_parser(user_input)
    result = func(argument)
    print(Fore.WHITE, result)
    return result.endswith('Goodbye!')


def main():
    prepare()
    while True:
        if main_cycle():
            break


def prepare() -> None:
    """
    Displays initial information to the user
    :return: None
    """
    init_colorama()
    print(Fore.BLUE + Style.BRIGHT + NOTEBOOK_LOGO)
    print(Fore.CYAN + "Welcome to your note-taking app!")
    print()
    print_menu()  # Display the menu with commands


def print_menu():
    print(Fore.GREEN, "Available commands:")
    separator = '|--------------------------------|-----------------------------------------|'
    print(separator, f'\n|           Commands             |     Action{" ":30}|\n', separator, sep='')
    for func, commands in COMMANDS_LISTS.items():  # generic way when we add new action
        print(f"| {Fore.WHITE} {', '.join(commands):<30}{Fore.GREEN}| {func.__doc__:<40}|")
    print(separator, '\n')


# Map of commands and their corresponding handler functions
COMMANDS_LISTS = {
    handle_add_note: ["add", 'plus'],
    handle_add_tags: ["add_tags"],
    handle_fill_with_random_notes: ["add_fake_notes"],
    handle_view_all_notes: ['all', 'all_notes', 'view'],
    handle_edit_note: ['edit'],
    handle_exit: ["bye", 'close', 'exit', 'goodbye'],
    handle_delete_note: ["delete"],
    handle_delete_tag_from_note: ["delete_tag"],
    handle_find_by_tag: ['tag', "find_tag", 'search_tag'],
    handle_help: ['help'],
    handle_load_notes: ["load"],
    handle_save_notes: ['save'],
    handle_search_notes: ['find', 'search'],
}
COMMANDS = {command: func for func, commands in COMMANDS_LISTS.items() for command in commands}


class RainbowLexer(Lexer):
    """
    Lexer for rainbow syntax highlighting.

    This lexer assigns colors to characters based on the rainbow spectrum.
    """

    def lex_document(self, document):
        colors = list(sorted({"Teal": "#028000"}, key=NAMED_COLORS.get))

        def get_line(lineno):
            return [
                (colors[i % len(colors)], c)
                for i, c in enumerate(document.lines[lineno])
            ]

        return get_line


if __name__ == "__main__":
    main()

orgapp/note_book/note_book.py
Vskesha/orgApp
import pygame
import random


class Snake:
    def __init__(self):
        self.dis_width = 600
        self.dis_height = 400
        self.snake_speed = 1
        self.speed_adjust = False
        self.snake_block = 25
        self.snake_border = round(self.snake_block / 20)
        self.size_of_food = 20
        self.border = round(30 / self.snake_block) * self.snake_block

        self.white = (255, 255, 255)
        self.yellow = 'yellow'
        self.black = (0, 0, 0)
        self.red = (255, 0, 0)
        self.green = (0, 255, 0)
        self.blue = (0, 0, 255)
        pygame.init()
        self.dis = pygame.display.set_mode((self.dis_width, self.dis_height))
        pygame.display.set_caption('Змійка by VsKesha')
        self.clock = pygame.time.Clock()

    def get_food(self, snake):
        x = round(random.randrange(self.border, self.dis_width - self.snake_block - self.border) / self.snake_block) * self.snake_block
        y = round(random.randrange(self.border, self.dis_height - self.snake_block - self.border) / self.snake_block) * self.snake_block
        for x1, y1 in snake:
            if x == x1 and y == y1:
                x, y = self.get_food(snake)
        return [x, y]

    def your_score(self, score=0):
        score_font = pygame.font.SysFont("comicsansms", 35)
        value = score_font.render("Рахунок: " + str(score) + "    P - пауза", True, self.yellow)
        self.dis.blit(value, [0, 0])
        pygame.display.update()

    def your_speed(self, speed=1):
        speed_font = pygame.font.SysFont("comicsansms", 35)
        value = speed_font.render("Швидкість: " + str(speed), True, self.yellow)
        self.dis.blit(value, [self.dis_width - 180, 0])
        pygame.display.update()

    def show_message(self, msg, color, coords=None, font_size=30):
        coords = coords or (self.dis_width / 7, self.dis_height / 3)
        font_style = pygame.font.SysFont("comicsansms", font_size)
        mesg = font_style.render(msg, True, color)  # .set_alpha(100)
        self.dis.blit(mesg, coords)
        pygame.display.update()

    def refresh_screen(self):
        # makes grid on the screen
        self.dis.fill('black')
        for i in range(self.border + self.snake_block, self.dis_width - self.snake_block, self.snake_block):
            pygame.draw.line(self.dis, 'grey10', [i, self.border], [i, self.dis_height - self.border], self.snake_border * 2)
        for i in range(self.border + self.snake_block, self.dis_height - self.snake_block, self.snake_block):
            pygame.draw.line(self.dis, 'grey10', [self.border, i], [self.dis_width - self.border, i], self.snake_border * 2)
        border_points = [
            [self.border, self.border],
            [self.dis_width - self.border - self.snake_border, self.border],
            [self.dis_width - self.border - self.snake_border, self.dis_height - self.border - self.snake_border],
            [self.border, self.dis_height - self.border - self.snake_border]]
        pygame.draw.lines(self.dis, (150, 150, 150), True, border_points, self.snake_border * 2)

    def draw_food(self, foodx, foody):
        pygame.draw.circle(self.dis, 'yellow', [foodx + round(self.snake_block / 2), foody + round(self.snake_block / 2)],
                           self.size_of_food / 2)
        pygame.draw.circle(self.dis, self.blue, [foodx + round(self.snake_block / 2), foody + round(self.snake_block / 2)], self.size_of_food / 3)

    def draw_snake(self, snake, direct):
        # draw tail
        if len(snake) > 1:
            tail = snake[0]
            previos = snake[1]
            t1 = []
            t2 = []
            t3 = []
            if tail[0] > previos[0]:  # tail goes left
                t1 = [tail[0] + 1, tail[1] + 1]
                t2 = [tail[0] + 1, tail[1] + self.snake_block - 1]
                t3 = [tail[0] + self.snake_block, tail[1] + round(self.snake_block / 2)]
            if tail[0] < previos[0]:  # tail goes right
                t1 = [tail[0] + self.snake_block - 1, tail[1] + 1]
                t2 = [tail[0] + self.snake_block - 1, tail[1] + self.snake_block - 1]
                t3 = [tail[0], tail[1] + round(self.snake_block / 2)]
            else:
                if tail[1] > previos[1]:  # tail goes up
                    t1 = [tail[0] + 1, tail[1] + 1]
                    t2 = [tail[0] + self.snake_block - 1, tail[1] + 1]
                    t3 = [tail[0] + round(self.snake_block / 2), tail[1] + self.snake_block]
                elif tail[1] < previos[1]:  # tail goes down
                    t1 = [tail[0] + 1, tail[1] + self.snake_block - 1]
                    t2 = [tail[0] + self.snake_block - 1, tail[1] + self.snake_block - 1]
                    t3 = [tail[0] + round(self.snake_block / 2), tail[1]]
            points = [t1, t2, t3]
            pygame.draw.polygon(self.dis, self.green, points)
            # pygame.draw.polygon

        # draw body of snake
        for x in snake[1:-1]:
            pygame.draw.rect(self.dis, self.green, [x[0] + 1, x[1] + 1, self.snake_block - 2, self.snake_block - 2])

        # draw_head(snake)
        head = snake[-1]
        center = [head[0] + round(self.snake_block / 2), head[1] + round(self.snake_block / 2)]
        head_radius = self.snake_block / 1.4
        pygame.draw.circle(self.dis, self.green, [center[0], center[1]], head_radius)

        # draw_eyes and tongue
        eyes_color = 'white'
        tongue_color = 'red'
        eye_size = head_radius * 0.35
        distance_forward = round(head_radius * 0.4)
        distance_between_eyes = round(head_radius * 0.5)
        tongue_length = round(head_radius * 0.3)
        tongue_thickness = round(head_radius * 0.05)
        eye_coord1 = []
        eye_coord2 = []
        tongue_coord = []
        if not direct:
            direct = 'right'
        if direct == 'up':
            eye_coord1 = [center[0] - distance_between_eyes, center[1] - distance_forward]
            eye_coord2 = [center[0] + distance_between_eyes, center[1] - distance_forward]
            tongue_coord = [center[0] - tongue_thickness, center[1] - head_radius - tongue_length, tongue_thickness * 2,
                            tongue_length]
        elif direct == 'down':
            eye_coord1 = [center[0] - distance_between_eyes, center[1] + distance_forward]
            eye_coord2 = [center[0] + distance_between_eyes, center[1] + distance_forward]
            tongue_coord = [center[0] - tongue_thickness, center[1] + head_radius, tongue_thickness * 2, tongue_length]
        elif direct == 'left':
            eye_coord1 = [center[0] - distance_forward, center[1] + distance_between_eyes]
            eye_coord2 = [center[0] - distance_forward, center[1] - distance_between_eyes]
            tongue_coord = [center[0] - head_radius - tongue_length, center[1] - tongue_thickness, tongue_length,
                            tongue_thickness * 2]
        elif direct == 'right':
            eye_coord1 = [center[0] + distance_forward, center[1] - distance_between_eyes]
            eye_coord2 = [center[0] + distance_forward, center[1] + distance_between_eyes]
            tongue_coord = [center[0] + head_radius, center[1] - tongue_thickness, tongue_length, tongue_thickness * 2]
        pygame.draw.circle(self.dis, eyes_color, eye_coord1, eye_size)
        pygame.draw.circle(self.dis, 'black', eye_coord1, eye_size / 2)
        pygame.draw.circle(self.dis, eyes_color, eye_coord2, eye_size)
        pygame.draw.circle(self.dis, 'black', eye_coord2, eye_size / 2)
        pygame.draw.rect(self.dis, tongue_color, tongue_coord)
        # pygame.draw.ellipse

    def game_menu(self):
        global snake_speed
        global speed_adjust
        self.refresh_screen()
        self.your_score()
        self.your_speed()
        menu_bg = pygame.Surface([self.dis_width, self.dis_height])  # .set_alpha(50)
        pygame.draw.rect(menu_bg, 'grey20', [0, 0, self.dis_width, self.dis_height])
        menu_bg.set_alpha(200)
        self.dis.blit(menu_bg, [0, 0])
        self.show_message("ЗМІЙКА", "grey5", [self.dis_width / 2 - 100, self.border], 80)
        self.show_message("by vskesha", "dark green", [self.dis_width / 2 + 100, self.border + 50], 30)
        x = self.dis_width / 2 - 200
        y = self.dis_height / 2
        self.show_message("Вибери швидкість:", 'black', [x, y], 50)
        self.show_message('"1" - "9" вибрати швидкість', 'black', [x, y + 50], 30)
        self.show_message('"A" - автоматично', 'black', [x, y + 80], 30)

        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.game_close()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_q:
                        self.game_close()
                    elif event.key == pygame.K_a:
                        speed_adjust = True
                        break
                    elif event.key == pygame.K_1:
                        snake_speed = 1
                        break
                    elif event.key == pygame.K_2:
                        snake_speed = 2
                        break
                    elif event.key == pygame.K_3:
                        snake_speed = 3
                        break
                    elif event.key == pygame.K_4:
                        snake_speed = 4
                        break
                    elif event.key == pygame.K_5:
                        snake_speed = 5
                        break
                    elif event.key == pygame.K_6:
                        snake_speed = 6
                        break
                    elif event.key == pygame.K_7:
                        snake_speed = 7
                        break
                    elif event.key == pygame.K_8:
                        snake_speed = 8
                        break
                    elif event.key == pygame.K_9:
                        snake_speed = 9
                        break
            else:
                continue
            break

    def game_pause(self):
        # print("GAME PAUSED")
        paused = True
        x = self.dis_width / 4
        y = self.dis_height / 3
        self.show_message("ПАУЗА!", self.red, [x, y], 50)
        self.show_message("Нажми С для продовження", self.red, [x, y + 65], 35)
        self.show_message("Нажми Q щоб вийти", self.red, [x, y + 100], 35)

        while paused:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.game_close()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_c:
                        # print("GAME RESUMED")
                        paused = False
                    if event.key == pygame.K_q:
                        self.game_close()

    def game_close(self):
        pygame.quit()
        quit()

    def game_over(self):
        while True:
            x = self.dis_width / 4
            y = self.dis_height / 3
            self.show_message("ПОРАЗКА!", self.red, [x, y], 50)
            self.show_message("Нажми С - нова гра", self.red, [x, y + 65], 35)
            self.show_message("Нажми Q - вийти", self.red, [x, y + 100], 35)

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.game_close()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_q:
                        self.game_close()
                    if event.key == pygame.K_c:
                        self.gameloop()

    def gameloop(self):
        # getting start position of the snake
        x1 = round(self.dis_width / 2 / self.snake_block) * self.snake_block
        y1 = round(self.dis_height / 2 / self.snake_block) * self.snake_block
        direction = 'right'
        snake_List = []
        Length_of_snake = 3
        Score = 0
        foodx, foody = self.get_food(snake_List)
        global snake_speed
        global speed_adjust
        self.game_menu()

        while True:
            for event in pygame.event.get():
                # print(event)
                if event.type == pygame.QUIT:
                    self.game_close()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_p:
                        self.game_pause()
                    elif event.key == pygame.K_q:
                        self.game_close()
                    elif event.key == pygame.K_LEFT and direction != 'right':
                        direction = 'left'
                    elif event.key == pygame.K_RIGHT and direction != 'left':
                        direction = 'right'
                    elif event.key == pygame.K_UP and direction != 'down':
                        direction = 'up'
                    elif event.key == pygame.K_DOWN and direction != 'up':
                        direction = 'down'

            if direction == 'left':
                x1 -= self.snake_block
            elif direction == 'right':
                x1 += self.snake_block
            elif direction == 'up':
                y1 -= self.snake_block
            elif direction == 'down':
                y1 += self.snake_block

            if x1 >= self.dis_width - self.border or x1 < self.border or y1 >= self.dis_height - self.border or y1 < self.border:
                self.game_over()

            snake_head = [x1, y1]
            snake_List.append(snake_head)
            if len(snake_List) > Length_of_snake:
                del snake_List[0]

            for x in snake_List[:-1]:
                if x == snake_head:
                    self.game_over()

            self.refresh_screen()
            self.draw_food(foodx, foody)
            self.draw_snake(snake_List, direction)
            self.your_score(Score)
            self.your_speed(snake_speed)

            if x1 == foodx and y1 == foody:
                self.show_message("Нямм", 'yellow', [foodx + self.snake_block, foody + self.snake_block])
                foodx, foody = self.get_food(snake_List)
                Length_of_snake += 1
                Score += snake_speed
            if self.speed_adjust:
                snake_speed = 1 + (Length_of_snake - 3) // 10
            self.clock.tick(snake_speed)


def main():
    snake = Snake()
    snake.gameloop()


if __name__ == '__main__':
    main()

orgapp/snake/snake.py
Vskesha/orgApp
from .sorter import clean_folder


if __name__ == '__main__':
    clean_folder()

orgapp/sorter/__init__.py
Vskesha/orgApp
from .sorter import clean_folder


if __name__ == '__main__':
    clean_folder()

orgapp/sorter/__main__.py
Vskesha/orgApp
from colorama import init as init_colorama, Fore, Back, Style
from pathlib import Path
import re
import shutil
import sys

CYRYLIC = 'абвгґдеєжзиіїйклмнопрстуфхцчшщьюяёъы'
LATIN = (
    'a', 'b', 'v', 'h', 'g', 'd', 'e', 'ye', 'zh', 'z', 'y', 'i',
    'yi', 'y', 'k', 'l', 'm', 'n', 'o', 'p', 'r', 's', 't', 'u',
    'f', 'kh', 'ts', 'ch', 'sh', 'sch', '', 'yu', 'ya', 'yo', '', 'y'
)

TRANS = {}
for c, l in zip(CYRYLIC, LATIN):
    TRANS[ord(c)] = l
    TRANS[ord(c.upper())] = l.upper()


IMAGES = []
VIDEO = []
DOCS = []
MUSIC = []
ARCHIVES = []
MY_OTHER = []

REGISTERED_EXTENSIONS = {
    'JPEG': IMAGES, 'PNG': IMAGES, 'JPG': IMAGES, 'SVG': IMAGES,
    'AVI': VIDEO, 'MP4': VIDEO, 'MOV': VIDEO, 'MKV': VIDEO,
    'DOC': DOCS, 'DOCX': DOCS, 'TXT': DOCS, 'PDF': DOCS, 'XLSX': DOCS, 'PPTX': DOCS,
    'MP3': MUSIC, 'OGG': MUSIC, 'WAV': MUSIC, 'AMR': MUSIC,
    'ZIP': ARCHIVES, 'GZ': ARCHIVES, 'TAR': ARCHIVES,
}

FOLDERS = []
KNOWN_EXTENSIONS = set()
UNKNOWN_EXTENSIONS = set()
SORTER_LOGO = """
 .oooooo..o                        .                     
d8P'    `Y8                      .o8                     
Y88bo.       .ooooo.  oooo d8b .o888oo  .ooooo.  oooo d8b
 `"Y8888o.  d88' `88b `888""8P   888   d88' `88b `888""8P
     `"Y88b 888   888  888       888   888ooo888  888    
oo     .d8P 888   888  888       888 . 888    .o  888    
8""88888P'  `Y8bod8P' d888b      "888" `Y8bod8P' d888b   
"""


def get_extension(file: Path) -> str:
    """
    returns uppercase extension of the given 'file'. This extension then
    will be used for determining the type of file (e.g. MUSIC, VIDEO)
    @param file: Path
    @return: str uppercase extension
    """
    return file.suffix[1:].upper()


def handle_archive(archive: Path, target_folder: Path) -> None:
    """
    unpacks given 'archive' to 'target_folder / sub_folder'
    (sub_folder named the same as archive without extension)
    Creates folders if they don't exist
    """
    if not target_folder.exists():
        target_folder.mkdir(exist_ok=True, parents=True)
    folder_for_file = target_folder / normalize(archive.name.replace(archive.suffix, ''))
    folder_for_file.mkdir(exist_ok=True, parents=True)
    try:
        shutil.unpack_archive(archive, folder_for_file)
    except shutil.ReadError:
        print('Cannot unpack archive')
        folder_for_file.rmdir()
    archive.unlink()


def handle_file(file: Path, target_folder: Path) -> None:
    """
    Moves given 'file' to 'target_folder'. Creates 'target_folder' if it doesn't exist
    """
    if not target_folder.exists():
        target_folder.mkdir(exist_ok=True, parents=True)
    file.replace(target_folder / normalize(file.name))


def handle_folder(folder: Path) -> None:
    """
    removes given 'folder'
    """
    try:
        folder.rmdir()
    except OSError:
        print(f'Cannot remove folder "{folder}"')


def normalize(file_name: str) -> str:
    """
    makes transliteration of the given file_name to latin characters
    and replace all non-alphanumeric characters with '_'
    :param file_name: str, old file name
    :return: new file name
    """
    latin_name = file_name.translate(TRANS)
    parts = latin_name.rsplit('.', 1)
    parts[0] = re.sub(r'\W', '_', parts[0])
    return '.'.join(parts)


def sort_folder(folder: Path) -> None:
    """
    sorts files in the given 'folder' according to the REGISTERED_EXTENSIONS in 'file_parser.py'
    """
    traverse(folder)

    for file in IMAGES:
        handle_file(file, folder / 'images')

    for file in VIDEO:
        handle_file(file, folder / 'video')

    for file in DOCS:
        handle_file(file, folder / 'documents')

    for file in MUSIC:
        handle_file(file, folder / 'audio')

    for file in MY_OTHER:
        handle_file(file, folder / 'my_other')

    for archive in ARCHIVES:
        handle_archive(archive, folder / 'archives')

    for folder in FOLDERS[::-1]:
        handle_folder(folder)


def traverse(folder: Path) -> None:
    """
    recursively scans given 'folder' and:
      - adds all directories' paths to 'FOLDERS'
      - adds files' paths to proper lists (IMAGES, VIDEO, DOCS, MUSIC, ARCHIVES or MY_OTHER)
        based on file extension
      - adds all encountered extensions to KNOWN_EXTENSIONS or UNKNOWN_EXTENSIONS
        according to REGISTERED_EXTENSIONS
    @param folder: str - path to existing directory to parse
    @return: None
    """
    for element in folder.iterdir():
        if element.is_dir():
            if element.name in ('archives', 'video', 'audio', 'documents', 'images', 'my_other'):
                continue
            FOLDERS.append(element)
            traverse(element)
        else:
            ext = get_extension(element)
            if not ext:
                MY_OTHER.append(element)
            elif ext in REGISTERED_EXTENSIONS:
                KNOWN_EXTENSIONS.add(ext)
                container = REGISTERED_EXTENSIONS[ext]
                container.append(element)
            else:
                UNKNOWN_EXTENSIONS.add(ext)
                MY_OTHER.append(element)


def clean_folder():
    prepare()
    folder_to_sort = sys.argv[1] if len(sys.argv) > 1 else ''
    while True:
        path = Path(folder_to_sort).resolve()
        if not path.exists() or not path.is_dir():
            folder_to_sort = input(f'\nThere is no such folder "{folder_to_sort}"\n'
                                   f'Please enter the path to the existing folder\n'
                                   f'(enter nothing to sort current folder): {Fore.WHITE}')
        else:
            confirmation = input(
                f'\n{Fore.GREEN}Do You really want to sort {"this" if folder_to_sort else "current"} folder:\n"{path}"?\n'
                f'{Fore.RED}!!! CHANGES ARE IRREVERSIBLE !!! \n'
                f'{Fore.GREEN}type {Fore.WHITE}y / n {Fore.GREEN} or enter another path: {Fore.WHITE}')
            if confirmation.lower() == 'n':
                return
            if confirmation.lower() == 'y':
                break
            folder_to_sort = confirmation

    print(f'\nStart in folder "{path}"')
    sort_folder(path)
    print('Done')


def prepare():
    init_colorama()
    print(Fore.BLUE + Style.BRIGHT + SORTER_LOGO)
    print(f" {Fore.CYAN}                    Welcome to your sorting app!")


if __name__ == '__main__':
    clean_folder()

orgapp/sorter/sorter.py
Vskesha/orgApp
from .tictactoe import main


if __name__ == '__main__':
    main()

orgapp/tictactoe/__init__.py
Vskesha/orgApp
from .tictactoe import main


if __name__ == '__main__':
    main()

orgapp/tictactoe/__main__.py
Vskesha/orgApp
import tkinter
import random
import time


class TicTacToe(tkinter.Canvas):
    def __init__(self, window, width=300):
        self.width = width
        super().__init__(window, width=self.width, height=self.width, background='grey70')
        self.state = [None, None, None, None, None, None, None, None, None]
        self.empty_squares = [0, 1, 2, 3, 4, 5, 6, 7, 8]
        self.win_combinations = ((0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6))
        self.colors = (('brown1', 'brown2', 'coral', 'coral3', 'OliveDrab1', 'orange red'),
                       ('cyan', 'cyan3', 'cornflower blue', 'DarkBlue', 'dark cyan', 'navy'))
        self.get_colors()
        self.bind('<Button-1>', self.click)
        self.game_menu = True
        self.game_running = True
        self.ready = True
        self.game_mode = ''
        self.first_move_x = 1  # 1 - first move x; -1 - first move o
        self.last_win = ''
        self.win_table = {'x_win': 0, 'o_win': 0, 'draw': 0}

    def click(self, event):
        if self.game_menu == True:
            w = self.width / 3
            h = self.width / 10
            n = self.width / 4
            if event.x > 2 * n - w and event.x < 2 * n + w:
                if event.y > n - h and event.y < n + h:
                    self.game_mode = 'comp hard'
                elif event.y > 2 * n - h and event.y < 2 * n + h:
                    self.game_mode = 'comp easy'
                elif event.y > 3 * n - h and event.y < 3 * n + h:
                    self.game_mode = 'person person'
            if self.game_mode:
                self.first_move_x = 1
                self.last_win = ''
                self.win_table = {'x_win': 0, 'o_win': 0, 'draw': 0}
                self.start_new_game()
                self.game_menu = False
        elif self.game_running and self.ready:
            x = self.width / 2
            y = self.width * 30 / 31
            h = self.width / 30
            w = h * 3
            if x - w < event.x < x + w and y - h < event.y < y + h:
                self.game_menu = True
                self.draw_menu()
            else:
                column = event.x // round(self.width / 3)
                row = event.y // round(self.width / 3)
                i = row * 3 + column
                if self.state[i] is None:
                    if self.game_mode == 'person person':
                        if self.first_move_x == 1:
                            if len(self.empty_squares) % 2:
                                self.state[i] = 'x'
                            else:
                                self.state[i] = 'o'
                        if self.first_move_x == -1:
                            if len(self.empty_squares) % 2:
                                self.state[i] = 'o'
                            else:
                                self.state[i] = 'x'
                    else:
                        self.state[i] = 'x'
                    self.empty_squares.remove(i)
                    self.add_figure(column, row)
                    self.get_winner()
                    if not self.last_win and self.game_mode != 'person person':
                        self.bot_move()
        elif self.ready:
            self.start_new_game()

    def bot_move(self):
        if self.empty_squares:
            i = self.best_move()
        else:
            return
        self.state[i] = 'o'
        self.empty_squares.remove(i)
        column = i % 3
        row = i // 3
        self.add_figure(column, row)
        self.get_winner()

    def best_move(self):
        if self.game_mode == 'comp easy' and len(self.empty_squares) > 7:
            return random.choice(self.empty_squares)
        for combination in self.win_combinations:
            i, j, k = combination
            line = [self.state[i], self.state[j], self.state[k]]
            if line.count('o') == 2 and None in line:
                return combination[line.index(None)]
        for combination in self.win_combinations:
            i, j, k = combination
            line = [self.state[i], self.state[j], self.state[k]]
            if line.count('x') == 2 and None in line:
                return combination[line.index(None)]
        free_combination = []
        future_combination = []
        future_xcombination = []
        for combination in self.win_combinations:
            i, j, k = combination
            line = [self.state[i], self.state[j], self.state[k]]
            if line.count('x') == 0:
                for l in range(3):
                    if line[l] != 'o':
                        free_combination.append(combination[l])
                if line.count('o') == 1:
                    for l in range(3):
                        if line[l] != 'o':
                            future_combination.append(combination[l])
            if line.count('o') == 0 and line.count('x') == 1:
                for l in range(3):
                    if line[l] != 'x':
                        future_xcombination.append(combination[l])
        for i in range(9):
            if future_xcombination.count(i) == 2:
                return i
        for i in range(9):
            if future_combination.count(i) == 2:
                return i
        max_count = 0
        max_index = random.choice(self.empty_squares)
        for i in range(9):
            if max_count < free_combination.count(i):
                max_count = free_combination.count(i)
                max_index = i
        return max_index

    def get_colors(self):
        self.x_color = random.choice(self.colors[0])
        self.o_color = random.choice(self.colors[1])

    def draw_menu(self):
        self.delete('all')
        self.draw_button(self.width / 2, self.width / 4, 'КОМП ТЯЖКО')
        self.draw_button(self.width / 2, self.width / 2, 'КОМП ЛЕГКО')
        self.draw_button(self.width / 2, self.width * 3 / 4, 'ДВА ГРАВЦІ')
        self.sign()

    def draw_button(self, x, y, text):
        w = self.width / 3
        h = self.width / 10
        self.create_oval(x - w - h, y - h, x - w + h, y + h, fill='grey15', outline='black')
        self.create_oval(x + w - h, y - h, x + w + h, y + h, fill='grey15', outline='black')
        self.create_rectangle(x - w, y - h, x + w, y + h, fill='grey15', outline='grey15')
        self.create_line(x - w, y - h, x + w, y - h, fill='black')
        self.create_line(x - w, y + h, x + w, y + h, fill='black')
        self.create_text(x, y + h / 5, text=text, font=('Courier', round(h * 0.75), 'bold'), fill='white')

    def add_figure(self, column, row):
        if len(self.empty_squares) % 2:
            self.add_o(column, row)
        else:
            self.add_x(column, row)

    def add_x(self, column, row):
        x = column * round(self.width / 3)
        y = row * round(self.width / 3)
        self.create_line(
            x + round(self.width / 20),
            y + round(self.width / 20),
            x + round(self.width * 85 / 300),
            y + round(self.width * 85 / 300),
            width=round(self.width / 50),
            fill=self.x_color)
        self.create_line(
            x + round(self.width * 85 / 300),
            y + round(self.width / 20),
            x + round(self.width / 20),
            y + round(self.width * 85 / 300),
            width=round(self.width / 50),
            fill=self.x_color)
        self.update()

    def add_o(self, column, row):
        x = column * round(self.width / 3)
        y = row * round(self.width / 3)
        self.create_oval(
            x + round(self.width / 20),
            y + round(self.width / 20),
            x + round(self.width * 85 / 300),
            y + round(self.width * 85 / 300),
            width=round(self.width / 50),
            outline=self.o_color)
        self.update()

    def draw_lines(self):
        self.delete('all')
        x = round(self.width / 3)
        s = self.width
        self.create_line(x, 0, x, s, fill='grey20')
        self.create_line(2 * x, 0, 2 * x, s, fill='grey20')
        self.create_line(0, x, s, x, fill='grey20')
        self.create_line(0, 2 * x, s, 2 * x, fill='grey20')
        self.update()
        self.sign()

    def draw_explanation(self):
        pos_x = self.width / 6
        pos_o = self.width / 2
        width = self.width / 100
        size = self.width / 35
        if self.game_mode == 'person person':
            if self.first_move_x == 1:
                text = ' - Гравець1      - Гравець2 '
            else:
                text = ' - Гравець2      - Гравець1 '
        else:
            if self.first_move_x == 1:
                text = ' - Гравець       - Компʼютер'
            else:
                text = ' - Компʼютер     - Гравець  '
        self.create_line(pos_x, width, pos_x + size, width + size, width=width, fill=self.x_color)
        self.create_line(pos_x + size, width, pos_x, width + size, width=width, fill=self.x_color)
        self.create_oval(pos_o, width, pos_o + size, width + size, width=width, outline=self.o_color)
        self.create_text(pos_o, size, text=text, font=('Courier', round(size), 'bold'), fill='grey15')
        self.update()

    def draw_menu_button(self):
        x = self.width / 2
        y = self.width * 30 / 31
        h = self.width / 30
        w = h * 3
        self.create_rectangle(x - w, y - h, x + w, y + h, outline='black', fill='grey15')
        self.create_text(x, y + h / 3, text='МЕНЮ', font=('Courier', round(h * 1.5), 'bold'), fill='white')
        self.update()

    def draw_winline(self):
        if self.win_line < 3:  # draw horizontal winline
            self.create_line(
                0,
                self.width * (self.win_combinations[self.win_line][0] / 9 + 1 / 6),
                self.width,
                self.width * (self.win_combinations[self.win_line][0] / 9 + 1 / 6),
                width=round(self.width / 50),
                fill=random.choice(random.choice(self.colors)))
        elif self.win_line < 6:  # draw vertical winline
            self.create_line(
                self.width * (self.win_combinations[self.win_line][0] / 3 + 1 / 6),
                0,
                self.width * (self.win_combinations[self.win_line][0] / 3 + 1 / 6),
                self.width,
                width=round(self.width / 50),
                fill=random.choice(random.choice(self.colors)))
        elif self.win_line == 6:  # first diagonal line
            self.create_line(
                0,
                0,
                self.width,
                self.width,
                width=round(self.width / 50),
                fill=random.choice(random.choice(self.colors)))
        elif self.win_line == 7:  # second diagonal line
            self.create_line(
                self.width,
                0,
                0,
                self.width,
                width=round(self.width / 50),
                fill=random.choice(random.choice(self.colors)))
        self.update()

    def draw_wintable(self):
        text = ''
        if self.last_win == 'x_win':
            if self.game_mode == 'person person':
                text = '1-й виграв'
            else:
                text = 'Ви виграли'
        elif self.last_win == 'o_win':
            if self.game_mode == 'person person':
                text = '2-й виграв'
            else:
                text = 'Ви програли'
        else:
            text = 'Нічия'
        self.create_text(self.width / 2, self.width / 2,
                         text=text, font=('Arial', round(self.width / 10), 'bold'), fill='grey15')
        self.update()
        time.sleep(1)
        self.delete('all')
        self.sign()
        if self.game_mode == 'person person':
            text = ['Гравець1', 'Гравець2']
        else:
            text = ['Перемоги', 'Поразки ']
        self.create_text(self.width / 2, self.width / 2.5,
                         text=f'{text[0]} - {self.win_table["x_win"]}\n{text[1]} - {self.win_table["o_win"]}\nНічия    - {self.win_table["draw"]}',
                         font=('Courier', round(self.width / 11), 'bold'), fill='grey15')
        self.update()
        time.sleep(1)
        self.create_text(self.width / 2, self.width * 3 / 4,
                         text='Клацни для продовження',
                         font=('Arial', round(self.width / 25), 'italic'), fill='brown')

    def sign(self):
        self.create_text(self.width * 9 / 10, self.width * 39 / 40,
                         text='by VsKesha', font=('Arial', round(self.width / 40), 'italic'), fill='Blue')
        self.update()

    def start_new_game(self):
        self.state = [None, None, None, None, None, None, None, None, None]
        self.empty_squares = [0, 1, 2, 3, 4, 5, 6, 7, 8]
        self.get_colors()
        self.delete('all')
        self.draw_lines()
        self.draw_explanation()
        self.draw_menu_button()
        self.game_running = True
        if self.first_move_x == -1 and self.game_mode != 'person person':
            self.bot_move()

    def get_winner(self):
        self.last_win = ''
        if not self.empty_squares:
            self.last_win = 'draw'
            self.win_line = 9
        for n, combination in enumerate(self.win_combinations):
            i, j, k = combination
            if self.state[i] and self.state[i] == self.state[j] and self.state[j] == self.state[k]:
                self.win_line = n
                if self.state[i] == 'x':
                    self.last_win = 'x_win'
                elif self.state[i] == 'o':
                    self.last_win = 'o_win'
        if self.last_win:
            self.game_ending()

    def game_ending(self):
        self.ready = False
        self.game_running = False
        self.win_table[self.last_win] += 1
        if self.win_line < 9:
            for i in range(5):
                self.draw_winline()
                time.sleep(0.3)
        self.draw_wintable()
        if self.last_win == 'x_win':
            self.first_move_x = 1
        elif self.last_win == 'o_win':
            self.first_move_x = -1
        else:
            self.first_move_x *= -1
        self.ready = True


def main():
    window = tkinter.Tk()
    window.title('Хрестики-Нyлики')
    game = TicTacToe(window, 600)
    game.pack()
    game.draw_menu()
    window.mainloop()


if __name__ == '__main__':
    main()

orgapp/tictactoe/tictactoe.py
Vskesha/orgApp
from setuptools import setup
from pathlib import Path


this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()
long_description += (this_directory / 'orgapp' / 'address_book' / 'README.md').read_text()
long_description += (this_directory / 'orgapp' / 'note_book' / 'README.md').read_text()
long_description += (this_directory / 'orgapp' / 'sorter' / 'README.md').read_text()


setup(
    name="orgApp",
    version='0.0.2',
    description="personal organizing tool",
    long_description=long_description,
    long_description_content_type='text/markdown',
    url="https://github.com/Vskesha/orgApp",
    author='Vasyl Boliukh, '
           'Olga Sirenko, '
           'Oleh Kolobaiev, '
           'Andriy Batig',
    author_email='vskesha@gmail.com, '
                 'olga19022020@gmail.com, '
                 'o.kolobaiev@gmail.com, '
                 'ashabatig1992@gmail.com',
    license='MIT License',
    packages=[
        "orgapp",
        "orgapp.address_book",
        "orgapp.bandergoose",
        "orgapp.hannoitower",
        "orgapp.note_book",
        "orgapp.snake",
        "orgapp.sorter",
        "orgapp.tictactoe"
    ],
    include_package_data=True,
    install_requires=[
        'markdown',
        "colorama==0.4.6",
        "prompt-toolkit==3.0.39",
        "pygame==2.5.1",
        "wcwidth==0.2.6",
        'Faker==19.6.1'
    ],
    entry_points={
        'console_scripts': [
            "orgapp = orgapp.main:main",
            "abk = orgapp.address_book.address_book:main",
            "addressbook = orgapp.address_book.address_book:main",
            "nbk = orgapp.note_book.note_book:main",
            "notebook = orgapp.note_book.note_book:main",
            "sorter = orgapp.sorter.sorter:clean_folder",
        ],
        'gui_scripts': [
            "bandergoose = orgapp.bandergoose.bandergoose:main",
            "hannoitower = orgapp.hannoitower.hannoitower:main",
            "snake = orgapp.snake.snake:main",
            "tictactoe = orgapp.tictactoe.tictactoe:main"
        ]
    }
)

setup.py
Vskesha/orgApp