asp-cli 0.1.0__tar.gz → 0.5.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.
- {asp_cli-0.1.0 → asp_cli-0.5.0}/.gitignore +39 -39
- {asp_cli-0.1.0 → asp_cli-0.5.0}/LICENSE +21 -21
- {asp_cli-0.1.0 → asp_cli-0.5.0}/PKG-INFO +49 -2
- asp_cli-0.5.0/README.md +72 -0
- {asp_cli-0.1.0 → asp_cli-0.5.0}/pyproject.toml +48 -48
- asp_cli-0.5.0/src/asp_cli/__init__.py +23 -0
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/api_client.py +118 -118
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/config.py +176 -176
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/errors.py +26 -26
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/main.py +1478 -1461
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/output.py +66 -66
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/spec/operations.json +404 -404
- {asp_cli-0.1.0 → asp_cli-0.5.0}/uv.lock +333 -333
- asp_cli-0.1.0/README.md +0 -25
- asp_cli-0.1.0/src/asp_cli/__init__.py +0 -1
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/py.typed +0 -0
- {asp_cli-0.1.0 → asp_cli-0.5.0}/src/asp_cli/spec/__init__.py +0 -0
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
# Python
|
|
2
|
-
__pycache__/
|
|
3
|
-
*.py[cod]
|
|
4
|
-
*.egg-info/
|
|
5
|
-
dist/
|
|
6
|
-
build/
|
|
7
|
-
*.egg
|
|
8
|
-
|
|
9
|
-
# Virtual environment
|
|
10
|
-
.venv/
|
|
11
|
-
|
|
12
|
-
# Django
|
|
13
|
-
*.sqlite3
|
|
14
|
-
media/
|
|
15
|
-
staticfiles/
|
|
16
|
-
|
|
17
|
-
# Environment
|
|
18
|
-
.env
|
|
19
|
-
.env.local
|
|
20
|
-
|
|
21
|
-
# IDE
|
|
22
|
-
.idea/
|
|
23
|
-
.vscode/
|
|
24
|
-
*.swp
|
|
25
|
-
*.swo
|
|
26
|
-
|
|
27
|
-
# Node
|
|
28
|
-
node_modules/
|
|
29
|
-
|
|
30
|
-
# Local git worktrees
|
|
31
|
-
.worktrees/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# OS
|
|
36
|
-
.DS_Store
|
|
37
|
-
Thumbs.db
|
|
38
|
-
|
|
39
|
-
.claude/*
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg
|
|
8
|
+
|
|
9
|
+
# Virtual environment
|
|
10
|
+
.venv/
|
|
11
|
+
|
|
12
|
+
# Django
|
|
13
|
+
*.sqlite3
|
|
14
|
+
media/
|
|
15
|
+
staticfiles/
|
|
16
|
+
|
|
17
|
+
# Environment
|
|
18
|
+
.env
|
|
19
|
+
.env.local
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea/
|
|
23
|
+
.vscode/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# Node
|
|
28
|
+
node_modules/
|
|
29
|
+
|
|
30
|
+
# Local git worktrees
|
|
31
|
+
.worktrees/
|
|
32
|
+
#/asf-doc/
|
|
33
|
+
#/asp-marketplace/
|
|
34
|
+
|
|
35
|
+
# OS
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
.claude/*
|
|
40
40
|
.asp/
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Agentic SOC Platform contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agentic SOC Platform contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Command line client for Agentic SOC Platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/FunnyWolf/agentic-soc-platform
|
|
6
6
|
Project-URL: Repository, https://github.com/FunnyWolf/agentic-soc-platform
|
|
@@ -47,8 +47,55 @@ asp doctor
|
|
|
47
47
|
asp case list
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`asp auth login` verifies the API URL and API key against the ASP server before writing local settings.
|
|
51
|
+
|
|
52
|
+
For automation and skills, prefer stable JSON output:
|
|
51
53
|
|
|
52
54
|
```powershell
|
|
53
55
|
asp case list --output json
|
|
54
56
|
```
|
|
57
|
+
|
|
58
|
+
## Publish to PyPI
|
|
59
|
+
|
|
60
|
+
The GitHub release workflow publishes `asp-cli` to PyPI automatically when a `v<version>` tag is pushed. The CLI package version does not include the leading `v` and must match the main release version, so update `version` in `pyproject.toml` before creating the tag.
|
|
61
|
+
|
|
62
|
+
For example, release tag `v0.1.0` publishes PyPI version `0.1.0`.
|
|
63
|
+
|
|
64
|
+
### Automatic publishing
|
|
65
|
+
|
|
66
|
+
The workflow uses PyPI Trusted Publishing, so it does not need a PyPI API token in the repository or GitHub Secrets. Configure PyPI once with a trusted publisher:
|
|
67
|
+
|
|
68
|
+
- PyPI project: `asp-cli`
|
|
69
|
+
- Owner/repository: `FunnyWolf/agentic-soc-platform`
|
|
70
|
+
- Workflow: `release.yml`
|
|
71
|
+
- Environment: `pypi`
|
|
72
|
+
|
|
73
|
+
If the PyPI project does not exist yet, add the same entry under PyPI's pending publishers before the first release.
|
|
74
|
+
|
|
75
|
+
Release steps:
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
# Update cli\pyproject.toml first, for example: version = "0.1.0"
|
|
79
|
+
git tag v0.1.0
|
|
80
|
+
git push origin v0.1.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Manual fallback
|
|
84
|
+
|
|
85
|
+
Manual publishing requires a PyPI API token. Keep it local and do not commit it:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
cd cli
|
|
89
|
+
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
|
|
90
|
+
uv build
|
|
91
|
+
uvx twine check dist\*
|
|
92
|
+
$env:UV_PUBLISH_TOKEN = "pypi-..."
|
|
93
|
+
uv publish --token $env:UV_PUBLISH_TOKEN
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
After publishing, verify the package can be installed:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
pipx install --force asp-cli
|
|
100
|
+
asp --version
|
|
101
|
+
```
|
asp_cli-0.5.0/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# ASP CLI
|
|
2
|
+
|
|
3
|
+
Command line client for Agentic SOC Platform.
|
|
4
|
+
|
|
5
|
+
`asp-cli` provides the `asp` command for SOC analysts and automation agents to authenticate with an ASP server, inspect cases and alerts, add comments, upload files, run playbooks, and query investigation integrations.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
pipx install asp-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
asp auth login --api-url https://asp.example.com --api-key asp_xxx
|
|
17
|
+
asp doctor
|
|
18
|
+
asp case list
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`asp auth login` verifies the API URL and API key against the ASP server before writing local settings.
|
|
22
|
+
|
|
23
|
+
For automation and skills, prefer stable JSON output:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
asp case list --output json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Publish to PyPI
|
|
30
|
+
|
|
31
|
+
The GitHub release workflow publishes `asp-cli` to PyPI automatically when a `v<version>` tag is pushed. The CLI package version does not include the leading `v` and must match the main release version, so update `version` in `pyproject.toml` before creating the tag.
|
|
32
|
+
|
|
33
|
+
For example, release tag `v0.1.0` publishes PyPI version `0.1.0`.
|
|
34
|
+
|
|
35
|
+
### Automatic publishing
|
|
36
|
+
|
|
37
|
+
The workflow uses PyPI Trusted Publishing, so it does not need a PyPI API token in the repository or GitHub Secrets. Configure PyPI once with a trusted publisher:
|
|
38
|
+
|
|
39
|
+
- PyPI project: `asp-cli`
|
|
40
|
+
- Owner/repository: `FunnyWolf/agentic-soc-platform`
|
|
41
|
+
- Workflow: `release.yml`
|
|
42
|
+
- Environment: `pypi`
|
|
43
|
+
|
|
44
|
+
If the PyPI project does not exist yet, add the same entry under PyPI's pending publishers before the first release.
|
|
45
|
+
|
|
46
|
+
Release steps:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
# Update cli\pyproject.toml first, for example: version = "0.1.0"
|
|
50
|
+
git tag v0.1.0
|
|
51
|
+
git push origin v0.1.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Manual fallback
|
|
55
|
+
|
|
56
|
+
Manual publishing requires a PyPI API token. Keep it local and do not commit it:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
cd cli
|
|
60
|
+
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
|
|
61
|
+
uv build
|
|
62
|
+
uvx twine check dist\*
|
|
63
|
+
$env:UV_PUBLISH_TOKEN = "pypi-..."
|
|
64
|
+
uv publish --token $env:UV_PUBLISH_TOKEN
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
After publishing, verify the package can be installed:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
pipx install --force asp-cli
|
|
71
|
+
asp --version
|
|
72
|
+
```
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "asp-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Command line client for Agentic SOC Platform"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.11"
|
|
7
|
-
license = "MIT"
|
|
8
|
-
authors = [
|
|
9
|
-
{ name = "Agentic SOC Platform contributors" },
|
|
10
|
-
]
|
|
11
|
-
keywords = ["agentic-soc", "soc", "security", "cli"]
|
|
12
|
-
classifiers = [
|
|
13
|
-
"Development Status :: 3 - Alpha",
|
|
14
|
-
"Environment :: Console",
|
|
15
|
-
"Intended Audience :: Information Technology",
|
|
16
|
-
"License :: OSI Approved :: MIT License",
|
|
17
|
-
"Operating System :: OS Independent",
|
|
18
|
-
"Programming Language :: Python :: 3",
|
|
19
|
-
"Programming Language :: Python :: 3.11",
|
|
20
|
-
"Programming Language :: Python :: 3.12",
|
|
21
|
-
"Programming Language :: Python :: 3.13",
|
|
22
|
-
"Topic :: Security",
|
|
23
|
-
]
|
|
24
|
-
dependencies = [
|
|
25
|
-
"httpx>=0.28.1",
|
|
26
|
-
"jmespath>=1.0.1",
|
|
27
|
-
"pydantic>=2.13.4",
|
|
28
|
-
"rich>=14.2.0",
|
|
29
|
-
"typer>=0.20.0",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
[project.urls]
|
|
33
|
-
Homepage = "https://github.com/FunnyWolf/agentic-soc-platform"
|
|
34
|
-
Repository = "https://github.com/FunnyWolf/agentic-soc-platform"
|
|
35
|
-
Issues = "https://github.com/FunnyWolf/agentic-soc-platform/issues"
|
|
36
|
-
|
|
37
|
-
[project.scripts]
|
|
38
|
-
asp = "asp_cli.main:run"
|
|
39
|
-
|
|
40
|
-
[build-system]
|
|
41
|
-
requires = ["hatchling>=1.28"]
|
|
42
|
-
build-backend = "hatchling.build"
|
|
43
|
-
|
|
44
|
-
[tool.hatch.build.targets.wheel]
|
|
45
|
-
packages = ["src/asp_cli"]
|
|
46
|
-
|
|
47
|
-
[tool.hatch.build.targets.wheel.force-include]
|
|
48
|
-
"src/asp_cli/spec/operations.json" = "asp_cli/spec/operations.json"
|
|
1
|
+
[project]
|
|
2
|
+
name = "asp-cli"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Command line client for Agentic SOC Platform"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Agentic SOC Platform contributors" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["agentic-soc", "soc", "security", "cli"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Information Technology",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"httpx>=0.28.1",
|
|
26
|
+
"jmespath>=1.0.1",
|
|
27
|
+
"pydantic>=2.13.4",
|
|
28
|
+
"rich>=14.2.0",
|
|
29
|
+
"typer>=0.20.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/FunnyWolf/agentic-soc-platform"
|
|
34
|
+
Repository = "https://github.com/FunnyWolf/agentic-soc-platform"
|
|
35
|
+
Issues = "https://github.com/FunnyWolf/agentic-soc-platform/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
asp = "asp_cli.main:run"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["hatchling>=1.28"]
|
|
42
|
+
build-backend = "hatchling.build"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/asp_cli"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
48
|
+
"src/asp_cli/spec/operations.json" = "asp_cli/spec/operations.json"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _source_version() -> str | None:
|
|
9
|
+
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
10
|
+
if not pyproject_path.exists():
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
14
|
+
project_version = pyproject.get("project", {}).get("version")
|
|
15
|
+
if not isinstance(project_version, str):
|
|
16
|
+
raise RuntimeError("Missing project.version in cli pyproject.toml")
|
|
17
|
+
return project_version
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = _source_version() or version("asp-cli")
|
|
22
|
+
except PackageNotFoundError as exc:
|
|
23
|
+
raise RuntimeError("Cannot resolve asp-cli package version") from exc
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
|
|
9
|
-
from . import __version__
|
|
10
|
-
from .config import redact_secret
|
|
11
|
-
from .errors import (
|
|
12
|
-
CliError,
|
|
13
|
-
EXIT_AUTH,
|
|
14
|
-
EXIT_NETWORK,
|
|
15
|
-
EXIT_NOT_FOUND,
|
|
16
|
-
EXIT_PERMISSION,
|
|
17
|
-
EXIT_SERVER,
|
|
18
|
-
EXIT_USAGE,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AspClient:
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
*,
|
|
26
|
-
api_url: str,
|
|
27
|
-
api_key: str | None = None,
|
|
28
|
-
verbose: bool = False,
|
|
29
|
-
console: Console | None = None,
|
|
30
|
-
timeout: float = 20.0,
|
|
31
|
-
) -> None:
|
|
32
|
-
self.base_url = _normalize_base_url(api_url)
|
|
33
|
-
self.api_key = api_key
|
|
34
|
-
self.verbose = verbose
|
|
35
|
-
self.console = console or Console(stderr=True)
|
|
36
|
-
self.timeout = timeout
|
|
37
|
-
|
|
38
|
-
def health(self) -> dict[str, Any]:
|
|
39
|
-
return self.request("GET", "/api/health/", authenticated=False)
|
|
40
|
-
|
|
41
|
-
def version(self) -> dict[str, Any]:
|
|
42
|
-
return self.request("GET", "/api/agent/v1/version/")
|
|
43
|
-
|
|
44
|
-
def request(self, method: str, path: str, *, authenticated: bool = True, json: Any = None, files: Any = None) -> dict[str, Any]:
|
|
45
|
-
if authenticated and not self.api_key:
|
|
46
|
-
raise CliError("missing_api_key", "API key is required", {}, EXIT_AUTH)
|
|
47
|
-
|
|
48
|
-
headers = {
|
|
49
|
-
"Accept": "application/json",
|
|
50
|
-
"User-Agent": f"asp-cli/{__version__}",
|
|
51
|
-
}
|
|
52
|
-
if authenticated and self.api_key:
|
|
53
|
-
headers["Authorization"] = f"Api-Key {self.api_key}"
|
|
54
|
-
|
|
55
|
-
url = f"{self.base_url}{path}"
|
|
56
|
-
started = time.perf_counter()
|
|
57
|
-
try:
|
|
58
|
-
response = httpx.request(method, url, headers=headers, json=json, files=files, timeout=self.timeout)
|
|
59
|
-
except httpx.HTTPError as exc:
|
|
60
|
-
raise CliError("network_error", f"Unable to reach ASP server: {exc}", {"url": _redact_url(url)}, EXIT_NETWORK) from exc
|
|
61
|
-
|
|
62
|
-
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
|
63
|
-
if self.verbose:
|
|
64
|
-
self.console.print(f"{method} {path} -> {response.status_code} ({elapsed_ms}ms)", style="dim")
|
|
65
|
-
|
|
66
|
-
if response.status_code >= 400:
|
|
67
|
-
self._raise_http_error(response, path)
|
|
68
|
-
|
|
69
|
-
if not response.content:
|
|
70
|
-
return {}
|
|
71
|
-
try:
|
|
72
|
-
payload = response.json()
|
|
73
|
-
except ValueError as exc:
|
|
74
|
-
raise CliError("invalid_response", "Server returned non-JSON response", {"status_code": response.status_code}, EXIT_SERVER) from exc
|
|
75
|
-
if not isinstance(payload, dict):
|
|
76
|
-
raise CliError("invalid_response", "Server response must be a JSON object", {"status_code": response.status_code}, EXIT_SERVER)
|
|
77
|
-
return payload
|
|
78
|
-
|
|
79
|
-
def _raise_http_error(self, response: httpx.Response, path: str) -> None:
|
|
80
|
-
message = _response_message(response)
|
|
81
|
-
details = {"status_code": response.status_code, "path": path}
|
|
82
|
-
if response.status_code == 400:
|
|
83
|
-
raise CliError("bad_request", message, details, EXIT_USAGE)
|
|
84
|
-
if response.status_code == 401:
|
|
85
|
-
raise CliError("authentication_failed", message, details, EXIT_AUTH)
|
|
86
|
-
if response.status_code == 403:
|
|
87
|
-
raise CliError("permission_denied", message, details, EXIT_PERMISSION)
|
|
88
|
-
if response.status_code == 404:
|
|
89
|
-
raise CliError("not_found", message, details, EXIT_NOT_FOUND)
|
|
90
|
-
raise CliError("server_error", message, details, EXIT_SERVER)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _normalize_base_url(api_url: str) -> str:
|
|
94
|
-
base = api_url.strip().rstrip("/")
|
|
95
|
-
if base.endswith("/api"):
|
|
96
|
-
base = base[:-4]
|
|
97
|
-
if not base:
|
|
98
|
-
raise CliError("missing_api_url", "ASP API URL is required", {}, EXIT_USAGE)
|
|
99
|
-
return base
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _response_message(response: httpx.Response) -> str:
|
|
103
|
-
try:
|
|
104
|
-
payload = response.json()
|
|
105
|
-
except ValueError:
|
|
106
|
-
return response.text.strip() or f"HTTP {response.status_code}"
|
|
107
|
-
if isinstance(payload, dict):
|
|
108
|
-
detail = payload.get("detail")
|
|
109
|
-
if isinstance(detail, str):
|
|
110
|
-
return detail
|
|
111
|
-
error = payload.get("error")
|
|
112
|
-
if isinstance(error, dict) and isinstance(error.get("message"), str):
|
|
113
|
-
return error["message"]
|
|
114
|
-
return f"HTTP {response.status_code}"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _redact_url(url: str) -> str:
|
|
118
|
-
return url.replace(redact_secret(url), "****") if "asp_" in url else url
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .config import redact_secret
|
|
11
|
+
from .errors import (
|
|
12
|
+
CliError,
|
|
13
|
+
EXIT_AUTH,
|
|
14
|
+
EXIT_NETWORK,
|
|
15
|
+
EXIT_NOT_FOUND,
|
|
16
|
+
EXIT_PERMISSION,
|
|
17
|
+
EXIT_SERVER,
|
|
18
|
+
EXIT_USAGE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AspClient:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
api_url: str,
|
|
27
|
+
api_key: str | None = None,
|
|
28
|
+
verbose: bool = False,
|
|
29
|
+
console: Console | None = None,
|
|
30
|
+
timeout: float = 20.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.base_url = _normalize_base_url(api_url)
|
|
33
|
+
self.api_key = api_key
|
|
34
|
+
self.verbose = verbose
|
|
35
|
+
self.console = console or Console(stderr=True)
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
|
|
38
|
+
def health(self) -> dict[str, Any]:
|
|
39
|
+
return self.request("GET", "/api/health/", authenticated=False)
|
|
40
|
+
|
|
41
|
+
def version(self) -> dict[str, Any]:
|
|
42
|
+
return self.request("GET", "/api/agent/v1/version/")
|
|
43
|
+
|
|
44
|
+
def request(self, method: str, path: str, *, authenticated: bool = True, json: Any = None, files: Any = None) -> dict[str, Any]:
|
|
45
|
+
if authenticated and not self.api_key:
|
|
46
|
+
raise CliError("missing_api_key", "API key is required", {}, EXIT_AUTH)
|
|
47
|
+
|
|
48
|
+
headers = {
|
|
49
|
+
"Accept": "application/json",
|
|
50
|
+
"User-Agent": f"asp-cli/{__version__}",
|
|
51
|
+
}
|
|
52
|
+
if authenticated and self.api_key:
|
|
53
|
+
headers["Authorization"] = f"Api-Key {self.api_key}"
|
|
54
|
+
|
|
55
|
+
url = f"{self.base_url}{path}"
|
|
56
|
+
started = time.perf_counter()
|
|
57
|
+
try:
|
|
58
|
+
response = httpx.request(method, url, headers=headers, json=json, files=files, timeout=self.timeout)
|
|
59
|
+
except httpx.HTTPError as exc:
|
|
60
|
+
raise CliError("network_error", f"Unable to reach ASP server: {exc}", {"url": _redact_url(url)}, EXIT_NETWORK) from exc
|
|
61
|
+
|
|
62
|
+
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
|
63
|
+
if self.verbose:
|
|
64
|
+
self.console.print(f"{method} {path} -> {response.status_code} ({elapsed_ms}ms)", style="dim")
|
|
65
|
+
|
|
66
|
+
if response.status_code >= 400:
|
|
67
|
+
self._raise_http_error(response, path)
|
|
68
|
+
|
|
69
|
+
if not response.content:
|
|
70
|
+
return {}
|
|
71
|
+
try:
|
|
72
|
+
payload = response.json()
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise CliError("invalid_response", "Server returned non-JSON response", {"status_code": response.status_code}, EXIT_SERVER) from exc
|
|
75
|
+
if not isinstance(payload, dict):
|
|
76
|
+
raise CliError("invalid_response", "Server response must be a JSON object", {"status_code": response.status_code}, EXIT_SERVER)
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
def _raise_http_error(self, response: httpx.Response, path: str) -> None:
|
|
80
|
+
message = _response_message(response)
|
|
81
|
+
details = {"status_code": response.status_code, "path": path}
|
|
82
|
+
if response.status_code == 400:
|
|
83
|
+
raise CliError("bad_request", message, details, EXIT_USAGE)
|
|
84
|
+
if response.status_code == 401:
|
|
85
|
+
raise CliError("authentication_failed", message, details, EXIT_AUTH)
|
|
86
|
+
if response.status_code == 403:
|
|
87
|
+
raise CliError("permission_denied", message, details, EXIT_PERMISSION)
|
|
88
|
+
if response.status_code == 404:
|
|
89
|
+
raise CliError("not_found", message, details, EXIT_NOT_FOUND)
|
|
90
|
+
raise CliError("server_error", message, details, EXIT_SERVER)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _normalize_base_url(api_url: str) -> str:
|
|
94
|
+
base = api_url.strip().rstrip("/")
|
|
95
|
+
if base.endswith("/api"):
|
|
96
|
+
base = base[:-4]
|
|
97
|
+
if not base:
|
|
98
|
+
raise CliError("missing_api_url", "ASP API URL is required", {}, EXIT_USAGE)
|
|
99
|
+
return base
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _response_message(response: httpx.Response) -> str:
|
|
103
|
+
try:
|
|
104
|
+
payload = response.json()
|
|
105
|
+
except ValueError:
|
|
106
|
+
return response.text.strip() or f"HTTP {response.status_code}"
|
|
107
|
+
if isinstance(payload, dict):
|
|
108
|
+
detail = payload.get("detail")
|
|
109
|
+
if isinstance(detail, str):
|
|
110
|
+
return detail
|
|
111
|
+
error = payload.get("error")
|
|
112
|
+
if isinstance(error, dict) and isinstance(error.get("message"), str):
|
|
113
|
+
return error["message"]
|
|
114
|
+
return f"HTTP {response.status_code}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _redact_url(url: str) -> str:
|
|
118
|
+
return url.replace(redact_secret(url), "****") if "asp_" in url else url
|