diff --git a/.env.test.sample b/.env.test.sample index d6eb1bd..232fbfd 100644 --- a/.env.test.sample +++ b/.env.test.sample @@ -2,3 +2,4 @@ MIDDLEMAN_BASE_URL=https://middleman-ai.com MIDDLEMAN_API_KEY=your_test_api_key MIDDLEMAN_TEST_PDF_TEMPLATE_ID=your_test_pdf_template_id MIDDLEMAN_TEST_PPTX_TEMPLATE_ID=your_test_pptx_template_id +MIDDLEMAN_TEST_XLSX_TEMPLATE_ID=your_test_xlsx_template_id diff --git a/CHANGELOG.md b/CHANGELOG.md index cac834f..71a46cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,26 @@ フォーマットは[Keep a Changelog](https://keepachangelog.com/ja/)に基づいています。 +## [v0.3.1] - 2026-01-15 + +- 「マークダウンから PDF 変換」ツールでローカル画像ファイルを使って PDF を生成できる機能を追加 + +### 追加 + +- 「XLSX → PDF 変換」ツールとの連携を追加(テンプレート解析・変換実行) + ## [v0.3.0] - 2025-12-22 ### 追加 -- 「XLSX → PDF変換」ツールとの連携を追加(テンプレート解析・変換実行) +- 「XLSX → PDF 変換」ツールとの連携を追加(テンプレート解析・変換実行) ## [v0.2.0] - 2025-07-05 ### 追加 - 「Mermaid ダイアグラムを画像変換」ツールとの連携を追加 -- 「マークダウン → DOCX変換」にテンプレート ID 指定機能を追加 +- 「マークダウン → DOCX 変換」にテンプレート ID 指定機能を追加 ### 削除 diff --git a/examples/client_usage/README.md b/examples/client_usage/README.md index 16c1abb..8cb4e90 100644 --- a/examples/client_usage/README.md +++ b/examples/client_usage/README.md @@ -15,6 +15,7 @@ Middleman.ai の ToolsClient メソッドを直接使用する最小限のサン export MIDDLEMAN_API_KEY="YOUR_API_KEY" export MIDDLEMAN_PDF_TEMPLATE_ID="YOUR_TEMPLATE_ID" # マークダウン to PDFのテンプレートIDを必要に応じて設定 export MIDDLEMAN_PPTX_TEMPLATE_ID="YOUR_TEMPLATE_ID" # JSON to PPTXの機能を使用する場合sample_template.pptxをMiddleman.aiにアップロードしてそのIDを設定 +export MIDDLEMAN_XLSX_TEMPLATE_ID="YOUR_TEMPLATE_ID" # XLSX to PDFの機能を使用する場合、プレースホルダー付きExcelテンプレートをアップロードしてそのIDを設定 ``` 2. 依存関係のインストール: diff --git a/examples/client_usage/main.py b/examples/client_usage/main.py index a6d900a..d0a33dc 100644 --- a/examples/client_usage/main.py +++ b/examples/client_usage/main.py @@ -1,11 +1,18 @@ import os +from dotenv import load_dotenv + from middleman_ai import ToolsClient +load_dotenv() + def main() -> None: # Initialize client - client = ToolsClient(api_key=os.getenv("MIDDLEMAN_API_KEY", "")) + client = ToolsClient( + api_key=os.getenv("MIDDLEMAN_API_KEY", ""), + base_url=os.getenv("MIDDLEMAN_BASE_URL", "https://middleman-ai.com"), + ) # Markdown → PDF markdown_text = "# Sample\nThis is a test." @@ -13,6 +20,11 @@ def main() -> None: pdf_url = client.md_to_pdf(markdown_text, pdf_template_id=pdf_template_id) print(f"Generated PDF URL (default template): {pdf_url}") + # Markdown → PDF (with local images) + markdown_with_image = "# Image Test\n\n![test](test_image.png)" + pdf_url = client.md_to_pdf(markdown_with_image, image_paths=["test_image.png"]) + print(f"Generated PDF URL (with image): {pdf_url}") + # Markdown → DOCX docx_url = client.md_to_docx(markdown_text) print(f"Generated DOCX URL: {docx_url}") @@ -73,6 +85,20 @@ def main() -> None: pptx_url = client.json_to_pptx_execute_v2(pptx_template_id, presentation) print(f"Generated PPTX URL: {pptx_url}") + # XLSX → PDF (analyze) + xlsx_template_id = os.getenv("MIDDLEMAN_XLSX_TEMPLATE_ID", "") + if xlsx_template_id: + result = client.xlsx_to_pdf_analyze(xlsx_template_id) + print(f"XLSX Template sheet: {result.sheet_name}") + print(f"Placeholders: {[p.key for p in result.placeholders]}") + + # XLSX → PDF (execute) + placeholders = {p.key: f"Sample {p.key}" for p in result.placeholders} + result = client.xlsx_to_pdf_execute(xlsx_template_id, placeholders) + print(f"Generated PDF URL (from XLSX): {result.pdf_url}") + else: + print("Skipping XLSX → PDF: MIDDLEMAN_XLSX_TEMPLATE_ID not set") + if __name__ == "__main__": main() diff --git a/examples/client_usage/test_image.png b/examples/client_usage/test_image.png new file mode 100644 index 0000000..7296313 Binary files /dev/null and b/examples/client_usage/test_image.png differ diff --git a/examples/client_usage/uv.lock b/examples/client_usage/uv.lock index 6026a6a..987e2cd 100644 --- a/examples/client_usage/uv.lock +++ b/examples/client_usage/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12.4'", @@ -244,7 +243,7 @@ wheels = [ [[package]] name = "middleman-ai" -version = "0.1.2" +version = "0.3.0" source = { editable = "../../" } dependencies = [ { name = "click" }, diff --git a/src/middleman_ai/cli/main.py b/src/middleman_ai/cli/main.py index e1ef088..66039d1 100644 --- a/src/middleman_ai/cli/main.py +++ b/src/middleman_ai/cli/main.py @@ -43,7 +43,14 @@ def cli() -> None: @cli.command() @click.argument("template_id", required=False) -def md_to_pdf(template_id: str | None = None) -> None: +@click.option( + "--images", + "-i", + multiple=True, + type=click.Path(exists=True), + help="画像ファイルパス(複数指定可)。Markdown内でファイル名で参照できます。", +) +def md_to_pdf(template_id: str | None = None, images: tuple[str, ...] = ()) -> None: """Convert Markdown to PDF.""" print("md_to_pdf コマンドを実行しています...") try: @@ -53,9 +60,14 @@ def md_to_pdf(template_id: str | None = None) -> None: print( f"読み込んだMarkdown ({len(markdown_text)} 文字): {markdown_text[:50]}..." ) + image_paths = list(images) if images else None + if image_paths: + print(f"添付画像: {image_paths}") with click.progressbar(length=1, label="PDFに変換中...", show_eta=False) as bar: print("APIを呼び出しています...") - pdf_url = client.md_to_pdf(markdown_text, pdf_template_id=template_id) + pdf_url = client.md_to_pdf( + markdown_text, pdf_template_id=template_id, image_paths=image_paths + ) bar.update(1) print(f"変換結果URL: {pdf_url}") if template_id: diff --git a/src/middleman_ai/client.py b/src/middleman_ai/client.py index 4e81686..6ea21c9 100644 --- a/src/middleman_ai/client.py +++ b/src/middleman_ai/client.py @@ -2,6 +2,7 @@ import json import logging +import os from typing import Any, Dict, List, cast import requests @@ -154,12 +155,18 @@ def _handle_response(self, response: requests.Response) -> Dict[str, Any]: except json.JSONDecodeError as e: raise ValidationError("Invalid JSON response") from e - def md_to_pdf(self, markdown_text: str, pdf_template_id: str | None = None) -> str: + def md_to_pdf( + self, + markdown_text: str, + pdf_template_id: str | None = None, + image_paths: List[str] | None = None, + ) -> str: """Markdown文字列をPDFに変換し、PDFのダウンロードURLを返します。 Args: markdown_text: 変換対象のMarkdown文字列 pdf_template_id: テンプレートID(UUID) + image_paths: ローカル画像ファイルパスのリスト(Markdown内で参照可能) Returns: str: 生成されたPDFのURL @@ -169,21 +176,51 @@ def md_to_pdf(self, markdown_text: str, pdf_template_id: str | None = None) -> s その他、_handle_responseで定義される例外 """ try: - response = self.session.post( - f"{self.base_url}/api/v1/tools/md-to-pdf", - json={ - "markdown": markdown_text, - "pdf_template_id": pdf_template_id, - }, + files: List[tuple[str, tuple[str, bytes, str]]] = [] + if image_paths: + for path in image_paths: + with open(path, "rb") as f: + filename = os.path.basename(path) + content = f.read() + mime_type = self._get_image_mime_type(filename) + files.append(("files", (filename, content, mime_type))) + + data: Dict[str, Any] = {"markdown": markdown_text} + if pdf_template_id: + data["pdf_template_id"] = pdf_template_id + + headers = dict(self.session.headers) + del headers["Content-Type"] + + response = requests.post( + f"{self.base_url}/api/v1/tools/md-to-pdf/form", + data=data, + files=files if files else None, + headers=headers, timeout=self.timeout, ) - data = self._handle_response(response) - result = MdToPdfResponse.model_validate(data) + result_data = self._handle_response(response) + result = MdToPdfResponse.model_validate(result_data) return result.pdf_url except PydanticValidationError as e: raise ValidationError(str(e)) from e except requests.exceptions.RequestException as e: raise ConnectionError() from e + except OSError as e: + raise ValidationError(f"Failed to read image file: {e}") from e + + def _get_image_mime_type(self, filename: str) -> str: + """ファイル名から画像のMIMEタイプを推測""" + ext = filename.lower().split(".")[-1] + mime_types = { + "png": "image/png", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "gif": "image/gif", + "webp": "image/webp", + "svg": "image/svg+xml", + } + return mime_types.get(ext, "application/octet-stream") def md_to_docx( self, diff --git a/src/middleman_ai/mcp/server.py b/src/middleman_ai/mcp/server.py index e3b64d8..35ddeb4 100644 --- a/src/middleman_ai/mcp/server.py +++ b/src/middleman_ai/mcp/server.py @@ -39,7 +39,11 @@ def md_to_pdf(markdown_text: str, pdf_template_id: str | None = None) -> str: @mcp.tool() -def md_file_to_pdf(md_file_full_path: str, pdf_template_id: str | None = None) -> str: +def md_file_to_pdf( + md_file_full_path: str, + pdf_template_id: str | None = None, + image_paths: List[str] | None = None, +) -> str: """ Convert a Markdown file to PDF and return the download URL. @@ -47,6 +51,8 @@ def md_file_to_pdf(md_file_full_path: str, pdf_template_id: str | None = None) - md_file_full_path: Path to the local Markdown file pdf_template_id: Optional ID of the PDF template to use. If not provided, the default template will be used + image_paths: Optional list of local image file paths. + These images can be referenced in the Markdown by their filename. Returns: The URL to download the generated PDF @@ -59,9 +65,21 @@ def md_file_to_pdf(md_file_full_path: str, pdf_template_id: str | None = None) - if not os.access(file_path, os.R_OK): raise ValueError(f"File not readable: {md_file_full_path}") + if image_paths: + for img_path in image_paths: + img_file = Path(img_path) + if not img_file.exists(): + raise ValueError(f"Image file not found: {img_path}") + if not img_file.is_file(): + raise ValueError(f"Image path is not a file: {img_path}") + if not os.access(img_file, os.R_OK): + raise ValueError(f"Image file not readable: {img_path}") + with file_path.open("r") as f: md_text = f.read() - return client.md_to_pdf(md_text, pdf_template_id=pdf_template_id) + return client.md_to_pdf( + md_text, pdf_template_id=pdf_template_id, image_paths=image_paths + ) @mcp.tool() diff --git a/tests/cassettes/test_md_to_pdf_vcr.yaml b/tests/cassettes/test_md_to_pdf_vcr.yaml index 0b879fb..f788775 100644 --- a/tests/cassettes/test_md_to_pdf_vcr.yaml +++ b/tests/cassettes/test_md_to_pdf_vcr.yaml @@ -1,47 +1,46 @@ interactions: - - request: - body: - '{"markdown": "# Test Heading\n\n This is a test markdown document.\n\n ## - Section 1\n - Item 1\n - Item 2\n ", "pdf_template_id": null}' - headers: - Accept: - - "*/*" - Accept-Encoding: - - gzip, deflate, zstd - Connection: - - keep-alive - Content-Length: - - "150" - Content-Type: - - application/json - User-Agent: - - python-requests/2.32.3 - authorization: - - DUMMY - method: POST - uri: https://middleman-ai.com/api/v1/tools/md-to-pdf - response: - body: - string: - '{"pdf_url":"https://middleman-ai.com/s/709dd3af/E9S5ohwi9r","important_remark_for_user":"The - URL expires in 1 hour.Please note that you will not be able to download the - file after it has expired."}' - headers: - content-length: - - "201" - content-type: - - application/json - date: - - FILTERED - server: - - FILTERED - vary: - - Accept-Encoding - x-middleware-rewrite: - - FILTERED - x-request-id: - - FILTERED - status: - code: 200 - message: OK +- request: + body: markdown=%23+Test+Heading%0A%0A++++This+is+a+test+markdown+document.%0A%0A++++%23%23+Section+1%0A++++-+Item+1%0A++++-+Item+2%0A++++ + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '131' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - python-requests/2.32.3 + authorization: + - DUMMY + method: POST + uri: https://middleman-ai.com/api/v1/tools/md-to-pdf/form + response: + body: + string: '{"pdf_url":"https://middleman-ai.com/s/cd6b4bab/1pWXW5I51n","important_remark_for_user":"The + URL expires in 1 hour.Please note that you will not be able to download the + file after it has expired."}' + headers: + content-length: + - '201' + content-type: + - application/json + date: + - FILTERED + server: + - FILTERED + set-cookie: + - FILTERED + vary: + - Accept-Encoding + x-middleware-rewrite: + - FILTERED + x-request-id: + - FILTERED + status: + code: 200 + message: OK version: 1 diff --git a/tests/cassettes/test_md_to_pdf_with_images_vcr.yaml b/tests/cassettes/test_md_to_pdf_with_images_vcr.yaml new file mode 100644 index 0000000..152879f --- /dev/null +++ b/tests/cassettes/test_md_to_pdf_with_images_vcr.yaml @@ -0,0 +1,58 @@ +interactions: +- request: + body: !!binary | + LS1lZGJhNDdkNDUzNjY2NjYxYjQzMzM2MGQ4NzU4NzkxNg0KQ29udGVudC1EaXNwb3NpdGlvbjog + Zm9ybS1kYXRhOyBuYW1lPSJtYXJrZG93biINCg0KIyBUZXN0IHdpdGggSW1hZ2UKClRoaXMgZG9j + dW1lbnQgaW5jbHVkZXMgYW4gaW1hZ2UuCgohW1Rlc3QgSW1hZ2VdKHRlc3RfaW1hZ2UucG5nKQoK + RW5kIG9mIGRvY3VtZW50LgoNCi0tZWRiYTQ3ZDQ1MzY2NjY2MWI0MzMzNjBkODc1ODc5MTYNCkNv + bnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iZmlsZXMiOyBmaWxlbmFtZT0idGVz + dF9pbWFnZS5wbmciDQpDb250ZW50LVR5cGU6IGltYWdlL3BuZw0KDQqJUE5HDQoaCgAAAA1JSERS + AAAAZAAAAGQIAgAAAP+AAgMAAADjSURBVHic7dCxAQAgDICw6v8/6wtlT2Ymzhu27rrErMKswKzA + rMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCs + wKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzA + rMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCs + wKzArMCswKzArMCswKzArMCs2fuKXgHHYZoHzwAAAABJRU5ErkJggg0KLS1lZGJhNDdkNDUzNjY2 + NjYxYjQzMzM2MGQ4NzU4NzkxNi0tDQo= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '650' + Content-Type: + - multipart/form-data; boundary=edba47d453666661b433360d87587916 + User-Agent: + - python-requests/2.32.3 + authorization: + - DUMMY + method: POST + uri: https://middleman-ai.com/api/v1/tools/md-to-pdf/form + response: + body: + string: '{"pdf_url":"https://middleman-ai.com/s/cd6b4bab/oNrUjiJaqE","important_remark_for_user":"The + URL expires in 1 hour.Please note that you will not be able to download the + file after it has expired."}' + headers: + content-length: + - '201' + content-type: + - application/json + date: + - FILTERED + server: + - FILTERED + set-cookie: + - FILTERED + vary: + - Accept-Encoding + x-middleware-rewrite: + - FILTERED + x-request-id: + - FILTERED + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_md_to_pdf_with_japanese_filename_image_vcr.yaml b/tests/cassettes/test_md_to_pdf_with_japanese_filename_image_vcr.yaml new file mode 100644 index 0000000..8a1e9d1 --- /dev/null +++ b/tests/cassettes/test_md_to_pdf_with_japanese_filename_image_vcr.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: !!binary | + LS05ZjI4YjA1YTYzZmU1NjgyOTY0YjcyNDUwM2Q0N2I4Zg0KQ29udGVudC1EaXNwb3NpdGlvbjog + Zm9ybS1kYXRhOyBuYW1lPSJtYXJrZG93biINCg0KIyDml6XmnKzoqp7jg5XjgqHjgqTjg6vlkI3j + g4bjgrnjg4gKCuOBk+OBruODieOCreODpeODoeODs+ODiOOBq+OBr+aXpeacrOiqnuODleOCoeOC + pOODq+WQjeOBrueUu+WDj+OBjOWQq+OBvuOCjOOBpuOBhOOBvuOBmeOAggoKIVvjg4bjgrnjg4jn + lLvlg49dKOODhuOCueODiOeUu+WDjy5wbmcpCgrjg4njgq3jg6Xjg6Hjg7Pjg4jntYLkuobjgIIK + DQotLTlmMjhiMDVhNjNmZTU2ODI5NjRiNzI0NTAzZDQ3YjhmDQpDb250ZW50LURpc3Bvc2l0aW9u + OiBmb3JtLWRhdGE7IG5hbWU9ImZpbGVzIjsgZmlsZW5hbWU9IuODhuOCueODiOeUu+WDjy5wbmci + DQpDb250ZW50LVR5cGU6IGltYWdlL3BuZw0KDQqJUE5HDQoaCgAAAA1JSERSAAAAZAAAAGQIAgAA + AP+AAgMAAADkSURBVHic7dCxAQAgDICw6v8/6wtlT2Ymzswbdu6yw6zGrMCswKzArMCswKzArMCs + wKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzA + rMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCs + wKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzA + rMCswKzArNn7iGABx9CglmwAAAAASUVORK5CYIINCi0tOWYyOGIwNWE2M2ZlNTY4Mjk2NGI3MjQ1 + MDNkNDdiOGYtLQ0K + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '753' + Content-Type: + - multipart/form-data; boundary=9f28b05a63fe5682964b724503d47b8f + User-Agent: + - python-requests/2.32.3 + authorization: + - DUMMY + method: POST + uri: https://middleman-ai.com/api/v1/tools/md-to-pdf/form + response: + body: + string: '{"pdf_url":"https://middleman-ai.com/s/cd6b4bab/u5EXgShUuq","important_remark_for_user":"The + URL expires in 1 hour.Please note that you will not be able to download the + file after it has expired."}' + headers: + content-length: + - '201' + content-type: + - application/json + date: + - FILTERED + server: + - FILTERED + set-cookie: + - FILTERED + vary: + - Accept-Encoding + x-middleware-rewrite: + - FILTERED + x-request-id: + - FILTERED + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_md_to_pdf_with_template_id_vcr.yaml b/tests/cassettes/test_md_to_pdf_with_template_id_vcr.yaml index c0eb76d..00255f2 100644 --- a/tests/cassettes/test_md_to_pdf_with_template_id_vcr.yaml +++ b/tests/cassettes/test_md_to_pdf_with_template_id_vcr.yaml @@ -1,47 +1,46 @@ interactions: - - request: - body: - '{"markdown": "# Test Heading\n\n This is a test markdown document.\n\n ## - Section 1\n - Item 1\n - Item 2\n ", "pdf_template_id": "TEMPLATE_ID"}' - headers: - Accept: - - "*/*" - Accept-Encoding: - - gzip, deflate, zstd - Connection: - - keep-alive - Content-Length: - - "184" - Content-Type: - - application/json - User-Agent: - - python-requests/2.32.3 - authorization: - - DUMMY - method: POST - uri: https://middleman-ai.com/api/v1/tools/md-to-pdf - response: - body: - string: - '{"pdf_url":"https://middleman-ai.com/s/709dd3af/M51KIysH9c","important_remark_for_user":"The - URL expires in 1 hour.Please note that you will not be able to download the - file after it has expired."}' - headers: - content-length: - - "201" - content-type: - - application/json - date: - - FILTERED - server: - - FILTERED - vary: - - Accept-Encoding - x-middleware-rewrite: - - FILTERED - x-request-id: - - FILTERED - status: - code: 200 - message: OK +- request: + body: markdown=%23+Test+Heading%0A%0A++++This+is+a+test+markdown+document.%0A%0A++++%23%23+Section+1%0A++++-+Item+1%0A++++-+Item+2%0A++++&pdf_template_id=cc6a5fa4-cb44-4d3c-a486-215873cf5fba + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '184' + Content-Type: + - application/x-www-form-urlencoded + User-Agent: + - python-requests/2.32.3 + authorization: + - DUMMY + method: POST + uri: https://middleman-ai.com/api/v1/tools/md-to-pdf/form + response: + body: + string: '{"pdf_url":"https://middleman-ai.com/s/cd6b4bab/fbShdttkI8","important_remark_for_user":"The + URL expires in 1 hour.Please note that you will not be able to download the + file after it has expired."}' + headers: + content-length: + - '201' + content-type: + - application/json + date: + - FILTERED + server: + - FILTERED + set-cookie: + - FILTERED + vary: + - Accept-Encoding + x-middleware-rewrite: + - FILTERED + x-request-id: + - FILTERED + status: + code: 200 + message: OK version: 1 diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 8fb2ef9..5811406 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -15,7 +15,9 @@ def test_md_to_pdf_cli(runner, mock_client): result = runner.invoke(cli, ["md-to-pdf"], input="# Test") assert result.exit_code == 0 assert "https://example.com/test.pdf" in result.output - mock_client.md_to_pdf.assert_called_once_with("# Test", pdf_template_id=None) + mock_client.md_to_pdf.assert_called_once_with( + "# Test", pdf_template_id=None, image_paths=None + ) def test_md_to_pdf_cli_with_template_id(runner, mock_client): @@ -24,7 +26,7 @@ def test_md_to_pdf_cli_with_template_id(runner, mock_client): assert result.exit_code == 0 assert "https://example.com/test.pdf" in result.output mock_client.md_to_pdf.assert_called_once_with( - "# Test", pdf_template_id="TEMPLATE_ID" + "# Test", pdf_template_id="TEMPLATE_ID", image_paths=None ) diff --git a/tests/data/test_image.png b/tests/data/test_image.png new file mode 100644 index 0000000..7296313 Binary files /dev/null and b/tests/data/test_image.png differ diff --git "a/tests/data/\343\203\206\343\202\271\343\203\210\347\224\273\345\203\217.png" "b/tests/data/\343\203\206\343\202\271\343\203\210\347\224\273\345\203\217.png" new file mode 100644 index 0000000..78431d2 Binary files /dev/null and "b/tests/data/\343\203\206\343\202\271\343\203\210\347\224\273\345\203\217.png" differ diff --git a/tests/test_client.py b/tests/test_client.py index 5e61ae4..c1ea154 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -56,23 +56,24 @@ def test_md_to_pdf_success( client: ToolsClient, mocker: "MockerFixture", mock_response: Mock ) -> None: """md_to_pdf成功時のテスト。""" - mock_post = mocker.patch.object(client.session, "post", return_value=mock_response) + mock_post = mocker.patch.object(requests, "post", return_value=mock_response) result = client.md_to_pdf("# Test") assert result == "https://example.com/test.pdf" - mock_post.assert_called_once_with( - "https://middleman-ai.com/api/v1/tools/md-to-pdf", - json={"markdown": "# Test", "pdf_template_id": None}, - timeout=30.0, - ) + mock_post.assert_called_once() + call_args = mock_post.call_args + assert call_args[0][0] == "https://middleman-ai.com/api/v1/tools/md-to-pdf/form" + assert call_args[1]["data"]["markdown"] == "# Test" + assert call_args[1]["files"] is None + assert call_args[1]["timeout"] == 30.0 def test_md_to_pdf_success_with_template_id( client: ToolsClient, mocker: "MockerFixture", mock_response: Mock ) -> None: """md_to_pdf成功時のテスト。""" - mock_post = mocker.patch.object(client.session, "post", return_value=mock_response) + mock_post = mocker.patch.object(requests, "post", return_value=mock_response) result = client.md_to_pdf( "# Test", @@ -80,14 +81,14 @@ def test_md_to_pdf_success_with_template_id( ) assert result == "https://example.com/test.pdf" - mock_post.assert_called_once_with( - "https://middleman-ai.com/api/v1/tools/md-to-pdf", - json={ - "markdown": "# Test", - "pdf_template_id": "00000000-0000-0000-0000-000000000001", - }, - timeout=30.0, - ) + mock_post.assert_called_once() + call_args = mock_post.call_args + assert call_args[0][0] == "https://middleman-ai.com/api/v1/tools/md-to-pdf/form" + assert call_args[1]["data"]["markdown"] == "# Test" + expected_id = "00000000-0000-0000-0000-000000000001" + assert call_args[1]["data"]["pdf_template_id"] == expected_id + assert call_args[1]["files"] is None + assert call_args[1]["timeout"] == 30.0 @pytest.mark.parametrize( @@ -109,11 +110,11 @@ def test_md_to_pdf_http_errors( ) -> None: """md_to_pdf HTTP エラー時のテスト。""" mock_response.status_code = status_code - mock_response.url = "https://example.com/api/test" # URLを追加 - mock_response.headers = {"content-type": "application/json"} # headersを追加 - mock_response.text = "" # textを追加 + mock_response.url = "https://example.com/api/test" + mock_response.headers = {"content-type": "application/json"} + mock_response.text = "" mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError() - mocker.patch.object(client.session, "post", return_value=mock_response) + mocker.patch.object(requests, "post", return_value=mock_response) with pytest.raises(expected_exception): client.md_to_pdf("# Test") @@ -124,16 +125,13 @@ def test_md_to_pdf_connection_error( ) -> None: """md_to_pdf 接続エラー時のテスト。""" mocker.patch.object( - client.session, + requests, "post", side_effect=requests.exceptions.RequestException(), ) with pytest.raises(ConnectionError): - try: - client.md_to_pdf("# Test") - except requests.exceptions.RequestException as e: - raise ConnectionError() from e + client.md_to_pdf("# Test") def test_md_to_pdf_validation_error( @@ -141,7 +139,7 @@ def test_md_to_pdf_validation_error( ) -> None: """md_to_pdf バリデーションエラー時のテスト。""" mock_response.json.return_value = {"invalid": "response"} - mocker.patch.object(client.session, "post", return_value=mock_response) + mocker.patch.object(requests, "post", return_value=mock_response) with pytest.raises(ValidationError): client.md_to_pdf("# Test") @@ -150,7 +148,7 @@ def test_md_to_pdf_validation_error( def test_md_to_pdf_timeout_error(client: ToolsClient, mocker: "MockerFixture") -> None: """md_to_pdf タイムアウトエラー時のテスト。""" mocker.patch.object( - client.session, + requests, "post", side_effect=requests.exceptions.Timeout("Connection timed out"), ) diff --git a/tests/test_client_vcr.py b/tests/test_client_vcr.py index 0d02a3e..096f3b5 100644 --- a/tests/test_client_vcr.py +++ b/tests/test_client_vcr.py @@ -24,7 +24,7 @@ def client() -> ToolsClient: ) -@pytest.mark.vcr() +@pytest.mark.vcr(match_on=["method", "scheme", "port", "path", "query"]) def test_md_to_pdf_vcr(client: ToolsClient) -> None: """ToolsClient.md_to_pdfの実際のAPIを使用したテスト。 @@ -45,7 +45,7 @@ def test_md_to_pdf_vcr(client: ToolsClient) -> None: assert "/s/" in pdf_url -@pytest.mark.vcr() +@pytest.mark.vcr(match_on=["method", "scheme", "port", "path", "query"]) def test_md_to_pdf_with_template_id_vcr(client: ToolsClient) -> None: """ToolsClient.md_to_pdfの実際のAPIを使用したテスト。 @@ -335,3 +335,47 @@ def test_xlsx_to_pdf_execute_vcr(client: ToolsClient) -> None: assert result.pdf_url is not None assert result.pdf_url.startswith("https://") assert "/s/" in result.pdf_url + + +@pytest.mark.vcr(match_on=["method", "scheme", "port", "path", "query"]) +def test_md_to_pdf_with_images_vcr(client: ToolsClient) -> None: + """ToolsClient.md_to_pdfの画像付きテスト。 + + Note: + このテストは実際のAPIを呼び出し、レスポンスをキャッシュします。 + 初回実行時のみAPIを呼び出し、以降はキャッシュを使用します。 + """ + test_markdown = """# Test with Image + +This document includes an image. + +![Test Image](test_image.png) + +End of document. +""" + image_path = "tests/data/test_image.png" + pdf_url = client.md_to_pdf(markdown_text=test_markdown, image_paths=[image_path]) + assert pdf_url.startswith("https://") + assert "/s/" in pdf_url + + +@pytest.mark.vcr(match_on=["method", "scheme", "port", "path", "query"]) +def test_md_to_pdf_with_japanese_filename_image_vcr(client: ToolsClient) -> None: + """ToolsClient.md_to_pdfの日本語ファイル名画像付きテスト。 + + Note: + このテストは実際のAPIを呼び出し、レスポンスをキャッシュします。 + 初回実行時のみAPIを呼び出し、以降はキャッシュを使用します。 + """ + test_markdown = """# 日本語ファイル名テスト + +このドキュメントには日本語ファイル名の画像が含まれています。 + +![テスト画像](テスト画像.png) + +ドキュメント終了。 +""" + image_path = "tests/data/テスト画像.png" + pdf_url = client.md_to_pdf(markdown_text=test_markdown, image_paths=[image_path]) + assert pdf_url.startswith("https://") + assert "/s/" in pdf_url diff --git a/tests/test_mcp.py b/tests/test_mcp.py index daebc27..d3717fa 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -86,6 +86,7 @@ def test_md_file_to_pdf_tool_mcp( mock_client.md_to_pdf.assert_called_once_with( "# Test MD", pdf_template_id=template_id, + image_paths=None, ) mock_os_access.assert_called_once_with(mock_path.return_value, mocker.ANY)