Примерни решения на задачите по тема 4

Примерни решения на задачите по тема 4#

Задача 1 (2 точки)#

Важно !: Във всяка задача е задължително използването на type hints в дефинициите на функциите.

Напишете функция validate_list, която приема път до списък за пазаруване. Списъкът е съставен от име на предмета, бройката която трябва да се закупи и единичната му цена. Функцията трябва да валидира списъка по зададени условия, и ако списъка е валиден, да върне общата сума, която е необходимо да похарчим. Ако списъка не отговаря на някое от условията, се хвърля специално дефинирана грешка.

Възможните грешки са:

  • InvalidLineError - приема реда, който не отговаря на условията

  • InvalidItemError - приема името на предмета, който не отговаря на условията

  • InvalidQuantityError - приема броя и предмета, който не отговаря на условията

  • InvalidPriceError - приема цената и предмета, който не отговаря на условията

  • ListFileError - приема пътя до файла, който не отговаря на условията

Правилата за валидиране на следните:

  • Ако файла не съществува, хвърлете ListFileError.

  • Ако файла не може да бъде прочетен, хвърлете ListFileError.

  • Всеки ред от файла започва с -. Ако не е така, хвърлете InvalidLineError.

  • Всеки ред има следната структура: име на предмета:брой:единична цена. Ако не е така, хвърлете InvalidLineError.

  • Ако името на предмета е празен низ, или е съставено само от цифри, хвърлете InvalidItemError.

  • Ако броят не е число, хвърлете InvalidQuantityError.

  • Ако броят не е цяло число, хвърлете InvalidQuantityError.

  • Ако броят е отрицателно, хвърлете InvalidQuantityError.

  • Ако единичната цена не е число, хвърлете InvalidPriceError.

  • Ако единичната цена е отрицателно число, хвърлете InvalidPriceError.

Примерен файл:

- мляко:2:2.50
- хляб:1:1.50
- банани:1:2.50
- ябълки:1:0.50
- круши:1:1.75

Примерни файлове може да намерите в папката lab04_files/task_1

import os

class InvalidLineError(Exception):
    def __init__(self, line):
        super().__init__(f"Invalid line: {line}")


class InvalidItemError(Exception):
    def __init__(self, item):
        super().__init__(f"Invalid item: {item}")


class InvalidQuantityError(Exception):
    def __init__(self, quantity, item):
        super().__init__(f"Invalid quantity ({quantity}) for item: {item}")


class InvalidPriceError(Exception):
    def __init__(self, price, item):
        super().__init__(f"Invalid price ({price}) for item: {item}")

class ListFileError(Exception):
    def __init__(self, filename):
        super().__init__(f"Error reading {filename}")


def validate_list(filepath: str) -> float:
    if not os.path.exists(filepath):
        raise ListFileError(f"File {filepath} does not exist")
    
    try:
        with open(filepath, "r") as f:
            lines = f.readlines()
    except OSError as e:
        raise ListFileError(f"Cannot read {filepath}")

    total = 0.0
    for line in lines:
        if not line.startswith('-'):
            raise InvalidLineError(line)

        line = line[1:].strip().split(':')

        if len(line) != 3:
            raise InvalidLineError(line)
        
        item, quantity, price = line

        if item == "" or item.isnumeric():
            raise InvalidItemError(item)
        
        if "." in quantity:
            raise InvalidQuantityError(quantity, item)
        try:
            quantity = int(quantity)
        except ValueError:
            raise InvalidQuantityError(quantity, item)
        
        if quantity <= 0:
            raise InvalidQuantityError(quantity, item)
        

        try:
            price = float(price)
        except ValueError:
            raise InvalidPriceError(price, item)
        
        if price <= 0:
            raise InvalidPriceError(price, item)

        total += quantity * price

    return total
assert abs(validate_list(os.path.join("lab04_files", "task_1", "list1.txt")) - 11.25) < 0.001

assert int(validate_list(os.path.join("lab04_files", "task_1", "list2.txt"))) == 0, "Empty files should return 0"

