Примерни решения на задачите по тема 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"