Skip to content

Commit f3b63d6

Browse files
committed
Addressing copilot code review comments
1 parent fc83031 commit f3b63d6

File tree

8 files changed

+116
-59
lines changed

8 files changed

+116
-59
lines changed

gpt_image_mcp/storage/manager.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,17 @@ def generate_task_id(self) -> str:
3434
async def store_image(
3535
self, image_id: str, image_data: Any, metadata: dict[str, Any]
3636
) -> None:
37-
"""Store image data (bytes or base64/data URL) and metadata."""
37+
"""Store image data with a provided image ID.
38+
39+
This method is primarily used in tests where you need to control
40+
the image ID. For production use, prefer save_image() which handles
41+
ID generation and organized storage.
42+
43+
Args:
44+
image_id: The specific image ID to use
45+
image_data: Image data as bytes or base64 data URL
46+
metadata: Image metadata dictionary
47+
"""
3848
# Determine format
3949
fmt = metadata.get("format", "png").lower()
4050
if isinstance(image_data, str) and image_data.startswith("data:image/"):
@@ -230,7 +240,20 @@ def get_metadata_path(self, image_id: str) -> Path:
230240
async def save_image(
231241
self, image_data: bytes, metadata: dict[str, Any], file_format: str = "png"
232242
) -> tuple[str, Path]:
233-
"""Save image data and metadata to storage."""
243+
"""Save image data to organized storage with auto-generated ID.
244+
245+
This is the primary method for saving images in production. It generates
246+
a unique image ID, uses date-organized directory structure, and includes
247+
comprehensive metadata with file information and image dimensions.
248+
249+
Args:
250+
image_data: Raw image data as bytes
251+
metadata: Image metadata dictionary
252+
file_format: Image file format (png, jpeg, webp)
253+
254+
Returns:
255+
Tuple of (image_id, image_path)
256+
"""
234257
image_id = self.generate_image_id()
235258
image_path = self.get_image_path(image_id, file_format)
236259
metadata_path = self.get_metadata_path(image_id)

gpt_image_mcp/tools/image_editing.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66
from typing import Any
77

8+
from ..config.settings import Settings
89
from ..storage.manager import ImageStorageManager
910
from ..utils.cache import CacheManager
1011
from ..utils.path_utils import build_image_url_path
@@ -19,29 +20,20 @@ def __init__(
1920
self,
2021
storage_manager: ImageStorageManager,
2122
cache_manager: CacheManager,
22-
settings,
23+
settings: Settings,
2324
openai_client=None,
2425
):
25-
# Accept either Settings or ImageSettings for test compatibility
26-
if hasattr(settings, "providers") and hasattr(settings, "images"):
27-
self.settings = settings
28-
else:
29-
# Wrap in dummy Settings
30-
class DummySettings:
31-
def __init__(self, images):
32-
self.images = images
33-
from types import SimpleNamespace
34-
35-
self.providers = SimpleNamespace(openai=None, gemini=None)
36-
self.storage = SimpleNamespace(base_path="./storage")
37-
self.server = SimpleNamespace(host="127.0.0.1", port=3001)
38-
39-
self.settings = DummySettings(settings)
40-
# Defensive: ensure openai is never None
26+
"""
27+
Args:
28+
storage_manager: ImageStorageManager instance.
29+
cache_manager: CacheManager instance.
30+
settings: Settings instance (must have .providers, .images, etc.).
31+
openai_client: Optional OpenAI client manager.
32+
"""
33+
self.settings = settings
34+
# Ensure openai provider settings are present
4135
if getattr(self.settings.providers, "openai", None) is None:
42-
from gpt_image_mcp.config.settings import OpenAISettings
43-
44-
self.settings.providers.openai = OpenAISettings(api_key="test-api-key")
36+
raise ValueError("OpenAI provider settings are missing in configuration.")
4537
self.storage_manager = storage_manager
4638
self.cache_manager = cache_manager
4739
self.openai_client = openai_client

gpt_image_mcp/tools/image_generation.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Any, Optional
77

