Skip to content

Commit

Permalink
chore(imaging): make imaging an extra
Browse files Browse the repository at this point in the history
  • Loading branch information
helmut-hoffer-von-ankershoffen committed Dec 26, 2024
1 parent c4216dc commit ffac1da
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 87 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test-and-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:

- name: Install Python, venv and dependencies
run: |
uv sync --frozen --link-mode=copy
uv sync --all-extras --frozen --link-mode=copy
- name: Release version check
if: startsWith(github.ref, 'refs/tags/v')
Expand Down Expand Up @@ -110,7 +110,7 @@ jobs:
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}

- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v4
env:
Expand Down
8 changes: 6 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.gibberish.should_exclude_secret",
"limit": 3.7
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
Expand Down Expand Up @@ -143,7 +147,7 @@
"filename": "src/starbridge/hello/service.py",
"hashed_secret": "b96ff7d04dbdf5211aa6bf8ac3a557f30dc7469b",
"is_verified": false,
"line_number": 123,
"line_number": 133,
"is_secret": false
}
],
Expand All @@ -158,5 +162,5 @@
}
]
},
"generated_at": "2024-12-23T17:24:38Z"
"generated_at": "2024-12-26T16:25:38Z"
}
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
nox.options.reuse_existing_virtualenvs = True
nox.options.default_venv_backend = "uv"

_INSTALL_ARGS = "-e .[dev]"
_INSTALL_ARGS = "-e .[dev,imaging]"


