diff --git a/homework_04/main.py b/homework_04/main.py new file mode 100644 index 0000000..e9b9687 --- /dev/null +++ b/homework_04/main.py @@ -0,0 +1,184 @@ + +# homework_04/media.py +# абстракции — через NotImplementedError + +class Storage: + """Абстракция хранилищ.""" + def save(self, path, bytes_or_stream): + raise NotImplementedError + def load(self, path): + raise NotImplementedError + def delete(self, path): + raise NotImplementedError + def exists(self, path): + raise NotImplementedError + +class LocalStorage(Storage): + """Заглушки для локального диска.""" + def save(self, path, bytes_or_stream): + # открыть файл и записать bytes_or_stream + pass + def load(self, path): + # вернуть байты/поток + return None + def delete(self, path): + # удалить файл + pass + def exists(self, path): + return True + +class S3Storage(Storage): + """Заглушки для s3-like.""" + def __init__(self, bucket): + self.bucket = bucket + def save(self, path, bytes_or_stream): + # загрузить объект в bucket/path + pass + def load(self, path): + return None + def delete(self, path): + pass + def exists(self, path): + return True + +class MediaFile: + """Базовый медиа-файл.""" + def __init__(self, name, size=0, created_at=None, owner=None, + storage=None, path=None, meta=None): + self.name = name + self.size = size + self.created_at = created_at + self.owner = owner + self.storage = storage or LocalStorage() + self.path = path or name + self.meta = meta or {} # общие метаданные + + # --- CRUD --- + def save(self, bytes_or_stream=None): + """Сохранить контент и метаданные в storage по self.path.""" + self.storage.save(self.path, bytes_or_stream) + def load(self): + """Загрузить контент из storage.""" + return self.storage.load(self.path) + def delete(self): + """Удалить из storage.""" + self.storage.delete(self.path) + + # --- Бизнес-операции (заглушки) --- + def convert(self, **params): + """Сконвертировать в другой формат/профиль, вернуть новый объект.""" + raise NotImplementedError + def extract_features(self, **params): + """Извлечь фичи (превью, спектр, EXIF...), вернуть dict.""" + raise NotImplementedError + def validate(self): + """Проверить согласованность метаданных/контента.""" + return True + +class AudioFile(MediaFile): + """Аудио-метаданные: bitrate, sample_rate, channels, codec...""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.meta.setdefault("bitrate", None) + self.meta.setdefault("sample_rate", None) + self.meta.setdefault("channels", None) + self.meta.setdefault("codec", None) + + def convert(self, target_codec=None, bitrate=None, sample_rate=None, **_): + # вернёт новый AudioFile с обновлёнными метаданными/путём + new_name = self.name + ".conv" + return AudioFile(new_name, storage=self.storage, path=new_name, + meta={ + "bitrate": bitrate or self.meta["bitrate"], + "sample_rate": sample_rate or self.meta["sample_rate"], + "channels": self.meta["channels"], + "codec": target_codec or self.meta["codec"], + }) + + def extract_features(self, **_): + # вернёт, например, длительность и спектральные признаки + return {"duration_sec": None, "mfcc": None} + +class VideoFile(MediaFile): + """Видео-метаданные: resolution, fps, vcodec, acodec, duration...""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.meta.setdefault("resolution", None) + self.meta.setdefault("fps", None) + self.meta.setdefault("vcodec", None) + self.meta.setdefault("acodec", None) + self.meta.setdefault("duration", None) + + def convert(self, resolution=None, fps=None, vcodec=None, **_): + new_name = self.name + ".conv" + return VideoFile(new_name, storage=self.storage, path=new_name, + meta={ + "resolution": resolution or self.meta["resolution"], + "fps": fps or self.meta["fps"], + "vcodec": vcodec or self.meta["vcodec"], + "acodec": self.meta["acodec"], + "duration": self.meta["duration"], + }) + + def extract_features(self, **_): + # ключевые кадры, превью, цветовая гистограмма + return {"thumbnails": None, "keyframes": None, "color_hist": None} + +class PhotoFile(MediaFile): + """Фото-метаданные: width, height, exif, colorspace...""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.meta.setdefault("width", None) + self.meta.setdefault("height", None) + self.meta.setdefault("exif", {}) + self.meta.setdefault("colorspace", None) + + def convert(self, colorspace=None, size=None, **_): + new_name = self.name + ".conv" + new_meta = dict(self.meta) + if colorspace is not None: + new_meta["colorspace"] = colorspace + if size is not None: + new_meta["width"], new_meta["height"] = size + return PhotoFile(new_name, storage=self.storage, path=new_name, meta=new_meta) + + def extract_features(self, **_): + # гистограммы, лица, EXIF + return {"faces": None, "histogram": None, "exif": self.meta.get("exif", {})} + +# ---- Примеры использования ---- + +def examples(): + local = LocalStorage() + s3 = S3Storage(bucket="media") + + # Создание + a = AudioFile("song.mp3", storage=local, meta={"bitrate": 320, "sample_rate": 44100, "channels": 2, "codec": "mp3"}) + v = VideoFile("clip.mp4", storage=s3, meta={"resolution": "1920x1080", "fps": 30, "vcodec": "h264"}) + p = PhotoFile("pic.jpg", storage=local, meta={"width": 4000, "height": 3000, "colorspace": "sRGB"}) + + # Save / Load / Delete + a.save(bytes_or_stream=None) # записать контент + data = a.load() # прочитать + a.delete() # удалить + + # Действия + a2 = a.convert(target_codec="aac", bitrate=256) + feats_a = a.extract_features() + + v2 = v.convert(resolution="1280x720", fps=24) + feats_v = v.extract_features() + + p2 = p.convert(colorspace="AdobeRGB", size=(2048, 1536)) + feats_p = p.extract_features() + + # Работа с разными хранилищами прозрачно: + p2.save() + v2.save() + + + + + +if __name__ == "__main__": + examples() \ No newline at end of file diff --git a/homework_05/base.py b/homework_05/base.py index 9788beb..f69aee8 100644 --- a/homework_05/base.py +++ b/homework_05/base.py @@ -2,8 +2,34 @@ Доработайте класс `Vehicle` """ -from abc import ABC +from homework_05 import exceptions -class Vehicle(ABC): - pass +class Vehicle: + weight: int = 1500 + started: bool = False + + fuel: float = 0 + fuel_consumption: float = 0 + + def __init__(self, weight: int = 1500, fuel: float = 0, fuel_consumption: float = 0): + self.weight = weight + self.fuel = fuel + self.fuel_consumption = fuel_consumption + self.started = False + + def start(self): + """Запуск двигателя: проверяем топливо и меняем состояние""" + if not self.started: + if self.fuel > 0: + self.started = True + else: + raise exceptions.LowFuelError("Невозможно запустить двигатель — нет топлива") + + def move(self, distance: float): + """Проверка топлива и движение""" + fuel_needed = distance * self.fuel_consumption + if self.fuel >= fuel_needed: + self.fuel -= fuel_needed + else: + raise exceptions.NotEnoughFuel("Недостаточно топлива для движения") \ No newline at end of file diff --git a/homework_05/car.py b/homework_05/car.py index 1947f4b..ffffc7e 100644 --- a/homework_05/car.py +++ b/homework_05/car.py @@ -1,3 +1,12 @@ """ Создайте класс `Car`, наследник `Vehicle` """ +from homework_05.base import Vehicle +from homework_05.engine import Engine + + +class Car(Vehicle): + engine: Engine = None + + def set_engine(self, engine: Engine): + self.engine = engine \ No newline at end of file diff --git a/homework_05/engine.py b/homework_05/engine.py index d3c4b61..4c662a4 100644 --- a/homework_05/engine.py +++ b/homework_05/engine.py @@ -1,3 +1,10 @@ """ Создайте dataclass `Engine` """ +from dataclasses import dataclass + + +@dataclass +class Engine: + volume: float + pistons: int \ No newline at end of file diff --git a/homework_05/exceptions.py b/homework_05/exceptions.py index a3feff8..6f12072 100644 --- a/homework_05/exceptions.py +++ b/homework_05/exceptions.py @@ -4,3 +4,16 @@ - NotEnoughFuel - CargoOverload """ +class LowFuelError(Exception): + """Выбрасывается, если топлива недостаточно для запуска""" + pass + + +class NotEnoughFuel(Exception): + """Выбрасывается, если топлива недостаточно для движения""" + pass + + +class CargoOverload(Exception): + """Выбрасывается, если перегружен самолёт""" + pass \ No newline at end of file diff --git a/homework_05/plane.py b/homework_05/plane.py index 1f37f69..52832ae 100644 --- a/homework_05/plane.py +++ b/homework_05/plane.py @@ -1,3 +1,28 @@ """ Создайте класс `Plane`, наследник `Vehicle` """ +from homework_05.base import Vehicle +from homework_05 import exceptions + + +class Plane(Vehicle): + cargo: float = 0 + max_cargo: float + + def __init__(self, weight: int = 1500, fuel: float = 0, fuel_consumption: float = 0, max_cargo: float = 0): + super().__init__(weight, fuel, fuel_consumption) + self.max_cargo = max_cargo + self.cargo = 0 + + def load_cargo(self, cargo: float): + """Добавляем груз, проверяя перегруз""" + if self.cargo + cargo <= self.max_cargo: + self.cargo += cargo + else: + raise exceptions.CargoOverload("Превышен лимит груза") + + def remove_all_cargo(self) -> float: + """Возвращает старый груз и очищает трюм""" + old_cargo = self.cargo + self.cargo = 0 + return old_cargo \ No newline at end of file diff --git a/homework_05/test.py b/homework_05/test.py new file mode 100644 index 0000000..fd3242d --- /dev/null +++ b/homework_05/test.py @@ -0,0 +1,17 @@ +from homework_05.car import Car +from homework_05.engine import Engine +from homework_05.plane import Plane + +car = Car(fuel=50, fuel_consumption=5) +car.start() +car.move(5) +print(car.fuel) # остаток топлива + +engine = Engine(volume=2.0, pistons=4) +car.set_engine(engine) +print(car.engine) + +plane = Plane(fuel=100, fuel_consumption=10, max_cargo=200) +plane.load_cargo(100) +print(plane.cargo) +print(plane.remove_all_cargo()) \ No newline at end of file diff --git a/homework_05/test_homework_05.py b/homework_05/test_homework_05.py new file mode 100644 index 0000000..62b212a --- /dev/null +++ b/homework_05/test_homework_05.py @@ -0,0 +1,55 @@ +import pytest +from homework_05.base import Vehicle +from homework_05.car import Car +from homework_05.engine import Engine +from homework_05.plane import Plane +from homework_05 import exceptions + + +def test_vehicle_start_success(): + v = Vehicle(fuel=10, fuel_consumption=1) + v.start() + assert v.started is True + + +def test_vehicle_start_no_fuel(): + v = Vehicle(fuel=0, fuel_consumption=1) + with pytest.raises(exceptions.LowFuelError): + v.start() + + +def test_vehicle_move_success(): + v = Vehicle(fuel=10, fuel_consumption=2) + v.start() + v.move(3) # тратим 6 топлива + assert v.fuel == 4 + + +def test_vehicle_move_not_enough_fuel(): + v = Vehicle(fuel=5, fuel_consumption=2) + v.start() + with pytest.raises(exceptions.NotEnoughFuel): + v.move(10) + + +def test_car_set_engine(): + car = Car(fuel=20, fuel_consumption=5) + engine = Engine(volume=2.0, pistons=4) + car.set_engine(engine) + assert car.engine == engine + + +def test_plane_load_and_remove_cargo(): + plane = Plane(fuel=50, fuel_consumption=5, max_cargo=100) + plane.load_cargo(40) + assert plane.cargo == 40 + + removed = plane.remove_all_cargo() + assert removed == 40 + assert plane.cargo == 0 + + +def test_plane_overload(): + plane = Plane(fuel=50, fuel_consumption=5, max_cargo=50) + with pytest.raises(exceptions.CargoOverload): + plane.load_cargo(100) \ No newline at end of file