Files
logimage-generator/docs/superpowers/plans/2026-05-20-pexels-image-source.md
T
Vincent Bourdon 5a03f8a38d import initial
2026-06-10 10:21:18 +02:00

342 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pexels Image Source Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ajouter `PexelsImageSource` et router automatiquement vers Pexels ou Unsplash selon les clés d'API disponibles dans l'environnement.
**Architecture:** Le port `ImageSource` (ABC) existe déjà. On ajoute une implémentation `PexelsImageSource` dans l'infrastructure, puis on modifie `main.py` pour instancier le bon provider selon `PEXELS_API_KEY` (prioritaire) ou `UNSPLASH_ACCESS_KEY`.
**Tech Stack:** Python, httpx, pytest, pytest-mock
---
## Fichiers concernés
| Action | Fichier | Rôle |
|----------|------------------------------------------------------------------|-------------------------------------|
| Créer | `src/logimage/infrastructure/image/pexels_source.py` | Implémentation Pexels de ImageSource |
| Créer | `tests/infrastructure/test_pexels_source.py` | Tests d'intégration Pexels |
| Créer | `tests/cli/__init__.py` | Package tests CLI |
| Créer | `tests/cli/test_main_routing.py` | Tests unitaires du routing CLI |
| Modifier | `src/logimage/cli/main.py` | Routing selon les vars d'env |
---
### Task 1 : PexelsImageSource
**Fichiers :**
- Créer : `src/logimage/infrastructure/image/pexels_source.py`
- Créer : `tests/infrastructure/test_pexels_source.py`
- [ ] **Step 1 : Écrire les tests d'intégration (failing)**
Créer `tests/infrastructure/test_pexels_source.py` :
```python
import os
import pytest
from logimage.infrastructure.image.pexels_source import PexelsImageSource
from logimage.domain.value_objects.image_data import ImageData
@pytest.mark.integration
def test_fetch_returns_image_data() -> None:
api_key = os.environ.get("PEXELS_API_KEY")
if not api_key:
pytest.skip("PEXELS_API_KEY not set")
source = PexelsImageSource(api_key=api_key)
result = source.fetch()
assert isinstance(result, ImageData)
assert len(result.content) > 1000
assert isinstance(result.title, str)
@pytest.mark.integration
def test_fetch_with_theme_returns_image_data() -> None:
api_key = os.environ.get("PEXELS_API_KEY")
if not api_key:
pytest.skip("PEXELS_API_KEY not set")
source = PexelsImageSource(api_key=api_key)
result = source.fetch(theme="forest")
assert isinstance(result, ImageData)
assert len(result.content) > 1000
assert isinstance(result.title, str)
```
- [ ] **Step 2 : Vérifier que les tests échouent**
```bash
pytest tests/infrastructure/test_pexels_source.py -v -m integration
```
Attendu : `ImportError: cannot import name 'PexelsImageSource'`
- [ ] **Step 3 : Implémenter PexelsImageSource**
Créer `src/logimage/infrastructure/image/pexels_source.py` :
```python
import httpx
from logimage.domain.ports.image_source import ImageSource
from logimage.domain.value_objects.image_data import ImageData
_SEARCH_URL = "https://api.pexels.com/v1/search"
_CURATED_URL = "https://api.pexels.com/v1/curated"
class PexelsImageSource(ImageSource):
def __init__(self, api_key: str) -> None:
self._api_key = api_key
def fetch(self, theme: str | None = None) -> ImageData:
headers = {"Authorization": self._api_key}
params: dict[str, str | int] = {"per_page": 1}
if theme:
url = _SEARCH_URL
params["query"] = theme
else:
url = _CURATED_URL
with httpx.Client(timeout=30.0) as client:
response = client.get(url, headers=headers, params=params)
response.raise_for_status()
photos = response.json()["photos"]
if not photos:
raise ValueError(f"No photos found for theme={theme!r}")
photo = photos[0]
image_url = photo["src"]["large"]
title: str = photo.get("alt") or theme or "logimage"
image_response = client.get(image_url, timeout=60.0)
image_response.raise_for_status()
return ImageData(content=image_response.content, title=title)
```
- [ ] **Step 4 : Vérifier que les tests passent**
```bash
pytest tests/infrastructure/test_pexels_source.py -v -m integration
```
Attendu : 2 tests PASSED (ou SKIPPED si `PEXELS_API_KEY` non définie)
- [ ] **Step 5 : Commit**
```bash
git add src/logimage/infrastructure/image/pexels_source.py tests/infrastructure/test_pexels_source.py
git commit -m "feat: add PexelsImageSource"
```
---
### Task 2 : Routing CLI
**Fichiers :**
- Modifier : `src/logimage/cli/main.py`
- Créer : `tests/cli/__init__.py`
- Créer : `tests/cli/test_main_routing.py`
- [ ] **Step 1 : Écrire les tests unitaires du routing (failing)**
Créer `tests/cli/__init__.py` (vide).
Créer `tests/cli/test_main_routing.py` :
```python
import pytest
from unittest.mock import patch
from logimage.cli.main import main
from logimage.infrastructure.image.pexels_source import PexelsImageSource
from logimage.infrastructure.image.unsplash_source import UnsplashImageSource
def test_pexels_key_uses_pexels_source(monkeypatch) -> None:
monkeypatch.setenv("PEXELS_API_KEY", "pexels-test-key")
monkeypatch.delenv("UNSPLASH_ACCESS_KEY", raising=False)
with patch("logimage.cli.main.GeneratePuzzlesUseCase") as mock_uc, \
patch("logimage.cli.main.load_dotenv"), \
patch("sys.argv", ["logimage", "--count", "1"]):
mock_uc.return_value.execute.return_value = None
main()
assert isinstance(mock_uc.call_args.kwargs["image_source"], PexelsImageSource)
def test_unsplash_key_uses_unsplash_source(monkeypatch) -> None:
monkeypatch.delenv("PEXELS_API_KEY", raising=False)
monkeypatch.setenv("UNSPLASH_ACCESS_KEY", "unsplash-test-key")
with patch("logimage.cli.main.GeneratePuzzlesUseCase") as mock_uc, \
patch("logimage.cli.main.load_dotenv"), \
patch("sys.argv", ["logimage", "--count", "1"]):
mock_uc.return_value.execute.return_value = None
main()
assert isinstance(mock_uc.call_args.kwargs["image_source"], UnsplashImageSource)
def test_pexels_takes_priority_over_unsplash(monkeypatch) -> None:
monkeypatch.setenv("PEXELS_API_KEY", "pexels-test-key")
monkeypatch.setenv("UNSPLASH_ACCESS_KEY", "unsplash-test-key")
with patch("logimage.cli.main.GeneratePuzzlesUseCase") as mock_uc, \
patch("logimage.cli.main.load_dotenv"), \
patch("sys.argv", ["logimage", "--count", "1"]):
mock_uc.return_value.execute.return_value = None
main()
assert isinstance(mock_uc.call_args.kwargs["image_source"], PexelsImageSource)
def test_no_key_exits_with_error(monkeypatch, capsys) -> None:
monkeypatch.delenv("PEXELS_API_KEY", raising=False)
monkeypatch.delenv("UNSPLASH_ACCESS_KEY", raising=False)
with patch("logimage.cli.main.load_dotenv"), \
patch("sys.argv", ["logimage", "--count", "1"]):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "PEXELS_API_KEY" in captured.err
assert "UNSPLASH_ACCESS_KEY" in captured.err
```
- [ ] **Step 2 : Vérifier que les tests échouent**
```bash
pytest tests/cli/test_main_routing.py -v
```
Attendu : tests échouent (la logique de routing n'est pas encore implémentée)
- [ ] **Step 3 : Modifier `main.py` pour le routing**
Remplacer dans `src/logimage/cli/main.py` le bloc d'import et de construction du `image_source` :
```python
import argparse
import sys
from pathlib import Path
from dotenv import load_dotenv
import os
from logimage.application.use_cases.generate_puzzles import (
GeneratePuzzlesRequest,
GeneratePuzzlesUseCase,
)
from logimage.infrastructure.image.pexels_source import PexelsImageSource
from logimage.infrastructure.image.unsplash_source import UnsplashImageSource
from logimage.infrastructure.image.pillow_converter import PillowImageConverter
from logimage.infrastructure.pdf.reportlab_exporter import ReportLabPdfExporter
def _parse_size(value: str) -> tuple[int, int]:
parts = value.lower().split("x")
if len(parts) != 2:
raise argparse.ArgumentTypeError(f"Size must be WxH (e.g. 20x15), got '{value}'")
try:
w, h = int(parts[0]), int(parts[1])
except ValueError:
raise argparse.ArgumentTypeError(f"Width and height must be integers, got '{value}'")
return w, h
def main() -> None:
load_dotenv()
parser = argparse.ArgumentParser(
description="Generate nonogram (logimage) puzzles as a printable PDF."
)
parser.add_argument("--theme", help="Image search keyword (default: random)")
parser.add_argument(
"--difficulty",
choices=["easy", "medium", "hard"],
default="medium",
help="Grid size preset: easy=10×10, medium=15×15, hard=20×20 (default: medium)",
)
parser.add_argument(
"--size",
type=_parse_size,
metavar="WxH",
help="Custom grid size, e.g. 20x25 (overrides --difficulty)",
)
parser.add_argument("--count", type=int, default=1, help="Number of puzzles (default: 1)")
parser.add_argument(
"--solution",
action="store_true",
help="Append a solution page after each puzzle",
)
parser.add_argument(
"--output",
type=Path,
default=Path("puzzles.pdf"),
metavar="PATH",
help="Output PDF path (default: puzzles.pdf)",
)
args = parser.parse_args()
pexels_key = os.environ.get("PEXELS_API_KEY")
unsplash_key = os.environ.get("UNSPLASH_ACCESS_KEY")
if pexels_key:
image_source = PexelsImageSource(api_key=pexels_key)
elif unsplash_key:
image_source = UnsplashImageSource(api_key=unsplash_key)
else:
print(
"Error: no image API key found.\n"
"Set PEXELS_API_KEY or UNSPLASH_ACCESS_KEY in your .env file.",
file=sys.stderr,
)
sys.exit(1)
use_case = GeneratePuzzlesUseCase(
image_source=image_source,
image_converter=PillowImageConverter(),
pdf_exporter=ReportLabPdfExporter(),
)
request = GeneratePuzzlesRequest(
count=args.count,
difficulty=args.difficulty,
size=args.size,
theme=args.theme,
output_path=args.output,
with_solution=args.solution,
)
print(f"Generating {args.count} puzzle(s)…")
use_case.execute(request)
print(f"Done! PDF saved to: {args.output}")
```
- [ ] **Step 4 : Vérifier que les tests passent**
```bash
pytest tests/cli/test_main_routing.py -v
```
Attendu : 4 tests PASSED
- [ ] **Step 5 : Lancer la suite complète**
```bash
pytest tests/ -v --ignore=tests/infrastructure/test_pexels_source.py --ignore=tests/infrastructure/test_unsplash_source.py
```
Attendu : tous les tests passent (les tests d'intégration réseau sont exclus)
- [ ] **Step 6 : Commit**
```bash
git add src/logimage/cli/main.py tests/cli/__init__.py tests/cli/test_main_routing.py
git commit -m "feat: route image source based on available API key (Pexels first, Unsplash fallback)"
```