try:
    validate_list(os.path.join("lab04_files", "task_1", "list3.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidLineError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list4.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidLineError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list5.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidItemError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list6.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidQuantityError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list7.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidQuantityError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list8.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidQuantityError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list9.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidPriceError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list10.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidPriceError:
    pass

try:
    validate_list(os.path.join("lab04_files", "task_1", "list11.txt"))
    assert False, "Should raise InvalidLineError"
except InvalidLineError:
    pass

"✅ All OK! +2 points"

Напишете клас SimpleBackup, който да реализира следните функционалности:

  • Създаване на копие на данни по подаден списък с файлове за копиране (create). Създаваме копие на всички файлове, но не и на самите директории.

  • Възстановяване на копие на данни по подадено име на копие и локация, където да се възстанови (restore)

  • Връщане на сортиран списък с всички копия на данни, които са направени (show)

  • Изтриване на копие на данни по подадено име на копие (delete)

Име на копие дефинираме по следния начин: backup_<timestamp>, където <timestamp> е времето на създаване на копието във формат YYYYMMDD_HHMMSS. (За форматирането на датата и часа може да използвате strftime от модула time.)

Списъка с файлове за копиране е в следния формат - на всеки ред е описан един файл с релативния му път. Важно: пътищата на файловете са разделени с интервал, а не с /.

Копията трябва да се съхраняват в директорията backups, като всяко копие е в отделна директория.

Методите create, restore, show и delete на копие трябва да връщат True ако операцията е била успешна, и False в противен случай.

В директорията task_2 се намира backups (където трябва да бъдат създавани вашите копия), списъците с файлове за копиране - backup_1.txt, backup_2.txt и т.н., и директорията sample_files, в която се намират тестовите файлове, на които ще правим копия.

Бележки:

Изпълнявайте тетрадката/кода ви в директорията labs, за да може да работите с релативни пътища.

Независимо от структурата на входните ви данни, съдържанието на резервните копия трябва да е на едно ниво - т.е всички файлове за дадено копие трябва да се намират в една директория.

import os
import re
import shutil
from time import strftime, localtime

class SimpleBackup:
    def __init__(self):
        self.__pattern = 'backup_{}'
        self.__location = os.path.join('lab04_files', 'task_2', 'backups')
        
    def create(self, config_filepath: str) -> bool:
        backup_name = self.__pattern.format(strftime("%Y%m%d_%H%M%S", localtime()))

        try: 
            filepaths_to_backup = SimpleBackup.__parse_config(config_filepath)
        except FileNotFoundError as ex:
            print(ex)
            return False

        backup_dir = os.path.join(self.__location, backup_name)

        os.makedirs(backup_dir, exist_ok=True)
        
        target_filepaths = [os.path.join(backup_dir, os.path.basename(filepath)) for filepath in filepaths_to_backup if os.path.isfile(filepath)]

        for source, target in zip(filepaths_to_backup, target_filepaths):
            result = SimpleBackup.__copy(source, target)

            if not result:
                return False

        return True

    def restore(self, backup_name: str, target_dir: str) -> bool:
        if not self.__is_backup_name_valid(backup_name):
            return False

        backup_dir = os.path.join(self.__location, backup_name)

        if not os.path.isdir(target_dir):
            return False

        source_filepaths = [os.path.join(backup_dir, content) for content in os.listdir(backup_dir)]
        target_filepaths = [os.path.join(target_dir, os.path.basename(source_filepath)) for source_filepath in source_filepaths]

        for source, target in zip(source_filepaths, target_filepaths):
            result = SimpleBackup.__copy(source, target)

            if not result:
                return False
        
        return True


    def show(self) -> list[str]:
        return sorted(self.__get_available_backup_names())

    def delete(self, backup_name: str) -> bool:
        backup_dir = os.path.join(self.__location, backup_name)

        for dirpath, _, filenames in os.walk(backup_dir):
            for filename in filenames:
                os.remove(os.path.join(dirpath, filename))
            os.rmdir(dirpath)
        
        return True

    @staticmethod
    def __parse_config(config_filepath: str) -> list[str]:
        with open(config_filepath, 'r') as f:
            filepaths_to_backup = f.readlines()

        filepaths_to_backup = [os.path.join(*filepath.strip().split()) for filepath in filepaths_to_backup]

        return filepaths_to_backup
    
    @staticmethod
    def __copy(source_filepath: str, target_filepath: str) -> bool:
        try:
            shutil.copy2(source_filepath, target_filepath)
        except FileNotFoundError as ex:
            print(ex)
            return False
        
        return True
    
    def __get_available_backup_names(self) -> list[str]:
        items_under_backup_location = os.listdir(self.__location)

        backup_names = [item for item in items_under_backup_location if self.__is_backup_name_valid(item)]

        return backup_names

    
    @staticmethod
    def __is_backup_name_valid(backup_name: str) -> bool:
        return re.match(r'.*backup_[0-9]{8}_[0-9]{6}', backup_name) is not None
# Some helper code

import os
def get_folder_content(backup_dir: str) -> list[str]:
    result = []

    for content in os.listdir(backup_dir):
        with open(os.path.join(backup_dir, content), 'r') as f:
            result.append(f.read().strip())

    return result
import time
import shutil

backups = SimpleBackup()


# Arrange 
default_listing = backups.show()

# Act 
backup1_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_1.txt'))
time.sleep(1)  # to ensure that the backups will have a different timestamp
backup2_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_2.txt'))

listing1_result = backups.show()

backup1_path = listing1_result[-2]
backup2_path = listing1_result[-1]

backup1_content = get_folder_content(os.path.join('lab04_files', 'task_2', 'backups', backup1_path))
backup2_content = get_folder_content(os.path.join('lab04_files', 'task_2', 'backups', backup2_path))

time.sleep(1)  # to ensure that the backups will have a different timestamp

backup3_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_3.txt'))
time.sleep(1)  # to ensure that the backups will have a different timestamp

backup4_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_4.txt'))
time.sleep(1)  # to ensure that the backups will have a different timestamp

backup5_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_5.txt'))
time.sleep(1)  # to ensure that the backups will have a different timestamp

listing2_result = backups.show()

backup3_path = listing2_result[-2]
backup4_path = listing2_result[-1]

backup3_content = get_folder_content(os.path.join('lab04_files', 'task_2', 'backups', backup3_path))
backup4_content = get_folder_content(os.path.join('lab04_files', 'task_2', 'backups', backup4_path))

backup6_result = backups.create(os.path.join('lab04_files', 'task_2', 'backup_4.txt'))
listing3_result = sorted(backups.show())
time.sleep(1)  # to ensure that the backups will have a different timestamp

removal1_result = backups.delete(listing3_result[-1])
listing4_result = backups.show()


restore_results = []
restore_paths = [os.path.join('lab04_files', 'task_2', f'restored_{i}') for i in range(1, 5)]
backups_to_restore = listing4_result[-4:]
restore_contents = []

for backup_to_restore, restore_path in zip(backups_to_restore, restore_paths):
    os.makedirs(restore_path)
    restore_results.append(backups.restore(backup_to_restore, restore_path))
    restore_contents.append(get_folder_content(restore_path))
    shutil.rmtree(restore_path)

restore5_result = backups.restore('backup_20231225_144719', restore_paths[0])
restore6_result = backups.restore('asd', restore_paths[0])

for backup in backups.show():
    backups.delete(backup)

listing5_result = backups.show()

# Tests

# Create
assert backup1_result == True
assert backup2_result == True
assert backup3_result == True
assert backup4_result == True
assert backup5_result == False, 'Config file for backup_5 does not exist'

assert set(backup1_content) == set(['123'])
assert set(backup2_content) == set(['456'])
assert set(backup3_content) == set(['456', '4567'])
assert set(backup4_content) == set(['123', '456', 'Атака, чичо !', '4567'])

# Show
assert len(listing1_result) == len(default_listing) + 2
assert len(listing2_result) == len(default_listing) + 4
assert len(listing3_result) == len(default_listing) + 5
assert len(listing4_result) == len(default_listing) + 4
assert len(listing5_result) == len(default_listing)
assert listing5_result == default_listing

# Restore
assert restore_results[0] == True
assert restore_results[1] == True
assert restore_results[2] == True
assert restore_results[3] == True
assert restore5_result == False, 'Restore of non-existing backup should fail'
assert restore6_result == False, 'Restore of backup with invalid name should fail'


assert set(restore_contents[0]) == set(['123'])
assert set(restore_contents[1]) == set(['456'])
assert set(restore_contents[2]) == set(['456', '4567'])
assert set(restore_contents[3]) == set(['123', '456', 'Атака, чичо !', '4567'])

# Delete
for backup in listing3_result:
    assert os.path.exists(os.path.join('lab04_files', 'task_2', 'backups', backup)) == False


"✅ All OK! +4 points"