@nox.session(python=["3.11", "3.12", "3.13"])
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ dependencies = [
"griffe>=1.5.1",
"typer>=0.15.1",
"atlassian-python-api>=3.41.16",
"cairosvg>=2.7.1",
"python-dotenv>=1.0.1",
"svglib>=1.5.1",
"logfire[system-metrics]>=2.11.0",
"opentelemetry-api>=1.29.0",
"opentelemetry-instrumentation>=0.50b0",
Expand Down Expand Up @@ -109,6 +107,7 @@ dev = [
]

[project.optional-dependencies]
imaging = ["cairosvg>=2.7.1", "svglib>=1.5.1"]
# used by uv pip install -e ".[dev]", as used within nox (uv run nox)
dev = [
"bump-my-version>=0.29.0",
Expand Down
62 changes: 34 additions & 28 deletions src/starbridge/hello/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,40 @@ def hello(locale: Annotated[str, typer.Option(help="Locale to use")] = "en_US")
console.print(Service().hello(locale))


@cli.command()
def bridge(
dump: Annotated[
bool,
typer.Option(
help="If set, will dump to file starbridge.png in current working directory. Defaults to opening viewer to show the image."
),
] = False,
) -> None:
"""Show image of starbridge"""
try:
image = Service().bridge()
if dump:
image.save("starbridge.png")
else:
image.show()
except OSError:
text = Text()
text.append("Please follow setup instructions for starbridge ")
text.append("Install the library needed for image manipulation using:\n")
text.append("• macOS: ", style="yellow")
text.append("brew install cairo\n")
text.append("• Linux: ", style="yellow")
text.append("sudo apt-get install libcairo2")
console.print(
Panel(text, title="Setup Required: Cairo not found", border_style="red")
)
sys.exit(78)
if hasattr(Service, "bridge"):

@cli.command()
def bridge(
dump: Annotated[
bool,
typer.Option(
help="If set, will dump to file starbridge.png in current working directory. Defaults to opening viewer to show the image."
),
] = False,
) -> None:
"""Show image of starbridge"""
try:
image = Service().bridge()
if dump:
image.save("starbridge.png")
else:
image.show()
except OSError:
text = Text()
text.append("Please follow setup instructions for starbridge ")
text.append("Install the library needed for image manipulation using:\n")
text.append("• macOS: ", style="yellow")
text.append("brew install cairo\n")
text.append("• Linux: ", style="yellow")
text.append("sudo apt-get install libcairo2")
console.print(
Panel(
text,
title="Setup Required: Cairo not found",
border_style="red",
)
)
sys.exit(78)


@cli.command()
Expand Down
40 changes: 25 additions & 15 deletions src/starbridge/hello/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@

import typer
from mcp.types import BlobResourceContents, EmbeddedResource
from PIL import Image

try:
from PIL import Image

_has_imaging = True
except ImportError:
_has_imaging = False

from pydantic import AnyUrl

from starbridge.mcp import MCPBaseService, MCPContext, mcp_tool
Expand Down Expand Up @@ -45,15 +52,18 @@ def hello(self, locale: str = "en_US", context: MCPContext | None = None):
return "Hallo Welt!"
return "Hello World!"

@mcp_tool()
def bridge(self, context: MCPContext | None = None):
"""Show image of starbridge"""
import cairosvg
import cairosvg.svg
if _has_imaging:

return Image.open(
io.BytesIO(cairosvg.svg2png(bytestring=Service._starbridge_svg()) or b"")
)
@mcp_tool()
def bridge(self, context: MCPContext | None = None):
"""Show image of starbridge"""
import cairosvg

return Image.open(
io.BytesIO(
cairosvg.svg2png(bytestring=Service._starbridge_svg()) or b""
)
)

@mcp_tool()
def pdf(self, context: MCPContext | None = None) -> EmbeddedResource:
Expand Down Expand Up @@ -84,7 +94,7 @@ def _starbridge_svg() -> str:
return """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<!-- Background -->
<rect width="256" height="256" fill="#1a1a2e"/>
<!-- Stars in background -->
<circle cx="30" cy="40" r="1" fill="white"/>
<circle cx="80" cy="30" r="1.5" fill="white"/>
Expand All @@ -93,26 +103,26 @@ def _starbridge_svg() -> str:
<circle cx="220" cy="60" r="1" fill="white"/>
<circle cx="50" cy="70" r="1" fill="white"/>
<circle cx="180" cy="80" r="1.5" fill="white"/>
<!-- Bridge structure -->
<!-- Left support -->
<path d="M40 180 L60 100 L80 180" fill="#4a4e69"/>
<!-- Right support -->
<path d="M176 180 L196 100 L216 180" fill="#4a4e69"/>
<!-- Bridge deck -->
<path d="M30 180 L226 180" stroke="#9a8c98" stroke-width="8" fill="none"/>
<!-- Suspension cables -->
<path d="M60 100 C128 50, 128 50, 196 100" stroke="#c9ada7" stroke-width="3" fill="none"/>
<path d="M60 100 L80 180" stroke="#c9ada7" stroke-width="2" fill="none"/>
<path d="M196 100 L176 180" stroke="#c9ada7" stroke-width="2" fill="none"/>
<!-- Star decorations -->
<path d="M128 70 L132 62 L140 60 L132 58 L128 50 L124 58 L116 60 L124 62 Z" fill="#ffd700"/>
<path d="M60 95 L62 91 L66 90 L62 89 L60 85 L58 89 L54 90 L58 91 Z" fill="#ffd700"/>
<path d="M196 95 L198 91 L202 90 L198 89 L196 85 L194 89 L190 90 L194 91 Z" fill="#ffd700"/>
<!-- Reflection in water -->
<path d="M40 180 L60 220 L80 180 M176 180 L196 220 L216 180" fill="#1a1a2e" opacity="0.3"/>
<path d="M60 100 C128 150, 128 150, 196 100" stroke="#c9ada7" stroke-width="1" fill="none" opacity="0.2"/>
Expand Down
11 changes: 9 additions & 2 deletions src/starbridge/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@
ImageContent,
TextContent,
)
from PIL import Image

try:
from PIL import Image

_has_imaging = True
except ImportError:
_has_imaging = False

from pydantic import AnyUrl
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
Expand Down Expand Up @@ -327,7 +334,7 @@ def _marshal_result(
if isinstance(result, str):
return [TextContent(type="text", text=result)]

if isinstance(result, Image.Image):
if _has_imaging and isinstance(result, Image.Image):
mime_type = (
Image.MIME.get(result.format, "application/octet-stream")
if result.format
Expand Down
34 changes: 20 additions & 14 deletions tests/starbridge_cli_hello_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@

from starbridge.cli import cli

try:
from starbridge.hello.cli import bridge
except ImportError:
bridge = None


@pytest.fixture
def runner():
return CliRunner()


def test_hello_bridge(runner):
"""Check we dump the image."""
with runner.isolated_filesystem():
result = runner.invoke(cli, ["hello", "bridge", "--dump"])
assert result.exit_code == 0
assert Path("starbridge.png").is_file()
assert Path("starbridge.png").stat().st_size == 6235
if bridge: # if extra imaging

def test_hello_bridge(runner):
"""Check we dump the image."""
with runner.isolated_filesystem():
result = runner.invoke(cli, ["hello", "bridge", "--dump"])
assert result.exit_code == 0
assert Path("starbridge.png").is_file()
assert Path("starbridge.png").stat().st_size == 6235

@patch("cairosvg.svg2png", side_effect=OSError)
def test_hello_bridge_error(mock_svg2png, runner):
"""Check we handle cairo missing."""
result = runner.invoke(cli, ["hello", "bridge"])
assert result.exit_code == 78


def test_hello_pdf(runner):
Expand All @@ -44,10 +57,3 @@ def test_hello_pdf_open(mock_run, runner):
assert args[0][0] == "xdg-open"
assert str(args[0][1]).endswith(".pdf")
assert kwargs["check"] is True


@patch("cairosvg.svg2png", side_effect=OSError)
def test_hello_bridge_error(mock_svg2png, runner):
"""Check we handle cairo missing."""
result = runner.invoke(cli, ["hello", "bridge"])
assert result.exit_code == 78
42 changes: 26 additions & 16 deletions tests/starbridge_mcp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
from pydantic import AnyUrl
from typer.testing import CliRunner

from starbridge.hello import Service as HelloService

try:
from starbridge.hello.cli import bridge
except ImportError:
bridge = None

MOCK_GET_ALL_SPACES = "atlassian.Confluence.get_all_spaces"
MOCK_GET_SPACE = "atlassian.Confluence.get_space"
PYPROJECT_TOML = "pyproject.toml"
Expand Down Expand Up @@ -68,12 +75,13 @@ async def test_mcp_server_list_tools():
"starbridge_confluence_page_list",
"starbridge_confluence_page_update",
"starbridge_confluence_space_list",
"starbridge_hello_bridge",
"starbridge_hello_health",
"starbridge_hello_hello",
"starbridge_hello_info",
"starbridge_hello_pdf",
]
if bridge:
expected_tools.append("starbridge_hello_bridge")

async with stdio_client(_server_parameters()) as (read, write):
async with ClientSession(read, write) as session:
Expand Down Expand Up @@ -275,22 +283,24 @@ async def test_mcp_server_tool_call():
assert content.text == "Hallo Welt!"


@pytest.mark.asyncio
async def test_mcp_server_tool_call_with_image():
"""Test listing of prompts from the server"""
async with stdio_client(_server_parameters()) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()
if hasattr(HelloService, "bridge"): # if extra imaging

# List available prompts
result = await session.call_tool("starbridge_hello_bridge", {})
assert len(result.content) == 1
content = result.content[0]
assert type(content) is ImageContent
assert content.data == base64.b64encode(
Path("tests/fixtures/starbridge.png").read_bytes()
).decode("utf-8")
@pytest.mark.asyncio
async def test_mcp_server_tool_call_with_image():
"""Test listing of prompts from the server"""
async with stdio_client(_server_parameters()) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection
await session.initialize()

# List available prompts
result = await session.call_tool("starbridge_hello_bridge", {})
assert len(result.content) == 1
content = result.content[0]
assert type(content) is ImageContent
assert content.data == base64.b64encode(
Path("tests/fixtures/starbridge.png").read_bytes()
).decode("utf-8")


@pytest.mark.asyncio
Expand Down
Loading

0 comments on commit ffac1da

Please sign in to comment.