8+
from ..config.settings import Settings
89
from ..providers.base import ProviderConfig, ProviderError
910
from ..providers.gemini import GeminiProvider
1011
from ..providers.openai import OpenAIProvider
@@ -31,24 +32,17 @@ def __init__(
3132
self,
3233
storage_manager: ImageStorageManager,
3334
cache_manager: CacheManager,
34-
settings,
35+
settings: Settings,
3536
openai_client=None,
3637
):
37-
# Accept either Settings or ImageSettings for test compatibility
38-
if hasattr(settings, "providers") and hasattr(settings, "images"):
39-
self.settings = settings
40-
else:
41-
# Wrap in dummy Settings
42-
class DummySettings:
43-
def __init__(self, images):
44-
self.images = images
45-
from types import SimpleNamespace
46-
47-
self.providers = SimpleNamespace(openai=None, gemini=None)
48-
self.storage = SimpleNamespace(base_path="./storage")
49-
self.server = SimpleNamespace(host="127.0.0.1", port=3001)
50-
51-
self.settings = DummySettings(settings)
38+
"""
39+
Args:
40+
storage_manager: ImageStorageManager instance.
41+
cache_manager: CacheManager instance.
42+
settings: Settings instance (must have .providers, .images, etc.).
43+
openai_client: Optional OpenAI client.
44+
"""
45+
self.settings = settings
5246
self.storage_manager = storage_manager
5347
self.cache_manager = cache_manager
5448
self.provider_registry = ProviderRegistry()
@@ -57,15 +51,11 @@ def __init__(self, images):
5751

