Skip to content
Merged
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
14 changes: 7 additions & 7 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
black
flake8
isort
mypy
pylint
pytest
pytest-asyncio
pytest-mock
pytest-cov
pylint
mypy
flake8
black
isort
types-yt-dlp
types-requests
pytest-asyncio
types-yt-dlp
61 changes: 40 additions & 21 deletions src/beauty.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime
import asyncio
import os
import random
from datetime import date
Expand All @@ -10,35 +10,54 @@

DEFAULT_QUERY = "woman,models,real,beauty,pose"
DEFAULT_OUTPUT_PATH = "tmp/downloads/beauty_of_the_day.png"
_BEAUTY_CACHE_LOCK = asyncio.Lock()


async def handle_beauty(update: Update):
path = Path(DEFAULT_OUTPUT_PATH)
message = update.message
if message is None:
return

path = await get_beauty_image_path()
if path is None:
print("Errore nel download dell'immagine")
return

with path.open("rb") as image_file:
await message.reply_photo(photo=image_file)


def is_beauty_image_fresh(path: Path, today: date | None = None) -> bool:
if not path.exists():
result = download_beauty_image()
if result is None:
print("Errore nel download dell'immagine")
return
path = Path(result)
print("non esiste ancora, la scarico")
return False

if today is None:
today = date.today()

elif datetime.datetime.fromtimestamp(path.stat().st_ctime).day == date.today().day:
print("già scaricata, invio l'immagine")
return date.fromtimestamp(path.stat().st_mtime) == today

else:
result = download_beauty_image()

async def get_beauty_image_path(
output_path: str = DEFAULT_OUTPUT_PATH,
downloader=None,
) -> Path | None:
if downloader is None:
downloader = download_beauty_image

async with _BEAUTY_CACHE_LOCK:
path = Path(output_path)

if is_beauty_image_fresh(path):
print("già scaricata, invio l'immagine")
return path

result = await asyncio.to_thread(downloader, DEFAULT_QUERY, output_path)
if result is None:
print("Errore nel download dell'immagine")
return
path = Path(result)
print("scarico l'immagine e la invio")
return None

with path.open("rb") as image_file:
message = update.message
if message is None or message.text is None:
return
await message.reply_photo(photo=image_file)
path = Path(result)
print("immagine aggiornata, invio l'immagine")
return path


def download_beauty_image(
Expand Down
1 change: 1 addition & 0 deletions src/handlers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=unused-argument


import validators
from telegram import Update
from telegram.ext import ContextTypes
Expand Down
101 changes: 101 additions & 0 deletions tests/test_beauty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import asyncio
import os
from datetime import date, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock

from src import beauty


def test_is_beauty_image_fresh_returns_false_when_file_does_not_exist(tmp_path):
path = tmp_path / "missing.png"

assert beauty.is_beauty_image_fresh(path, today=date(2026, 3, 28)) is False


def test_is_beauty_image_fresh_returns_true_for_file_from_same_day(tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image")

today = date.today()
_set_file_mtime(path, datetime.combine(today, datetime.min.time()).timestamp())

assert beauty.is_beauty_image_fresh(path, today=today) is True


def test_is_beauty_image_fresh_returns_false_for_file_from_previous_day(tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image")

today = date.today()
yesterday = today - timedelta(days=1)
_set_file_mtime(path, datetime.combine(yesterday, datetime.min.time()).timestamp())

assert beauty.is_beauty_image_fresh(path, today=today) is False


def test_get_beauty_image_path_returns_cached_file_without_downloading(
tmp_path, monkeypatch
):
path = tmp_path / "beauty.png"
path.write_bytes(b"cached")
monkeypatch.setattr(
beauty, "is_beauty_image_fresh", lambda candidate: candidate == path
)

downloader = MagicMock()
to_thread = AsyncMock()
monkeypatch.setattr(beauty.asyncio, "to_thread", to_thread)

result = asyncio.run(beauty.get_beauty_image_path(str(path), downloader))

assert result == path
downloader.assert_not_called()
to_thread.assert_not_awaited()


def test_get_beauty_image_path_downloads_stale_file_in_thread(tmp_path, monkeypatch):
path = tmp_path / "beauty.png"
monkeypatch.setattr(beauty, "is_beauty_image_fresh", lambda candidate: False)

downloader = MagicMock(return_value=str(path))
to_thread = AsyncMock(return_value=str(path))
monkeypatch.setattr(beauty.asyncio, "to_thread", to_thread)

result = asyncio.run(beauty.get_beauty_image_path(str(path), downloader))

assert result == path
to_thread.assert_awaited_once_with(downloader, beauty.DEFAULT_QUERY, str(path))


def test_handle_beauty_replies_with_photo(monkeypatch, tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image-content")

message = MagicMock()
message.reply_photo = AsyncMock()
update = MagicMock(message=message)

get_path = AsyncMock(return_value=path)
monkeypatch.setattr(beauty, "get_beauty_image_path", get_path)

asyncio.run(beauty.handle_beauty(update))

message.reply_photo.assert_awaited_once()
sent_photo = message.reply_photo.await_args.kwargs["photo"]
assert sent_photo.name == str(path)


def test_handle_beauty_returns_when_download_fails(monkeypatch):
message = MagicMock()
message.reply_photo = AsyncMock()
update = MagicMock(message=message)

monkeypatch.setattr(beauty, "get_beauty_image_path", AsyncMock(return_value=None))

asyncio.run(beauty.handle_beauty(update))

message.reply_photo.assert_not_awaited()


def _set_file_mtime(path, timestamp: float) -> None:
os.utime(path, (timestamp, timestamp))