apdf-cloud-cli 0.2.0__tar.gz → 0.3.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.
Files changed (19) hide show
  1. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/PKG-INFO +13 -2
  2. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/README.md +12 -1
  3. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/pyproject.toml +1 -1
  4. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/__init__.py +1 -1
  5. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/cli.py +144 -1
  6. apdf_cloud_cli-0.3.0/src/apdf_cloud_cli/config.py +178 -0
  7. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/operations.py +23 -0
  8. apdf_cloud_cli-0.2.0/src/apdf_cloud_cli/config.py +0 -75
  9. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/client.py +0 -0
  10. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/mcp_server.py +0 -0
  11. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/py.typed +0 -0
  12. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/SKILL.md +0 -0
  13. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/agents/openai.yaml +0 -0
  14. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/references/live-tests.md +0 -0
  15. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/references/mcp-config.md +0 -0
  16. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/references/prompts.md +0 -0
  17. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/references/security.md +0 -0
  18. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/resources/skills/apdf-cloud-mcp/references/setup.md +0 -0
  19. {apdf_cloud_cli-0.2.0 → apdf_cloud_cli-0.3.0}/src/apdf_cloud_cli/skill_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apdf-cloud-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI and MCP server tools for Aspose PDF Cloud
5
5
  Keywords: aspose,pdf,cli,mcp
6
6
  Author: Andriy Andruhovski
@@ -58,7 +58,16 @@ python -m pip install -e ".[dev]"
58
58
 
59
59
  ## Configuration
60
60
 
61
- Set credentials before running CLI commands or the MCP server:
61
+ Set credentials before running CLI commands or the MCP server. The easiest
62
+ local setup path is:
63
+
64
+ ```powershell
65
+ apdf auth login
66
+ apdf auth status
67
+ apdf auth test
68
+ ```
69
+
70
+ You can also set credentials directly in your shell:
62
71
 
63
72
  ```powershell
64
73
  $env:ASPOSE_CLIENT_ID = "your-client-id"
@@ -82,6 +91,8 @@ also installed for convenience.
82
91
 
83
92
  ```powershell
84
93
  apdf-cloud-cli storage list /
94
+ apdf-cloud-cli auth status
95
+ apdf-cloud-cli auth test
85
96
  apdf-cloud-cli storage upload .\sample.pdf /sample.pdf
86
97
  apdf-cloud-cli storage download /sample.pdf .\sample.pdf
87
98
  apdf-cloud-cli pdf merge /a.pdf /b.pdf merged.pdf
@@ -26,7 +26,16 @@ python -m pip install -e ".[dev]"
26
26
 
27
27
  ## Configuration
28
28
 
29
- Set credentials before running CLI commands or the MCP server:
29
+ Set credentials before running CLI commands or the MCP server. The easiest
30
+ local setup path is:
31
+
32
+ ```powershell
33
+ apdf auth login
34
+ apdf auth status
35
+ apdf auth test
36
+ ```
37
+
38
+ You can also set credentials directly in your shell:
30
39
 
31
40
  ```powershell
32
41
  $env:ASPOSE_CLIENT_ID = "your-client-id"
@@ -50,6 +59,8 @@ also installed for convenience.
50
59
 
51
60
  ```powershell
52
61
  apdf-cloud-cli storage list /
62
+ apdf-cloud-cli auth status
63
+ apdf-cloud-cli auth test
53
64
  apdf-cloud-cli storage upload .\sample.pdf /sample.pdf
54
65
  apdf-cloud-cli storage download /sample.pdf .\sample.pdf
