Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions homework_04/main.py
Original file line number Diff line number Diff line change
@@ -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()
32 changes: 29 additions & 3 deletions homework_05/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("Недостаточно топлива для движения")
9 changes: 9 additions & 0 deletions homework_05/car.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions homework_05/engine.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
Создайте dataclass `Engine`
"""
from dataclasses import dataclass


@dataclass
class Engine:
volume: float
pistons: int
13 changes: 13 additions & 0 deletions homework_05/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,16 @@
- NotEnoughFuel
- CargoOverload
"""
class LowFuelError(Exception):
"""Выбрасывается, если топлива недостаточно для запуска"""
pass


class NotEnoughFuel(Exception):
"""Выбрасывается, если топлива недостаточно для движения"""
pass


class CargoOverload(Exception):
"""Выбрасывается, если перегружен самолёт"""
pass
25 changes: 25 additions & 0 deletions homework_05/plane.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions homework_05/test.py
Original file line number Diff line number Diff line change
@@ -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())
55 changes: 55 additions & 0 deletions homework_05/test_homework_05.py
Original file line number Diff line number Diff line change
@@ -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)