apdf-cloud-cli 0.1.0__py3-none-any.whl
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.
- apdf_cloud_cli/__init__.py +5 -0
- apdf_cloud_cli/cli.py +162 -0
- apdf_cloud_cli/client.py +20 -0
- apdf_cloud_cli/config.py +75 -0
- apdf_cloud_cli/mcp_server.py +82 -0
- apdf_cloud_cli/operations.py +230 -0
- apdf_cloud_cli/py.typed +1 -0
- apdf_cloud_cli-0.1.0.dist-info/METADATA +109 -0
- apdf_cloud_cli-0.1.0.dist-info/RECORD +11 -0
- apdf_cloud_cli-0.1.0.dist-info/WHEEL +4 -0
- apdf_cloud_cli-0.1.0.dist-info/entry_points.txt +4 -0
apdf_cloud_cli/cli.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Command-line interface for APDF tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from . import operations
|
|
14
|
+
from .config import ConfigError
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="APDF CLI tools.")
|
|
17
|
+
storage_app = typer.Typer(help="Cloud storage operations.")
|
|
18
|
+
pdf_app = typer.Typer(help="PDF operations.")
|
|
19
|
+
mcp_app = typer.Typer(help="MCP server commands.")
|
|
20
|
+
app.add_typer(storage_app, name="storage")
|
|
21
|
+
app.add_typer(pdf_app, name="pdf")
|
|
22
|
+
app.add_typer(mcp_app, name="mcp")
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _json(data: object) -> None:
|
|
28
|
+
console.print_json(json.dumps(data, ensure_ascii=False))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _handle_error(exc: Exception) -> None:
|
|
32
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
33
|
+
raise typer.Exit(code=1) from exc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _run(action):
|
|
37
|
+
try:
|
|
38
|
+
return action()
|
|
39
|
+
except (operations.ApdfToolError, ConfigError) as exc:
|
|
40
|
+
_handle_error(exc)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@storage_app.command("list")
|
|
44
|
+
def list_storage_files(
|
|
45
|
+
path: Annotated[str, typer.Argument(help="Folder path in Aspose storage.")],
|
|
46
|
+
storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
|
|
47
|
+
as_json: Annotated[bool, typer.Option("--json", help="Emit JSON output.")] = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""List files and folders in Aspose storage."""
|
|
50
|
+
|
|
51
|
+
result = _run(lambda: operations.list_files(path, storage))
|
|
52
|
+
if as_json:
|
|
53
|
+
_json(result)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
table = Table(title=f"Storage: {storage or 'default'} Path: {path}")
|
|
57
|
+
table.add_column("Name")
|
|
58
|
+
table.add_column("Path")
|
|
59
|
+
table.add_column("Size", justify="right")
|
|
60
|
+
table.add_column("Folder")
|
|
61
|
+
|
|
62
|
+
items = result.get("items", {})
|
|
63
|
+
values = items.get("value") or items.get("Value") or items.get("files") or items
|
|
64
|
+
if isinstance(values, dict):
|
|
65
|
+
values = values.get("value") or values.get("Value") or []
|
|
66
|
+
if not isinstance(values, list):
|
|
67
|
+
values = []
|
|
68
|
+
|
|
69
|
+
for item in values:
|
|
70
|
+
if not isinstance(item, dict):
|
|
71
|
+
continue
|
|
72
|
+
table.add_row(
|
|
73
|
+
str(item.get("name") or item.get("Name") or ""),
|
|
74
|
+
str(item.get("path") or item.get("Path") or ""),
|
|
75
|
+
str(item.get("size") or item.get("Size") or ""),
|
|
76
|
+
str(item.get("is_folder") or item.get("IsFolder") or False),
|
|
77
|
+
)
|
|
78
|
+
console.print(table)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@storage_app.command("upload")
|
|
82
|
+
def upload_storage_file(
|
|
83
|
+
local_path: Annotated[Path, typer.Argument(help="Local file to upload.")],
|
|
84
|
+
remote_path: Annotated[str, typer.Argument(help="Remote storage path.")],
|
|
85
|
+
storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Upload a local file to Aspose storage."""
|
|
88
|
+
|
|
89
|
+
result = _run(lambda: operations.upload_file(local_path, remote_path, storage))
|
|
90
|
+
console.print(f"Uploaded [bold]{result['local_path']}[/bold] to [bold]{remote_path}[/bold].")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@storage_app.command("download")
|
|
94
|
+
def download_storage_file(
|
|
95
|
+
remote_path: Annotated[str, typer.Argument(help="Remote storage path.")],
|
|
96
|
+
local_path: Annotated[Path, typer.Argument(help="Local output path.")],
|
|
97
|
+
storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
|
|
98
|
+
version_id: Annotated[str | None, typer.Option("--version-id", help="File version ID.")] = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Download a file from Aspose storage."""
|
|
101
|
+
|
|
102
|
+
result = _run(
|
|
103
|
+
lambda: operations.download_file(remote_path, local_path, storage, version_id)
|
|
104
|
+
)
|
|
105
|
+
console.print(f"Downloaded [bold]{remote_path}[/bold] to [bold]{result['local_path']}[/bold].")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pdf_app.command("merge")
|
|
109
|
+
def merge_pdf_files(
|
|
110
|
+
input_1: Annotated[str, typer.Argument(help="First input PDF path in storage.")],
|
|
111
|
+
input_2: Annotated[str, typer.Argument(help="Second input PDF path in storage.")],
|
|
112
|
+
output_name: Annotated[str, typer.Argument(help="Output PDF name.")],
|
|
113
|
+
folder: Annotated[str | None, typer.Option("--folder", help="Output folder.")] = None,
|
|
114
|
+
storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Merge two PDF files already present in Aspose storage."""
|
|
117
|
+
|
|
118
|
+
result = _run(lambda: operations.merge_pdfs(input_1, input_2, output_name, folder, storage))
|
|
119
|
+
console.print(
|
|
120
|
+
f"Merged [bold]{input_1}[/bold] and [bold]{input_2}[/bold] into "
|
|
121
|
+
f"[bold]{result['output_name']}[/bold]."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pdf_app.command("extract-text")
|
|
126
|
+
def extract_pdf_text(
|
|
127
|
+
name: Annotated[str, typer.Argument(help="PDF name/path in storage.")],
|
|
128
|
+
folder: Annotated[str | None, typer.Option("--folder", help="Document folder.")] = None,
|
|
129
|
+
storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
|
|
130
|
+
as_json: Annotated[bool, typer.Option("--json", help="Emit JSON output.")] = False,
|
|
131
|
+
output: Annotated[Path | None, typer.Option("--output", help="Write plain text to a file.")] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Extract text from a PDF in Aspose storage."""
|
|
134
|
+
|
|
135
|
+
result = _run(lambda: operations.extract_text(name, folder, storage))
|
|
136
|
+
if output:
|
|
137
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
output.write_text(result["text"], encoding="utf-8")
|
|
139
|
+
|
|
140
|
+
if as_json:
|
|
141
|
+
_json(result)
|
|
142
|
+
elif output:
|
|
143
|
+
console.print(f"Wrote extracted text to [bold]{output}[/bold].")
|
|
144
|
+
else:
|
|
145
|
+
console.print(result["text"])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp_app.command("serve")
|
|
149
|
+
def serve_mcp() -> None:
|
|
150
|
+
"""Run the MCP server over stdio."""
|
|
151
|
+
|
|
152
|
+
from .mcp_server import main
|
|
153
|
+
|
|
154
|
+
main()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> None:
|
|
158
|
+
app()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
apdf_cloud_cli/client.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""APDF SDK client factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asposepdfcloud
|
|
6
|
+
|
|
7
|
+
from .config import AsposeConfig, load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_pdf_api(config: AsposeConfig | None = None) -> asposepdfcloud.PdfApi:
|
|
11
|
+
"""Create an authenticated Aspose PdfApi instance."""
|
|
12
|
+
|
|
13
|
+
cfg = config or load_config()
|
|
14
|
+
api_client = asposepdfcloud.ApiClient(
|
|
15
|
+
cfg.client_secret,
|
|
16
|
+
cfg.client_id,
|
|
17
|
+
cfg.base_url,
|
|
18
|
+
cfg.self_host,
|
|
19
|
+
)
|
|
20
|
+
return asposepdfcloud.PdfApi(api_client)
|
apdf_cloud_cli/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Configuration helpers for APDF access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigError(RuntimeError):
|
|
11
|
+
"""Raised when required configuration is missing or invalid."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class AsposeConfig:
|
|
16
|
+
client_id: str
|
|
17
|
+
client_secret: str
|
|
18
|
+
base_url: str | None = None
|
|
19
|
+
self_host: bool = False
|
|
20
|
+
storage_name: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _truthy(value: str | None) -> bool:
|
|
24
|
+
return value is not None and value.strip().lower() in {"1", "true", "yes", "on"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_env_file(path: str | Path | None) -> dict[str, str]:
|
|
28
|
+
if path is None:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
env_path = Path(path)
|
|
32
|
+
if not env_path.is_file():
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
values: dict[str, str] = {}
|
|
36
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
37
|
+
stripped = line.strip()
|
|
38
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
39
|
+
continue
|
|
40
|
+
key, value = stripped.split("=", 1)
|
|
41
|
+
key = key.strip()
|
|
42
|
+
value = value.strip().strip('"').strip("'")
|
|
43
|
+
if key:
|
|
44
|
+
values[key] = value
|
|
45
|
+
return values
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_setting(name: str, env_file_values: dict[str, str]) -> str:
|
|
49
|
+
return os.getenv(name, env_file_values.get(name, "")).strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_config(env_file: str | Path | None = ".env") -> AsposeConfig:
|
|
53
|
+
"""Load Aspose Cloud configuration from environment variables and optional .env."""
|
|
54
|
+
|
|
55
|
+
env_file_values = _read_env_file(env_file)
|
|
56
|
+
|
|
57
|
+
client_id = _get_setting("ASPOSE_CLIENT_ID", env_file_values)
|
|
58
|
+
client_secret = _get_setting("ASPOSE_CLIENT_SECRET", env_file_values)
|
|
59
|
+
|
|
60
|
+
if not client_id or not client_secret:
|
|
61
|
+
raise ConfigError(
|
|
62
|
+
"Missing Aspose credentials. Set ASPOSE_CLIENT_ID and "
|
|
63
|
+
"ASPOSE_CLIENT_SECRET."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
base_url = _get_setting("ASPOSE_BASE_URL", env_file_values) or None
|
|
67
|
+
storage_name = _get_setting("ASPOSE_STORAGE_NAME", env_file_values) or None
|
|
68
|
+
|
|
69
|
+
return AsposeConfig(
|
|
70
|
+
client_id=client_id,
|
|
71
|
+
client_secret=client_secret,
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
self_host=_truthy(_get_setting("ASPOSE_SELF_HOST", env_file_values)),
|
|
74
|
+
storage_name=storage_name,
|
|
75
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""MCP server exposing APDF Stage 1 tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from . import operations
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP("aspose-pdf-cloud")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _safe_call(func, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
15
|
+
try:
|
|
16
|
+
return {"ok": True, "data": func(*args, **kwargs)}
|
|
17
|
+
except Exception as exc: # noqa: BLE001 - MCP should receive sanitized failures.
|
|
18
|
+
return {"ok": False, "error": str(exc)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@mcp.tool()
|
|
22
|
+
def list_files(path: str, storage_name: str | None = None) -> dict[str, Any]:
|
|
23
|
+
"""List files and folders in Aspose storage."""
|
|
24
|
+
|
|
25
|
+
return _safe_call(operations.list_files, path, storage_name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@mcp.tool()
|
|
29
|
+
def upload_file(
|
|
30
|
+
local_path: str,
|
|
31
|
+
remote_path: str,
|
|
32
|
+
storage_name: str | None = None,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Upload a local file to Aspose storage."""
|
|
35
|
+
|
|
36
|
+
return _safe_call(operations.upload_file, local_path, remote_path, storage_name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
def download_file(
|
|
41
|
+
remote_path: str,
|
|
42
|
+
local_path: str,
|
|
43
|
+
storage_name: str | None = None,
|
|
44
|
+
version_id: str | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Download a file from Aspose storage."""
|
|
47
|
+
|
|
48
|
+
return _safe_call(
|
|
49
|
+
operations.download_file,
|
|
50
|
+
remote_path,
|
|
51
|
+
local_path,
|
|
52
|
+
storage_name,
|
|
53
|
+
version_id,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@mcp.tool()
|
|
58
|
+
def merge_pdfs(
|
|
59
|
+
input_1: str,
|
|
60
|
+
input_2: str,
|
|
61
|
+
output_name: str,
|
|
62
|
+
folder: str | None = None,
|
|
63
|
+
storage: str | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Merge two PDF files already present in Aspose storage."""
|
|
66
|
+
|
|
67
|
+
return _safe_call(operations.merge_pdfs, input_1, input_2, output_name, folder, storage)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@mcp.tool()
|
|
71
|
+
def extract_text(
|
|
72
|
+
name: str,
|
|
73
|
+
folder: str | None = None,
|
|
74
|
+
storage: str | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""Extract text from a PDF in Aspose storage."""
|
|
77
|
+
|
|
78
|
+
return _safe_call(operations.extract_text, name, folder, storage)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> None:
|
|
82
|
+
mcp.run()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Shared Stage 1 APDF operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import asposepdfcloud
|
|
10
|
+
|
|
11
|
+
from .client import create_pdf_api
|
|
12
|
+
from .config import AsposeConfig, load_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApdfToolError(RuntimeError):
|
|
16
|
+
"""Raised for sanitized user-facing operation failures."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _api_error(exc: Exception) -> ApdfToolError:
|
|
20
|
+
reason = getattr(exc, "reason", None) or str(exc)
|
|
21
|
+
status = getattr(exc, "status", None)
|
|
22
|
+
message = "Aspose API error"
|
|
23
|
+
if status:
|
|
24
|
+
message += f" ({status})"
|
|
25
|
+
if reason:
|
|
26
|
+
message += f": {reason}"
|
|
27
|
+
return ApdfToolError(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _call_api(func, *args: Any, **kwargs: Any) -> Any:
|
|
31
|
+
try:
|
|
32
|
+
return func(*args, **{k: v for k, v in kwargs.items() if v is not None})
|
|
33
|
+
except Exception as exc: # noqa: BLE001 - SDK raises several exception shapes.
|
|
34
|
+
api_exception = getattr(getattr(asposepdfcloud, "rest", None), "ApiException", None)
|
|
35
|
+
if api_exception and isinstance(exc, api_exception):
|
|
36
|
+
raise _api_error(exc) from exc
|
|
37
|
+
raise ApdfToolError(str(exc)) from exc
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _api_and_config(api: Any | None, config: AsposeConfig | None) -> tuple[Any, AsposeConfig | None]:
|
|
41
|
+
if api is not None:
|
|
42
|
+
return api, config
|
|
43
|
+
cfg = config or load_config()
|
|
44
|
+
return create_pdf_api(cfg), cfg
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _storage(explicit: str | None, config: AsposeConfig | None) -> str | None:
|
|
48
|
+
return explicit if explicit is not None else (config.storage_name if config else None)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def to_plain_data(value: Any) -> Any:
|
|
52
|
+
"""Convert SDK model objects into JSON-friendly values."""
|
|
53
|
+
|
|
54
|
+
if value is None or isinstance(value, str | int | float | bool):
|
|
55
|
+
return value
|
|
56
|
+
if isinstance(value, Path):
|
|
57
|
+
return str(value)
|
|
58
|
+
if isinstance(value, list | tuple | set):
|
|
59
|
+
return [to_plain_data(item) for item in value]
|
|
60
|
+
if isinstance(value, dict):
|
|
61
|
+
return {str(key): to_plain_data(item) for key, item in value.items()}
|
|
62
|
+
if hasattr(value, "to_dict"):
|
|
63
|
+
return to_plain_data(value.to_dict())
|
|
64
|
+
if hasattr(value, "__dict__"):
|
|
65
|
+
return {
|
|
66
|
+
key.lstrip("_"): to_plain_data(item)
|
|
67
|
+
for key, item in vars(value).items()
|
|
68
|
+
if not key.startswith("__")
|
|
69
|
+
}
|
|
70
|
+
return str(value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def list_files(
|
|
74
|
+
path: str,
|
|
75
|
+
storage_name: str | None = None,
|
|
76
|
+
*,
|
|
77
|
+
api: Any | None = None,
|
|
78
|
+
config: AsposeConfig | None = None,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
pdf_api, cfg = _api_and_config(api, config)
|
|
81
|
+
effective_storage = _storage(storage_name, cfg)
|
|
82
|
+
response = _call_api(pdf_api.get_files_list, path, storage_name=effective_storage)
|
|
83
|
+
return {"path": path, "storage_name": effective_storage, "items": to_plain_data(response)}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def upload_file(
|
|
87
|
+
local_path: str | Path,
|
|
88
|
+
remote_path: str,
|
|
89
|
+
storage_name: str | None = None,
|
|
90
|
+
*,
|
|
91
|
+
api: Any | None = None,
|
|
92
|
+
config: AsposeConfig | None = None,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
source = Path(local_path)
|
|
95
|
+
if not source.is_file():
|
|
96
|
+
raise ApdfToolError(f"Local file not found: {source}")
|
|
97
|
+
|
|
98
|
+
pdf_api, cfg = _api_and_config(api, config)
|
|
99
|
+
effective_storage = _storage(storage_name, cfg)
|
|
100
|
+
response = _call_api(
|
|
101
|
+
pdf_api.upload_file,
|
|
102
|
+
remote_path,
|
|
103
|
+
str(source),
|
|
104
|
+
storage_name=effective_storage,
|
|
105
|
+
)
|
|
106
|
+
return {
|
|
107
|
+
"local_path": str(source),
|
|
108
|
+
"remote_path": remote_path,
|
|
109
|
+
"storage_name": effective_storage,
|
|
110
|
+
"result": to_plain_data(response),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def download_file(
|
|
115
|
+
remote_path: str,
|
|
116
|
+
local_path: str | Path,
|
|
117
|
+
storage_name: str | None = None,
|
|
118
|
+
version_id: str | None = None,
|
|
119
|
+
*,
|
|
120
|
+
api: Any | None = None,
|
|
121
|
+
config: AsposeConfig | None = None,
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
destination = Path(local_path)
|
|
124
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
pdf_api, cfg = _api_and_config(api, config)
|
|
127
|
+
effective_storage = _storage(storage_name, cfg)
|
|
128
|
+
downloaded = _call_api(
|
|
129
|
+
pdf_api.download_file,
|
|
130
|
+
remote_path,
|
|
131
|
+
storage_name=effective_storage,
|
|
132
|
+
version_id=version_id,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if isinstance(downloaded, bytes):
|
|
136
|
+
destination.write_bytes(downloaded)
|
|
137
|
+
elif hasattr(downloaded, "read"):
|
|
138
|
+
with destination.open("wb") as output:
|
|
139
|
+
shutil.copyfileobj(downloaded, output)
|
|
140
|
+
else:
|
|
141
|
+
downloaded_path = Path(str(downloaded))
|
|
142
|
+
if not downloaded_path.is_file():
|
|
143
|
+
raise ApdfToolError(f"Download did not return a readable file: {downloaded}")
|
|
144
|
+
shutil.copyfile(downloaded_path, destination)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"remote_path": remote_path,
|
|
148
|
+
"local_path": str(destination),
|
|
149
|
+
"storage_name": effective_storage,
|
|
150
|
+
"version_id": version_id,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def merge_pdfs(
|
|
155
|
+
input_1: str,
|
|
156
|
+
input_2: str,
|
|
157
|
+
output_name: str,
|
|
158
|
+
folder: str | None = None,
|
|
159
|
+
storage: str | None = None,
|
|
160
|
+
*,
|
|
161
|
+
api: Any | None = None,
|
|
162
|
+
config: AsposeConfig | None = None,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
pdf_api, cfg = _api_and_config(api, config)
|
|
165
|
+
effective_storage = _storage(storage, cfg)
|
|
166
|
+
merge_documents = asposepdfcloud.MergeDocuments(list=[input_1, input_2])
|
|
167
|
+
response = _call_api(
|
|
168
|
+
pdf_api.put_merge_documents,
|
|
169
|
+
output_name,
|
|
170
|
+
merge_documents,
|
|
171
|
+
folder=folder,
|
|
172
|
+
storage=effective_storage,
|
|
173
|
+
)
|
|
174
|
+
return {
|
|
175
|
+
"inputs": [input_1, input_2],
|
|
176
|
+
"output_name": output_name,
|
|
177
|
+
"folder": folder,
|
|
178
|
+
"storage": effective_storage,
|
|
179
|
+
"result": to_plain_data(response),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_text_from_rects(data: Any) -> str:
|
|
184
|
+
plain = to_plain_data(data)
|
|
185
|
+
fragments: list[str] = []
|
|
186
|
+
|
|
187
|
+
def collect(value: Any) -> None:
|
|
188
|
+
if isinstance(value, dict):
|
|
189
|
+
text = value.get("text") or value.get("Text")
|
|
190
|
+
if isinstance(text, str) and text:
|
|
191
|
+
fragments.append(text)
|
|
192
|
+
for item in value.values():
|
|
193
|
+
collect(item)
|
|
194
|
+
elif isinstance(value, list):
|
|
195
|
+
for item in value:
|
|
196
|
+
collect(item)
|
|
197
|
+
|
|
198
|
+
collect(plain)
|
|
199
|
+
return "\n".join(fragments)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def extract_text(
|
|
203
|
+
name: str,
|
|
204
|
+
folder: str | None = None,
|
|
205
|
+
storage: str | None = None,
|
|
206
|
+
*,
|
|
207
|
+
api: Any | None = None,
|
|
208
|
+
config: AsposeConfig | None = None,
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
pdf_api, cfg = _api_and_config(api, config)
|
|
211
|
+
effective_storage = _storage(storage, cfg)
|
|
212
|
+
response = _call_api(
|
|
213
|
+
pdf_api.get_text,
|
|
214
|
+
name,
|
|
215
|
+
0,
|
|
216
|
+
0,
|
|
217
|
+
10000,
|
|
218
|
+
10000,
|
|
219
|
+
folder=folder,
|
|
220
|
+
storage=effective_storage,
|
|
221
|
+
)
|
|
222
|
+
raw = to_plain_data(response)
|
|
223
|
+
text = _extract_text_from_rects(raw)
|
|
224
|
+
return {
|
|
225
|
+
"name": name,
|
|
226
|
+
"folder": folder,
|
|
227
|
+
"storage": effective_storage,
|
|
228
|
+
"text": text,
|
|
229
|
+
"raw": raw,
|
|
230
|
+
}
|
apdf_cloud_cli/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apdf-cloud-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI and MCP server tools for Aspose PDF Cloud
|
|
5
|
+
Keywords: aspose,pdf,cli,mcp
|
|
6
|
+
Author: Andriy Andruhovski
|
|
7
|
+
Author-email: andruhovski@gmail.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Office/Business
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: asposepdfcloud (>=26.4.0,<27.0.0)
|
|
21
|
+
Requires-Dist: build (>=1.2.0) ; extra == "dev"
|
|
22
|
+
Requires-Dist: mcp (>=1.0.0)
|
|
23
|
+
Requires-Dist: pytest (>=8.0.0) ; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-mock (>=3.12.0) ; extra == "dev"
|
|
25
|
+
Requires-Dist: rich (>=13.0.0)
|
|
26
|
+
Requires-Dist: twine (>=5.0.0) ; extra == "dev"
|
|
27
|
+
Requires-Dist: typer (>=0.12.0)
|
|
28
|
+
Project-URL: Homepage, https://github.com/andruhovski/apdf-tools
|
|
29
|
+
Project-URL: Issues, https://github.com/andruhovski/apdf-tools/issues
|
|
30
|
+
Project-URL: Repository, https://github.com/andruhovski/apdf-tools
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# APDF Cloud CLI + MCP Tools
|
|
34
|
+
|
|
35
|
+
Stage 1 exposes APDF storage and PDF operations through both a CLI
|
|
36
|
+
and an MCP server.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
Set credentials before running CLI commands or the MCP server:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
$env:ASPOSE_CLIENT_ID = "your-client-id"
|
|
44
|
+
$env:ASPOSE_CLIENT_SECRET = "your-client-secret"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional environment variables:
|
|
48
|
+
|
|
49
|
+
- `ASPOSE_BASE_URL`
|
|
50
|
+
- `ASPOSE_SELF_HOST`
|
|
51
|
+
- `ASPOSE_STORAGE_NAME`
|
|
52
|
+
|
|
53
|
+
## CLI
|
|
54
|
+
|
|
55
|
+
The canonical executable is `apdf-cloud-cli`. A shorter `apdf`
|
|
56
|
+
alias is also installed for convenience.
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
apdf-cloud-cli storage list /
|
|
60
|
+
apdf-cloud-cli storage upload .\sample.pdf /sample.pdf
|
|
61
|
+
apdf-cloud-cli storage download /sample.pdf .\sample.pdf
|
|
62
|
+
apdf-cloud-cli pdf merge /a.pdf /b.pdf merged.pdf
|
|
63
|
+
apdf-cloud-cli pdf extract-text sample.pdf --output sample.txt
|
|
64
|
+
apdf-cloud-cli mcp serve
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
Install the package with development dependencies:
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
python -m pip install -e ".[dev]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run the default test suite:
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
python -m pytest -q
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Build and validate the package distributions:
|
|
82
|
+
|
|
83
|
+
```powershell
|
|
84
|
+
python -m build
|
|
85
|
+
python -m twine check dist/*
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Publishing
|
|
89
|
+
|
|
90
|
+
Publishing is handled by GitHub Actions with PyPI trusted publishing.
|
|
91
|
+
|
|
92
|
+
1. In PyPI, add a trusted publisher for repository
|
|
93
|
+
`andruhovski/apdf-tools`, workflow `publish.yml`, environment `pypi`,
|
|
94
|
+
and project name `apdf-cloud-cli`.
|
|
95
|
+
2. Update the version in `pyproject.toml` and
|
|
96
|
+
`src/apdf_cloud_cli/__init__.py`.
|
|
97
|
+
3. Push a matching version tag, for example:
|
|
98
|
+
|
|
99
|
+
```powershell
|
|
100
|
+
git tag v0.1.0
|
|
101
|
+
git push origin v0.1.0
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Stage 2 Roadmap
|
|
105
|
+
|
|
106
|
+
- Split PDF files
|
|
107
|
+
- Extract images from PDF files
|
|
108
|
+
- Extract tables from PDF files
|
|
109
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
apdf_cloud_cli/__init__.py,sha256=WQvXqxmVCUsQsd_kiwbDwfUhQhQIxUIwW_cYHUK0QX0,84
|
|
2
|
+
apdf_cloud_cli/cli.py,sha256=b5QGUryFsVUzJqBRIUtQab7_OEwI-qHXyWTeq-1jGQY,5562
|
|
3
|
+
apdf_cloud_cli/client.py,sha256=F9W_GbdJvGAE747CtIcBZIjkvyDQq4WXrCOmqSHNJ-M,502
|
|
4
|
+
apdf_cloud_cli/config.py,sha256=jh5GUZmZKOGjP-l01vRFDw32y79NGeR2oD51sKPGUN0,2225
|
|
5
|
+
apdf_cloud_cli/mcp_server.py,sha256=f-E7WDElSOY3NyX_NQJt5BUxlM7wNV0sj8gekv-Qk-c,1940
|
|
6
|
+
apdf_cloud_cli/operations.py,sha256=JdIu4Qbb9kwkcvvBHEyQXlDYO9VvexZPkOHzPbP5VkQ,6732
|
|
7
|
+
apdf_cloud_cli/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
apdf_cloud_cli-0.1.0.dist-info/METADATA,sha256=0dZj3WYfU-e8exPMVVMfTLGl-T3HUgMNQIONJTuhVn4,2951
|
|
9
|
+
apdf_cloud_cli-0.1.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
|
|
10
|
+
apdf_cloud_cli-0.1.0.dist-info/entry_points.txt,sha256=Tonz2NfAIqASp5YTh5g8OhWuI-RkaFS8Q9YG5jo3sOg,87
|
|
11
|
+
apdf_cloud_cli-0.1.0.dist-info/RECORD,,
|