55
66
  apdf-cloud-cli pdf merge /a.pdf /b.pdf merged.pdf
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "apdf-cloud-cli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "CLI and MCP server tools for Aspose PDF Cloud"
5
5
  authors = [
6
6
  {name = "Andriy Andruhovski",email = "andruhovski@gmail.com"}
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.0"
@@ -11,14 +11,16 @@ from rich.console import Console
11
11
  from rich.table import Table
12
12
 
13
13
  from . import operations
14
- from .config import ConfigError
14
+ from .config import ConfigError, get_auth_status, write_auth_env_file
15
15
  from .skill_installer import SkillInstallError, install_skill
16
16
 
17
17
  app = typer.Typer(help="APDF CLI tools.")
18
+ auth_app = typer.Typer(help="Credential setup and validation.")
18
19
  storage_app = typer.Typer(help="Cloud storage operations.")
19
20
  pdf_app = typer.Typer(help="PDF operations.")
20
21
  mcp_app = typer.Typer(help="MCP server commands.")
21
22
  skill_app = typer.Typer(help="Install bundled agent skills.")
23
+ app.add_typer(auth_app, name="auth")
22
24
  app.add_typer(storage_app, name="storage")
23
25
  app.add_typer(pdf_app, name="pdf")
24
26
  app.add_typer(mcp_app, name="mcp")
@@ -43,6 +45,147 @@ def _run(action):
43
45
  _handle_error(exc)
44
46
 
45
47
 
48
+ @auth_app.command("status")
49
+ def auth_status(
50
+ env_file: Annotated[
51
+ Path | None,
52
+ typer.Option("--env-file", help="Local .env file to inspect."),
53
+ ] = Path(".env"),
54
+ as_json: Annotated[bool, typer.Option("--json", help="Emit JSON output.")] = False,
55
+ ) -> None:
56
+ """Show redacted APDF credential configuration status."""
57
+
58
+ status = get_auth_status(env_file)
59
+ if as_json:
60
+ _json(status)
61
+ return
62
+
63
+ console.print(
64
+ "[green]Configured[/green]"
65
+ if status["configured"]
66
+ else "[yellow]Missing required credentials[/yellow]"
67
+ )
68
+ table = Table(title=f"Auth status: {status['env_file'] or 'environment only'}")
69
+ table.add_column("Setting")
70
+ table.add_column("Source")
71
+ table.add_column("Value")
72
+
73
+ settings = status["settings"]
74
+ if isinstance(settings, dict):
75
+ for key, details in settings.items():
76
+ if not isinstance(details, dict):
77
+ continue
78
+ table.add_row(
79
+ key,
80
+ str(details.get("source") or "missing"),
81
+ str(details.get("value") or ""),
82
+ )
83
+ console.print(table)
84
+
85
+
86
+ @auth_app.command("login")
87
+ def auth_login(
88
+ env_file: Annotated[
89
+ Path,
90
+ typer.Option("--env-file", help="Local .env file to write."),
91
+ ] = Path(".env"),
92
+ client_id: Annotated[
93
+ str | None,
94
+ typer.Option("--client-id", help="Aspose Cloud client ID."),
95
+ ] = None,
96
+ client_secret: Annotated[
97
+ str | None,
98
+ typer.Option("--client-secret", help="Aspose Cloud client secret."),
99
+ ] = None,
100
+ storage_name: Annotated[
101
+ str | None,
102
+ typer.Option("--storage", help="Default Aspose storage name."),
103
+ ] = None,
104
+ base_url: Annotated[
105
+ str | None,
106
+ typer.Option("--base-url", help="Alternate Aspose PDF Cloud base URL."),
107
+ ] = None,
108
+ self_host: Annotated[
109
+ bool,
110
+ typer.Option("--self-host/--no-self-host", help="Use self-hosted Aspose PDF Cloud."),
111
+ ] = False,
112
+ force: Annotated[
113
+ bool,
114
+ typer.Option("--force", help="Overwrite existing APDF values in the env file."),
115
+ ] = False,
116
+ ) -> None:
117
+ """Save APDF credentials to a local .env file."""
118
+
119
+ status = get_auth_status(env_file)
120
+ settings = status.get("settings", {})
121
+ has_existing_file_values = (
122
+ isinstance(settings, dict)
123
+ and any(
124
+ isinstance(details, dict) and details.get("source") == "env_file"
125
+ for details in settings.values()
126
+ )
127
+ )
128
+ if has_existing_file_values and not force:
129
+ typer.confirm(
130
+ f"{env_file} already contains APDF settings. Overwrite them?",
131
+ abort=True,
132
+ )
133
+
134
+ prompt_optional = client_id is None or client_secret is None
135
+ if client_id is None:
136
+ client_id = typer.prompt("Aspose client ID")
137
+ if client_secret is None:
138
+ client_secret = typer.prompt("Aspose client secret", hide_input=True)
139
+ if prompt_optional and storage_name is None:
140
+ storage_name = typer.prompt(
141
+ "Default storage name (optional)",
142
+ default="",
143
+ show_default=False,
144
+ )
145
+ if prompt_optional and base_url is None:
146
+ base_url = typer.prompt(
147
+ "Base URL (optional)",
148
+ default="",
149
+ show_default=False,
150
+ )
151
+
152
+ result = _run(
153
+ lambda: write_auth_env_file(
154
+ env_file,
155
+ {
156
+ "ASPOSE_CLIENT_ID": client_id,
157
+ "ASPOSE_CLIENT_SECRET": client_secret,
158
+ "ASPOSE_STORAGE_NAME": storage_name,
159
+ "ASPOSE_BASE_URL": base_url,
160
+ "ASPOSE_SELF_HOST": self_host,
161
+ },
162
+ )
163
+ )
164
+ console.print(f"Saved APDF credentials to [bold]{result}[/bold].")
165
+
166
+
167
+ @auth_app.command("test")
168
+ def auth_test(
169
+ path: Annotated[
170
+ str,
171
+ typer.Option("--path", help="Storage folder path to list during validation."),
172
+ ] = "/",
173
+ storage: Annotated[str | None, typer.Option("--storage", help="Storage name.")] = None,
174
+ as_json: Annotated[bool, typer.Option("--json", help="Emit JSON output.")] = False,
175
+ ) -> None:
176
+ """Validate APDF credentials with a harmless storage API call."""
177
+
178
+ result = _run(lambda: operations.test_auth(path, storage))
179
+ if as_json:
180
+ _json(result)
181
+ return
182
+ console.print(
183
+ "[green]APDF credentials are valid.[/green] "
184
+ f"Listed [bold]{result['path']}[/bold]"
185
+ + (f" in storage [bold]{result['storage_name']}[/bold]." if result["storage_name"] else ".")
186
+ )
187
+
188
+
46
189
  @storage_app.command("list")
47
190
  def list_storage_files(
48
191
  path: Annotated[str, typer.Argument(help="Folder path in Aspose storage.")],
@@ -0,0 +1,178 @@
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
+ APDF_ENV_KEYS = (
15
+ "ASPOSE_CLIENT_ID",
16
+ "ASPOSE_CLIENT_SECRET",
17
+ "ASPOSE_STORAGE_NAME",
18
+ "ASPOSE_BASE_URL",
19
+ "ASPOSE_SELF_HOST",
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AsposeConfig:
25
+ client_id: str
26
+ client_secret: str
27
+ base_url: str | None = None
28
+ self_host: bool = False
29
+ storage_name: str | None = None
30
+
31
+
32
+ def _truthy(value: str | None) -> bool:
33
+ return value is not None and value.strip().lower() in {"1", "true", "yes", "on"}
34
+
35
+
36
+ def _read_env_file(path: str | Path | None) -> dict[str, str]:
37
+ if path is None:
38
+ return {}
39
+
40
+ env_path = Path(path)
41
+ if not env_path.is_file():
42
+ return {}
43
+
44
+ values: dict[str, str] = {}
45
+ for line in env_path.read_text(encoding="utf-8").splitlines():
46
+ stripped = line.strip()
47
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
48
+ continue
49
+ key, value = stripped.split("=", 1)
50
+ key = key.strip()
51
+ value = value.strip().strip('"').strip("'")
52
+ if key:
53
+ values[key] = value
54
+ return values
55
+
56
+
57
+ def _get_setting(name: str, env_file_values: dict[str, str]) -> str:
58
+ return os.getenv(name, env_file_values.get(name, "")).strip()
59
+
60
+
61
+ def _format_env_value(value: str) -> str:
62
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
63
+ return f'"{escaped}"'
64
+
65
+
66
+ def _env_key_from_line(line: str) -> str | None:
67
+ stripped = line.strip()
68
+ if not stripped or stripped.startswith("#") or "=" not in stripped:
69
+ return None
70
+ key, _ = stripped.split("=", 1)
71
+ key = key.strip()
72
+ return key or None
73
+
74
+
75
+ def load_config(env_file: str | Path | None = ".env") -> AsposeConfig:
76
+ """Load Aspose Cloud configuration from environment variables and optional .env."""
77
+
78
+ env_file_values = _read_env_file(env_file)
79
+
80
+ client_id = _get_setting("ASPOSE_CLIENT_ID", env_file_values)
81
+ client_secret = _get_setting("ASPOSE_CLIENT_SECRET", env_file_values)
82
+
83
+ if not client_id or not client_secret:
84
+ raise ConfigError(
85
+ "Missing Aspose credentials. Set ASPOSE_CLIENT_ID and "
86
+ "ASPOSE_CLIENT_SECRET."
87
+ )
88
+
89
+ base_url = _get_setting("ASPOSE_BASE_URL", env_file_values) or None
90
+ storage_name = _get_setting("ASPOSE_STORAGE_NAME", env_file_values) or None
91
+
92
+ return AsposeConfig(
93
+ client_id=client_id,
94
+ client_secret=client_secret,
95
+ base_url=base_url,
96
+ self_host=_truthy(_get_setting("ASPOSE_SELF_HOST", env_file_values)),
97
+ storage_name=storage_name,
98
+ )
99
+
100
+
101
+ def get_auth_status(env_file: str | Path | None = ".env") -> dict[str, object]:
102
+ """Return redacted APDF credential status from environment and optional .env."""
103
+
104
+ env_file_values = _read_env_file(env_file)
105
+ settings: dict[str, dict[str, str | bool | None]] = {}
106
+
107
+ for key in APDF_ENV_KEYS:
108
+ env_value = os.getenv(key)
109
+ file_value = env_file_values.get(key)
110
+ value = (env_value if env_value is not None else file_value) or ""
111
+ source = "environment" if env_value is not None else "env_file" if file_value else "missing"
112
+ settings[key] = {
113
+ "configured": bool(value.strip()),
114
+ "source": source,
115
+ "value": redact_secret(value),
116
+ }
117
+
118
+ configured = bool(
119
+ settings["ASPOSE_CLIENT_ID"]["configured"]
120
+ and settings["ASPOSE_CLIENT_SECRET"]["configured"]
121
+ )
122
+ return {
123
+ "configured": configured,
124
+ "env_file": str(env_file) if env_file is not None else None,
125
+ "settings": settings,
126
+ }
127
+
128
+
129
+ def redact_secret(value: str | None) -> str:
130
+ """Return a display-safe representation of a secret-like value."""
131
+
132
+ if not value:
133
+ return ""
134
+ stripped = value.strip()
135
+ if len(stripped) <= 8:
136
+ return "<set>"
137
+ return f"{stripped[:4]}...{stripped[-4:]}"
138
+
139
+
140
+ def write_auth_env_file(
141
+ env_file: str | Path,
142
+ values: dict[str, str | bool | None],
143
+ ) -> Path:
144
+ """Write APDF auth settings to a .env file while preserving unrelated lines."""
145
+
146
+ env_path = Path(env_file)
147
+ updates: dict[str, str] = {}
148
+ for key in APDF_ENV_KEYS:
149
+ value = values.get(key)
150
+ if isinstance(value, bool):
151
+ updates[key] = "true" if value else "false"
152
+ elif value is not None and str(value).strip():
153
+ updates[key] = str(value).strip()
154
+
155
+ if not updates.get("ASPOSE_CLIENT_ID") or not updates.get("ASPOSE_CLIENT_SECRET"):
156
+ raise ConfigError("ASPOSE_CLIENT_ID and ASPOSE_CLIENT_SECRET are required.")
157
+
158
+ lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
159
+ written_keys: set[str] = set()
160
+ output: list[str] = []
161
+
162
+ for line in lines:
163
+ key = _env_key_from_line(line)
164
+ if key in updates:
165
+ output.append(f"{key}={_format_env_value(updates[key])}")
166
+ written_keys.add(key)
167
+ else:
168
+ output.append(line)
169
+
170
+ if output and any(key not in written_keys for key in updates):
171
+ output.append("")
172
+ for key in APDF_ENV_KEYS:
173
+ if key in updates and key not in written_keys:
174
+ output.append(f"{key}={_format_env_value(updates[key])}")
175
+
176
+ env_path.parent.mkdir(parents=True, exist_ok=True)
177
+ env_path.write_text("\n".join(output) + "\n", encoding="utf-8")
178
+ return env_path
@@ -83,6 +83,29 @@ def list_files(
83
83
  return {"path": path, "storage_name": effective_storage, "items": to_plain_data(response)}
84
84
 
85
85
 
86
+ def test_auth(
87
+ path: str = "/",
88
+ storage_name: str | None = None,
89
+ *,
90
+ api: Any | None = None,
91
+ config: AsposeConfig | None = None,
92
+ ) -> dict[str, Any]:
93
+ """Validate APDF credentials with a harmless storage listing."""
94
+
95
+ result = list_files(path, storage_name, api=api, config=config)
96
+ items = result.get("items", {})
97
+ values = items.get("value") or items.get("Value") or items.get("files") or items
98
+ if isinstance(values, dict):
99
+ values = values.get("value") or values.get("Value") or []
100
+ item_count = len(values) if isinstance(values, list) else None
101
+ return {
102
+ "ok": True,
103
+ "path": result["path"],
104
+ "storage_name": result["storage_name"],
105
+ "item_count": item_count,
106
+ }
107
+
108
+
86
109
  def upload_file(
87
110
  local_path: str | Path,
88
111
  remote_path: str,
@@ -1,75 +0,0 @@
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
- )