hanzo-tools-gimp 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ # IDE and editor
2
+ .vscode
3
+ .idea
4
+
5
+ # Python
6
+ *.egg-info
7
+ __pycache__
8
+ .mypy_cache
9
+ .pytest_cache
10
+ *.pyc
11
+ .venv
12
+
13
+ # Build
14
+ build/
15
+ dist
16
+ _dev
17
+
18
+ # Environment
19
+ .env
20
+ .envrc
21
+
22
+ # Logs
23
+ .prism.log
24
+ codegen.log
25
+ *.log
26
+
27
+ # Package manager
28
+ Brewfile.lock.json
29
+
30
+ # Agent config files (symlinked from user home)
31
+ LLM.md
32
+ AGENTS.md
33
+ CLAUDE.md
34
+ GEMINI.md
35
+ GROK.md
36
+ QWEN.md
37
+
38
+ # Local databases and state
39
+ .hanzo/
40
+ .grok/
41
+
42
+ # Documentation build
43
+ docs/.next/
44
+ docs/out/
45
+ docs/node_modules/
46
+
47
+ # Training data and scripts (DO NOT COMMIT)
48
+ training_dataset.jsonl
49
+ scripts/full_extractor.py
50
+ scripts/mega_extractor.py
51
+ scripts/mega_full_extractor.py
52
+ scripts/streaming_extractor.py
53
+ scripts/supplement_extractor.py
54
+
55
+ # Test files at root (experimental)
56
+ test_post_quantum_*.py
57
+
58
+ # Analysis documents (internal)
59
+ HANZO_INNOVATION_OPPORTUNITIES.md
60
+ POST_QUANTUM_CRYPTOGRAPHY_IMPLEMENTATION.md
61
+
62
+ # Experimental cryptography (WIP)
63
+ pkg/hanzo/src/hanzo/cryptography/
64
+ site/
65
+
66
+ # hygiene (untrack node_modules, block common build output)
67
+ node_modules/
68
+ **/node_modules/
69
+ .pnpm-store/
70
+ dist/
71
+ .next/
72
+ coverage/
73
+ playwright-report/
74
+ test-results/
75
+ tmp/
76
+ .DS_Store
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: hanzo-tools-gimp
3
+ Version: 1.0.0
4
+ Summary: Unified GIMP automation tool for Hanzo AI (HIP-0300) — clean-room BSD-3 client for the GIMP PDB bridge
5
+ Project-URL: Homepage, https://github.com/hanzoai/python-sdk
6
+ Project-URL: Documentation, https://docs.hanzo.ai
7
+ Project-URL: Repository, https://github.com/hanzoai/python-sdk
8
+ Author-email: Hanzo AI <dev@hanzo.ai>
9
+ License: BSD-3-Clause
10
+ Keywords: automation,gimp,hanzo,image,mcp,pdb,tools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Multimedia :: Graphics
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: hanzo-tools>=0.3.0
19
+ Requires-Dist: mcp>=1.0.0
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # hanzo-tools-gimp
28
+
29
+ Unified **GIMP automation** tool for Hanzo AI (HIP-0300).
30
+
31
+ Drives [GIMP](https://www.gimp.org/) 3.0 through the **Hanzo GIMP bridge** — a
32
+ clean-room, **BSD-3** GIMP plug-in (see `hanzo/gimp-mcp`) that exposes GIMP's
33
+ Procedural Database (PDB) over a line-oriented JSON TCP socket. This package is
34
+ the Python client for that bridge.
35
+
36
+ This is a clean-room implementation with **no GPL lineage**: both the bridge
37
+ and this client use GIMP's documented, public PDB / GObject-Introspection API.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ pip install hanzo-tools-gimp
43
+ ```
44
+
45
+ ## Prerequisites
46
+
47
+ The Hanzo GIMP bridge plug-in must be running inside GIMP. Briefly:
48
+
49
+ 1. Copy `hanzo_gimp_bridge.py` (from `hanzo/gimp-mcp`) into your GIMP 3.0
50
+ plug-ins directory (one folder per plug-in, script made executable).
51
+ 2. Start GIMP and run **Filters → Hanzo → Start Hanzo Bridge**.
52
+ 3. The bridge listens on `127.0.0.1:9876` by default (override with
53
+ `HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`).
54
+
55
+ See the `gimp-mcp` repo's `README.md` and `PROTOCOL.md` for full details.
56
+
57
+ ## Usage
58
+
59
+ ```python
60
+ from hanzo_tools.gimp import GimpTool, register_tools, TOOLS
61
+
62
+ # Register with a FastMCP server
63
+ register_tools(mcp_server) # optional host=..., port=...
64
+
65
+ # Or use the tool directly
66
+ tool = GimpTool(host="127.0.0.1", port=9876)
67
+ ```
68
+
69
+ The tool exposes the same action surface as the Hanzo MCP TypeScript `gimp`
70
+ tool:
71
+
72
+ | Action | Params | Result |
73
+ |-------------------|---------------------------------|--------|
74
+ | `version` | — | `{bridge, gimp, protocol}` |
75
+ | `open` | `path` | `{image_id, width, height}` |
76
+ | `export` | `image_id`, `path` | `{image_id, path, saved}` |
77
+ | `pdb_call` | `procedure`, `args` | the procedure's return value(s) |
78
+ | `new_image` | `width`, `height` | `{image_id, width, height}` |
79
+ | `image_info` | `image_id` | `{image_id, width, height, ...}` |
80
+ | `list_procedures` | `prefix?` | `{count, procedures}` |
81
+ | `flatten` | `image_id` | `{image_id, layer_id}` |
82
+
83
+ Every action also accepts `host` / `port` overrides.
84
+
85
+ `pdb_call` is the generic escape hatch — it can invoke **any** PDB procedure,
86
+ so anything GIMP can do is reachable.
87
+
88
+ ## License
89
+
90
+ **BSD-3-Clause**. Copyright (c) 2026 Hanzo AI.
@@ -0,0 +1,64 @@
1
+ # hanzo-tools-gimp
2
+
3
+ Unified **GIMP automation** tool for Hanzo AI (HIP-0300).
4
+
5
+ Drives [GIMP](https://www.gimp.org/) 3.0 through the **Hanzo GIMP bridge** — a
6
+ clean-room, **BSD-3** GIMP plug-in (see `hanzo/gimp-mcp`) that exposes GIMP's
7
+ Procedural Database (PDB) over a line-oriented JSON TCP socket. This package is
8
+ the Python client for that bridge.
9
+
10
+ This is a clean-room implementation with **no GPL lineage**: both the bridge
11
+ and this client use GIMP's documented, public PDB / GObject-Introspection API.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install hanzo-tools-gimp
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ The Hanzo GIMP bridge plug-in must be running inside GIMP. Briefly:
22
+
23
+ 1. Copy `hanzo_gimp_bridge.py` (from `hanzo/gimp-mcp`) into your GIMP 3.0
24
+ plug-ins directory (one folder per plug-in, script made executable).
25
+ 2. Start GIMP and run **Filters → Hanzo → Start Hanzo Bridge**.
26
+ 3. The bridge listens on `127.0.0.1:9876` by default (override with
27
+ `HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`).
28
+
29
+ See the `gimp-mcp` repo's `README.md` and `PROTOCOL.md` for full details.
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from hanzo_tools.gimp import GimpTool, register_tools, TOOLS
35
+
36
+ # Register with a FastMCP server
37
+ register_tools(mcp_server) # optional host=..., port=...
38
+
39
+ # Or use the tool directly
40
+ tool = GimpTool(host="127.0.0.1", port=9876)
41
+ ```
42
+
43
+ The tool exposes the same action surface as the Hanzo MCP TypeScript `gimp`
44
+ tool:
45
+
46
+ | Action | Params | Result |
47
+ |-------------------|---------------------------------|--------|
48
+ | `version` | — | `{bridge, gimp, protocol}` |
49
+ | `open` | `path` | `{image_id, width, height}` |
50
+ | `export` | `image_id`, `path` | `{image_id, path, saved}` |
51
+ | `pdb_call` | `procedure`, `args` | the procedure's return value(s) |
52
+ | `new_image` | `width`, `height` | `{image_id, width, height}` |
53
+ | `image_info` | `image_id` | `{image_id, width, height, ...}` |
54
+ | `list_procedures` | `prefix?` | `{count, procedures}` |
55
+ | `flatten` | `image_id` | `{image_id, layer_id}` |
56
+
57
+ Every action also accepts `host` / `port` overrides.
58
+
59
+ `pdb_call` is the generic escape hatch — it can invoke **any** PDB procedure,
60
+ so anything GIMP can do is reachable.
61
+
62
+ ## License
63
+
64
+ **BSD-3-Clause**. Copyright (c) 2026 Hanzo AI.
@@ -0,0 +1,4 @@
1
+ # Namespace package
2
+ from pkgutil import extend_path
3
+
4
+ __path__ = extend_path(__path__, __name__)
@@ -0,0 +1,64 @@
1
+ """GIMP automation tools for Hanzo AI (HIP-0300).
2
+
3
+ Drives GIMP 3.0 through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP
4
+ plug-in that exposes GIMP's Procedural Database (PDB) over a JSON TCP socket.
5
+ No GPL lineage.
6
+
7
+ Tools:
8
+ - gimp: Unified GIMP automation tool (HIP-0300)
9
+ - version: Bridge + GIMP version
10
+ - open: Load an image file
11
+ - export: Export/save an image
12
+ - pdb_call: Invoke any PDB procedure (the core power)
13
+ - new_image: Create a blank RGB image
14
+ - image_info: Inspect an image
15
+ - list_procedures: Enumerate PDB procedures
16
+ - flatten: Flatten an image
17
+
18
+ Install:
19
+ pip install hanzo-tools-gimp
20
+
21
+ Usage:
22
+ from hanzo_tools.gimp import register_tools, TOOLS, GimpTool
23
+
24
+ # Register with an MCP server
25
+ register_tools(mcp_server)
26
+
27
+ # Or use the unified tool directly
28
+ tool = GimpTool(host="127.0.0.1", port=9876)
29
+
30
+ Requires the Hanzo GIMP bridge plug-in (hanzo/gimp-mcp) running inside GIMP.
31
+ """
32
+
33
+ from hanzo_tools.core import BaseTool, ToolRegistry
34
+
35
+ from .gimp_tool import DEFAULT_HOST, DEFAULT_PORT, GimpTool, gimp_tool
36
+
37
+ # Export list for tool discovery - HIP-0300 unified tool
38
+ TOOLS = [GimpTool]
39
+
40
+ __all__ = [
41
+ "GimpTool",
42
+ "gimp_tool",
43
+ "DEFAULT_HOST",
44
+ "DEFAULT_PORT",
45
+ "register_tools",
46
+ "TOOLS",
47
+ ]
48
+
49
+
50
+ def register_tools(mcp_server, **kwargs) -> list[BaseTool]:
51
+ """Register gimp tools with the MCP server.
52
+
53
+ Args:
54
+ mcp_server: The FastMCP server instance.
55
+ **kwargs: Optional ``host`` / ``port`` for the GIMP bridge.
56
+
57
+ Returns:
58
+ List of registered tool instances.
59
+ """
60
+ host = kwargs.get("host")
61
+ port = kwargs.get("port")
62
+ tool = GimpTool(host=host, port=port)
63
+ ToolRegistry.register_tool(mcp_server, tool)
64
+ return [tool]
@@ -0,0 +1,299 @@
1
+ """Unified GIMP automation tool for the HIP-0300 architecture.
2
+
3
+ This module provides a single unified ``gimp`` tool that drives GIMP 3.0
4
+ through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP plug-in that exposes
5
+ GIMP's Procedural Database (PDB) over a line-oriented JSON TCP socket.
6
+
7
+ Actions (matching the TypeScript ``gimp`` tool):
8
+ - version: bridge + GIMP version
9
+ - open: load an image file
10
+ - export: export/save an image to a path
11
+ - pdb_call: invoke any PDB procedure (the core power)
12
+ - new_image: create a blank RGB image
13
+ - image_info: inspect an image
14
+ - list_procedures: enumerate PDB procedures (optional prefix filter)
15
+ - flatten: flatten an image
16
+
17
+ Following Unix philosophy: one tool for the GIMP-automation axis. No GPL
18
+ lineage -- the bridge and this client speak GIMP's documented public API.
19
+ """
20
+
21
+ import asyncio
22
+ import json
23
+ import os
24
+ from typing import Any, ClassVar
25
+
26
+ from mcp.server.fastmcp import Context as MCPContext
27
+
28
+ from hanzo_tools.core import (
29
+ BaseTool,
30
+ InvalidParamsError,
31
+ ToolError,
32
+ )
33
+
34
+ DEFAULT_HOST = "127.0.0.1"
35
+ DEFAULT_PORT = 9876
36
+
37
+
38
+ class GimpTool(BaseTool):
39
+ """Unified GIMP automation tool (HIP-0300).
40
+
41
+ Connects to the Hanzo GIMP bridge over TCP and drives GIMP through its
42
+ PDB. The bridge default endpoint is ``127.0.0.1:9876``; every action
43
+ accepts ``host`` / ``port`` overrides.
44
+
45
+ Actions: version, open, export, pdb_call, new_image, image_info,
46
+ list_procedures, flatten.
47
+ """
48
+
49
+ name: ClassVar[str] = "gimp"
50
+ VERSION: ClassVar[str] = "1.0.0"
51
+
52
+ def __init__(self, host: str | None = None, port: int | None = None):
53
+ super().__init__()
54
+ self.host = host or os.environ.get("HANZO_GIMP_HOST", DEFAULT_HOST)
55
+ env_port = os.environ.get("HANZO_GIMP_PORT")
56
+ self.port = port or (int(env_port) if env_port else DEFAULT_PORT)
57
+ self._register_gimp_actions()
58
+
59
+ @property
60
+ def description(self) -> str:
61
+ return """Unified GIMP automation tool (HIP-0300).
62
+
63
+ Drives GIMP 3.0 via the clean-room BSD-3 bridge (JSON-over-TCP to the GIMP PDB).
64
+
65
+ Actions:
66
+ - version: Bridge and GIMP version
67
+ - open: Load an image file (returns image_id, width, height)
68
+ - export: Export/save an image to a path
69
+ - pdb_call: Invoke any PDB procedure -- the generic core power
70
+ - new_image: Create a blank RGB image
71
+ - image_info: Inspect an image (size, layers, precision, file, dirty)
72
+ - list_procedures: Enumerate PDB procedures (optional prefix filter)
73
+ - flatten: Flatten an image
74
+
75
+ Requires the Hanzo GIMP bridge plug-in running inside GIMP
76
+ (default 127.0.0.1:9876).
77
+ """
78
+
79
+ async def _bridge_request(
80
+ self,
81
+ method: str,
82
+ params: dict[str, Any],
83
+ host: str | None = None,
84
+ port: int | None = None,
85
+ timeout: float = 60.0,
86
+ ) -> Any:
87
+ """Send one JSON request to the GIMP bridge and return its result.
88
+
89
+ Opens a short-lived connection, writes one newline-terminated JSON
90
+ request, reads one newline-terminated JSON reply.
91
+
92
+ Raises:
93
+ ToolError: on connection failure, timeout, malformed reply, or an
94
+ error response from the bridge.
95
+ """
96
+ host = host or self.host
97
+ port = port or self.port
98
+ request = json.dumps({"id": 1, "method": method, "params": params}) + "\n"
99
+
100
+ try:
101
+ reader, writer = await asyncio.wait_for(
102
+ asyncio.open_connection(host, port), timeout=timeout
103
+ )
104
+ except (OSError, asyncio.TimeoutError) as exc:
105
+ raise ToolError(
106
+ code="NOT_FOUND",
107
+ message=f"GIMP bridge connection failed ({host}:{port}): {exc}",
108
+ details={"host": host, "port": port},
109
+ )
110
+
111
+ try:
112
+ writer.write(request.encode("utf-8"))
113
+ await writer.drain()
114
+ line = await asyncio.wait_for(reader.readline(), timeout=timeout)
115
+ except asyncio.TimeoutError:
116
+ raise ToolError(code="TIMEOUT", message="GIMP bridge timed out")
117
+ finally:
118
+ writer.close()
119
+ try:
120
+ await writer.wait_closed()
121
+ except Exception:
122
+ pass
123
+
124
+ if not line:
125
+ raise ToolError(
126
+ code="INTERNAL_ERROR", message="GIMP bridge closed without replying"
127
+ )
128
+
129
+ try:
130
+ msg = json.loads(line.decode("utf-8"))
131
+ except json.JSONDecodeError as exc:
132
+ raise ToolError(
133
+ code="INTERNAL_ERROR",
134
+ message=f"Invalid response from GIMP bridge: {exc}",
135
+ )
136
+
137
+ if isinstance(msg, dict) and msg.get("error"):
138
+ err = msg["error"]
139
+ raise ToolError(
140
+ code="INTERNAL_ERROR",
141
+ message=err.get("message", "GIMP bridge error"),
142
+ details={"type": err.get("type")},
143
+ )
144
+
145
+ return msg.get("result") if isinstance(msg, dict) else msg
146
+
147
+ def _register_gimp_actions(self):
148
+ """Register all GIMP actions."""
149
+
150
+ @self.action("version", "Get bridge and GIMP version")
151
+ async def version(
152
+ ctx: MCPContext,
153
+ host: str | None = None,
154
+ port: int | None = None,
155
+ ) -> dict:
156
+ """Return the bridge version, GIMP version, and protocol id."""
157
+ return await self._bridge_request("version", {}, host, port)
158
+
159
+ @self.action("open", "Open (load) an image file")
160
+ async def open_image(
161
+ ctx: MCPContext,
162
+ path: str,
163
+ host: str | None = None,
164
+ port: int | None = None,
165
+ ) -> dict:
166
+ """Load an image from disk.
167
+
168
+ Args:
169
+ path: Filesystem path to the image.
170
+
171
+ Returns:
172
+ ``{image_id, width, height}``.
173
+ """
174
+ if not path:
175
+ raise InvalidParamsError("path required", param="path")
176
+ return await self._bridge_request("open_image", {"path": path}, host, port)
177
+
178
+ @self.action("export", "Export/save an image to a path")
179
+ async def export(
180
+ ctx: MCPContext,
181
+ image_id: int,
182
+ path: str,
183
+ host: str | None = None,
184
+ port: int | None = None,
185
+ ) -> dict:
186
+ """Export an image; the format is chosen by the file extension.
187
+
188
+ Args:
189
+ image_id: GIMP image id.
190
+ path: Destination path (e.g. ``out.png``, ``out.xcf``).
191
+ """
192
+ if image_id is None:
193
+ raise InvalidParamsError("image_id required", param="image_id")
194
+ if not path:
195
+ raise InvalidParamsError("path required", param="path")
196
+ return await self._bridge_request(
197
+ "export", {"image_id": image_id, "path": path}, host, port
198
+ )
199
+
200
+ @self.action("pdb_call", "Invoke any PDB procedure (generic core power)")
201
+ async def pdb_call(
202
+ ctx: MCPContext,
203
+ procedure: str,
204
+ args: list | None = None,
205
+ host: str | None = None,
206
+ port: int | None = None,
207
+ ) -> Any:
208
+ """Invoke an arbitrary GIMP PDB procedure.
209
+
210
+ Anything GIMP can do is reachable here. Integer ids may be passed
211
+ where a procedure expects a GIMP object (image/drawable/layer/...).
212
+
213
+ Args:
214
+ procedure: PDB procedure name, e.g. ``gimp-image-flatten``.
215
+ args: Positional arguments in PDB order.
216
+ """
217
+ if not procedure:
218
+ raise InvalidParamsError("procedure required", param="procedure")
219
+ return await self._bridge_request(
220
+ "pdb_call", {"procedure": procedure, "args": args or []}, host, port
221
+ )
222
+
223
+ @self.action("new_image", "Create a blank RGB image")
224
+ async def new_image(
225
+ ctx: MCPContext,
226
+ width: int,
227
+ height: int,
228
+ host: str | None = None,
229
+ port: int | None = None,
230
+ ) -> dict:
231
+ """Create a new blank RGB image.
232
+
233
+ Args:
234
+ width: Image width in pixels.
235
+ height: Image height in pixels.
236
+
237
+ Returns:
238
+ ``{image_id, width, height}``.
239
+ """
240
+ if width is None or height is None:
241
+ raise InvalidParamsError(
242
+ "width and height required", param="width"
243
+ )
244
+ return await self._bridge_request(
245
+ "new_image", {"width": width, "height": height}, host, port
246
+ )
247
+
248
+ @self.action("image_info", "Inspect an image")
249
+ async def image_info(
250
+ ctx: MCPContext,
251
+ image_id: int,
252
+ host: str | None = None,
253
+ port: int | None = None,
254
+ ) -> dict:
255
+ """Return image size, layers, precision, source file, and dirty flag."""
256
+ if image_id is None:
257
+ raise InvalidParamsError("image_id required", param="image_id")
258
+ return await self._bridge_request(
259
+ "image_info", {"image_id": image_id}, host, port
260
+ )
261
+
262
+ @self.action("list_procedures", "Enumerate PDB procedures")
263
+ async def list_procedures(
264
+ ctx: MCPContext,
265
+ prefix: str | None = None,
266
+ host: str | None = None,
267
+ port: int | None = None,
268
+ ) -> dict:
269
+ """List PDB procedure names, optionally filtered by ``prefix``.
270
+
271
+ Returns:
272
+ ``{count, procedures: [...]}``.
273
+ """
274
+ params = {"prefix": prefix} if prefix else {}
275
+ return await self._bridge_request(
276
+ "list_procedures", params, host, port
277
+ )
278
+
279
+ @self.action("flatten", "Flatten an image")
280
+ async def flatten(
281
+ ctx: MCPContext,
282
+ image_id: int,
283
+ host: str | None = None,
284
+ port: int | None = None,
285
+ ) -> dict:
286
+ """Flatten all layers of an image into one.
287
+
288
+ Returns:
289
+ ``{image_id, layer_id}``.
290
+ """
291
+ if image_id is None:
292
+ raise InvalidParamsError("image_id required", param="image_id")
293
+ return await self._bridge_request(
294
+ "flatten", {"image_id": image_id}, host, port
295
+ )
296
+
297
+
298
+ # Backward compatibility / convenience handle.
299
+ gimp_tool = GimpTool
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hanzo-tools-gimp"
7
+ version = "1.0.0"
8
+ description = "Unified GIMP automation tool for Hanzo AI (HIP-0300) — clean-room BSD-3 client for the GIMP PDB bridge"
9
+ readme = "README.md"
10
+ license = { text = "BSD-3-Clause" }
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ { name = "Hanzo AI", email = "dev@hanzo.ai" },
14
+ ]
15
+ keywords = [
16
+ "hanzo",
17
+ "mcp",
18
+ "tools",
19
+ "gimp",
20
+ "pdb",
21
+ "image",
22
+ "automation",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: BSD License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Topic :: Multimedia :: Graphics",
31
+ ]
32
+ dependencies = [
33
+ "hanzo-tools>=0.3.0",
34
+ "mcp>=1.0.0",
35
+ "pydantic>=2.0.0",
36
+ ]
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ "pytest>=8.0.0",
41
+ "pytest-asyncio>=0.23.0",
42
+ "ruff>=0.1.0",
43
+ ]
44
+
45
+ [project.entry-points."hanzo.tools"]
46
+ gimp = "hanzo_tools.gimp:TOOLS"
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/hanzoai/python-sdk"
50
+ Documentation = "https://docs.hanzo.ai"
51
+ Repository = "https://github.com/hanzoai/python-sdk"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["hanzo_tools"]
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ target-version = "py312"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP"]
62
+ ignore = ["E501"]
63
+
64
+ [tool.pytest.ini_options]
65
+ asyncio_mode = "auto"
66
+ testpaths = ["tests"]
@@ -0,0 +1,123 @@
1
+ """Tests for hanzo-tools-gimp."""
2
+
3
+ import pytest
4
+
5
+
6
+ class TestImports:
7
+ """Test that all modules can be imported."""
8
+
9
+ def test_import_package(self):
10
+ from hanzo_tools import gimp
11
+
12
+ assert gimp is not None
13
+
14
+ def test_import_tools(self):
15
+ from hanzo_tools.gimp import TOOLS
16
+
17
+ assert len(TOOLS) == 1
18
+
19
+ def test_import_gimp_tool(self):
20
+ from hanzo_tools.gimp import GimpTool
21
+
22
+ assert GimpTool.name == "gimp"
23
+
24
+ def test_register_tools_callable(self):
25
+ from hanzo_tools.gimp import register_tools
26
+
27
+ assert callable(register_tools)
28
+
29
+
30
+ class TestGimpTool:
31
+ """Tests for GimpTool."""
32
+
33
+ @pytest.fixture
34
+ def tool(self):
35
+ from hanzo_tools.gimp import GimpTool
36
+
37
+ return GimpTool()
38
+
39
+ def test_name(self, tool):
40
+ assert tool.name == "gimp"
41
+
42
+ def test_version(self, tool):
43
+ assert tool.VERSION
44
+
45
+ def test_has_description(self, tool):
46
+ assert tool.description
47
+ assert "gimp" in tool.description.lower()
48
+
49
+ def test_default_endpoint(self, tool):
50
+ assert tool.host == "127.0.0.1"
51
+ assert tool.port == 9876
52
+
53
+ def test_custom_endpoint(self):
54
+ from hanzo_tools.gimp import GimpTool
55
+
56
+ t = GimpTool(host="10.0.0.5", port=12345)
57
+ assert t.host == "10.0.0.5"
58
+ assert t.port == 12345
59
+
60
+ def test_action_surface(self, tool):
61
+ # The tool must register exactly the documented actions
62
+ # (plus the BaseTool built-ins: help, schema, status).
63
+ expected = {
64
+ "version",
65
+ "open",
66
+ "export",
67
+ "pdb_call",
68
+ "new_image",
69
+ "image_info",
70
+ "list_procedures",
71
+ "flatten",
72
+ }
73
+ registered = set(tool._handlers.keys())
74
+ assert expected <= registered, expected - registered
75
+
76
+ def test_builtin_actions_present(self, tool):
77
+ registered = set(tool._handlers.keys())
78
+ assert {"help", "schema", "status"} <= registered
79
+
80
+
81
+ class TestSchema:
82
+ """Schema generation from the registered actions."""
83
+
84
+ @pytest.fixture
85
+ def tool(self):
86
+ from hanzo_tools.gimp import GimpTool
87
+
88
+ return GimpTool()
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_schema_action_returns_schemas(self, tool):
92
+ env = await tool.call(None, "schema")
93
+ assert env["ok"] is True
94
+ schemas = env["data"]["schemas"]
95
+ assert "pdb_call" in schemas
96
+ # pdb_call requires a "procedure" string parameter.
97
+ pdb = schemas["pdb_call"]
98
+ assert pdb["properties"]["procedure"]["type"] == "string"
99
+ assert "procedure" in pdb["required"]
100
+
101
+
102
+ class TestBridgeErrors:
103
+ """Behavioral tests that do not require a running GIMP."""
104
+
105
+ @pytest.fixture
106
+ def tool(self):
107
+ from hanzo_tools.gimp import GimpTool
108
+
109
+ # Point at a closed port so connection fails fast.
110
+ return GimpTool(host="127.0.0.1", port=9)
111
+
112
+ @pytest.mark.asyncio
113
+ async def test_connection_failure_is_reported(self, tool):
114
+ # call() catches ToolError and returns an error envelope.
115
+ env = await tool.call(None, "version")
116
+ assert env["ok"] is False
117
+ assert env["error"] is not None
118
+
119
+ @pytest.mark.asyncio
120
+ async def test_missing_required_param(self, tool):
121
+ # pdb_call without a procedure -> InvalidParamsError -> error envelope.
122
+ env = await tool.call(None, "pdb_call")
123
+ assert env["ok"] is False