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.
@@ -0,0 +1,5 @@
1
+ """CLI and MCP tools for APDF."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.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()
@@ -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)
@@ -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
+ }
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ apdf=apdf_cloud_cli.cli:main
3
+ apdf-cloud-cli=apdf_cloud_cli.cli:main
4
+