5852
def _initialize_providers(self) -> None:
5953
"""Initialize and register all available providers."""
60-
# Defensive: ensure openai and gemini are never None
54+
# Ensure openai and gemini provider settings are present
6155
if getattr(self.settings.providers, "openai", None) is None:
62-
from gpt_image_mcp.config.settings import OpenAISettings
63-
64-
self.settings.providers.openai = OpenAISettings(api_key="test-api-key")
56+
raise ValueError("OpenAI provider settings are missing in configuration.")
6557
if getattr(self.settings.providers, "gemini", None) is None:
66-
from gpt_image_mcp.config.settings import GeminiSettings
67-
68-
self.settings.providers.gemini = GeminiSettings(api_key="test-gemini-key")
58+
raise ValueError("Gemini provider settings are missing in configuration.")
6959
# Initialize OpenAI provider
7060
if (
7161
self.settings.providers.openai.enabled

gpt_image_mcp/utils/openai_client.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,18 @@
1313
logger = logging.getLogger(__name__)
1414

1515

16-
# Dummy OpenAI class for patching in tests
17-
class OpenAI:
18-
pass
19-
2016

2117
class OpenAIClientManager:
22-
@property
23-
def client(self):
24-
return self._client
25-
2618
"""Manages OpenAI API client with retry logic and error handling."""
2719

2820
def __init__(self, settings: OpenAISettings):
2921
self.settings = settings
3022
self._client = self._create_client()
3123

24+
@property
25+
def client(self):
26+
return self._client
27+
3228
def _create_client(self):
3329
return AsyncOpenAI(
3430
api_key=self.settings.api_key,

gpt_image_mcp/utils/validators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def validate_base64_image(data: str) -> str:
313313
Raises:
314314
ValueError: If data is invalid
315315
"""
316-
if not data or not isinstance(data, str) or not data.strip():
316+
if not isinstance(data, str) or not data.strip():
317317
raise ValueError("Image data must be a non-empty string")
318318

319319
import base64

tests/unit/test_storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ async def test_background_cleanup_task(self, mock_storage_settings):
327327
"""Test background cleanup task functionality."""
328328
# Create manager with short cleanup interval for testing
329329
mock_storage_settings.cleanup_interval_hours = (
330-
0.00001 # Very short interval (0.036 seconds)
330+
0.001 # Short interval for testing (3.6 seconds)
331331
)
332332
manager = ImageStorageManager(mock_storage_settings)
333333

tests/unit/test_tools.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from gpt_image_mcp.config.settings import ProvidersSettings, Settings
56
from gpt_image_mcp.providers.base import ProviderError
67
from gpt_image_mcp.tools.image_editing import ImageEditingTool
78
from gpt_image_mcp.tools.image_generation import ImageGenerationTool
@@ -28,9 +29,9 @@ def mock_editing_tool(storage_manager, cache_manager, mock_settings):
2829
cache_manager=cache_manager,
2930
settings=mock_settings,
3031
)
31-
# Mock the OpenAI client with a mix of sync and async methods
32+
# Mock the OpenAI client manager (which has edit_image method)
3233
tool.openai_client = MagicMock()
33-
tool.openai_client.edit_image = AsyncMock() # This method is async
34+
tool.openai_client.edit_image = AsyncMock() # OpenAIClientManager method
3435
tool.openai_client.estimate_cost = MagicMock(
3536
return_value={"estimated_cost_usd": 0.01}
3637
) # This is sync
@@ -363,6 +364,42 @@ async def test_generate_image_storage_integration(
363364
assert result["image_id"] == "stored_id"
364365
storage_manager.save_image.assert_called_once()
365366

367+
def test_missing_openai_provider_configuration(
368+
self, storage_manager, cache_manager
369+
):
370+
"""Test that missing OpenAI provider configuration raises proper error."""
371+
# Create settings with missing OpenAI provider
372+
settings = Settings()
373+
settings.providers = ProvidersSettings() # Empty providers (no openai/gemini)
374+
375+
with pytest.raises(
376+
ValueError, match="OpenAI provider settings are missing in configuration"
377+
):
378+
ImageGenerationTool(
379+
storage_manager=storage_manager,
380+
cache_manager=cache_manager,
381+
settings=settings
382+
)
383+
384+
def test_missing_gemini_provider_configuration(
385+
self, storage_manager, cache_manager, mock_openai_settings
386+
):
387+
"""Test that missing Gemini provider configuration raises proper error."""
388+
# Create settings with only OpenAI provider (missing Gemini)
389+
settings = Settings()
390+
settings.providers = ProvidersSettings()
391+
settings.providers.openai = mock_openai_settings
392+
# Note: settings.providers.gemini remains None
393+
394+
with pytest.raises(
395+
ValueError, match="Gemini provider settings are missing in configuration"
396+
):
397+
ImageGenerationTool(
398+
storage_manager=storage_manager,
399+
cache_manager=cache_manager,
400+
settings=settings
401+
)
402+
366403

367404
class TestImageEditingTool:
368405
"""Unit tests for the ImageEditingTool."""
@@ -583,3 +620,20 @@ async def test_edit_image_storage_integration(
583620

584621
assert result["image_id"] == "edited_id"
585622
storage_manager.save_image.assert_called_once()
623+
624+
def test_missing_openai_provider_configuration(
625+
self, storage_manager, cache_manager
626+
):
627+
"""Test that missing OpenAI provider configuration raises proper error."""
628+
# Create settings with missing OpenAI provider
629+
settings = Settings()
630+
settings.providers = ProvidersSettings() # Empty providers (no openai)
631+
632+
with pytest.raises(
633+
ValueError, match="OpenAI provider settings are missing in configuration"
634+
):
635+
ImageEditingTool(
636+
storage_manager=storage_manager,
637+
cache_manager=cache_manager,
638+
settings=settings
639+
)

tests/unit/test_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,10 @@ def test_openai_client_manager_creation(self, mock_openai_settings):
531531
assert hasattr(manager, "_client")
532532

533533
@patch("gpt_image_mcp.utils.openai_client.AsyncOpenAI")
534-
def test_openai_client_creation(self, mock_openai_class, mock_openai_settings):
535-
"""Test OpenAI client creation with proper settings."""
534+
def test_async_openai_client_creation(
535+
self, mock_openai_class, mock_openai_settings
536+
):
537+
"""Test AsyncOpenAI client creation with proper settings."""
536538
mock_client = MagicMock()
537539
mock_openai_class.return_value = mock_client
538540

0 commit comments

Comments
 (0)