stackgen-sdk 0.1.1__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.
- stackgen_sdk-0.1.1/.gitignore +36 -0
- stackgen_sdk-0.1.1/PKG-INFO +60 -0
- stackgen_sdk-0.1.1/README.md +39 -0
- stackgen_sdk-0.1.1/pyproject.toml +63 -0
- stackgen_sdk-0.1.1/src/stackgen/__init__.py +52 -0
- stackgen_sdk-0.1.1/src/stackgen/_http.py +96 -0
- stackgen_sdk-0.1.1/src/stackgen/_version.py +24 -0
- stackgen_sdk-0.1.1/src/stackgen/aiden/__init__.py +9 -0
- stackgen_sdk-0.1.1/src/stackgen/aiden/namespace.py +26 -0
- stackgen_sdk-0.1.1/src/stackgen/aiden/runs.py +36 -0
- stackgen_sdk-0.1.1/src/stackgen/aiden/webhook.py +185 -0
- stackgen_sdk-0.1.1/src/stackgen/cli.py +33 -0
- stackgen_sdk-0.1.1/src/stackgen/client.py +31 -0
- stackgen_sdk-0.1.1/src/stackgen/config.py +88 -0
- stackgen_sdk-0.1.1/src/stackgen/errors.py +41 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/.openapi-generator/FILES +141 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/.openapi-generator/VERSION +1 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/.openapi-generator-ignore +23 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/__init__.py +131 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/api/__init__.py +6 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/api/aiden_api.py +1806 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/api/sre_api.py +1618 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/api_client.py +801 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/api_response.py +21 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/configuration.py +600 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/exceptions.py +216 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/__init__.py +57 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_analysis_status.py +40 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_attention.py +39 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_categorization_summary.py +97 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_role.py +38 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_sort_by.py +37 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_status.py +38 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_summary.py +95 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_sync_run_status.py +38 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/alert_v1.py +213 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/artifact_info.py +98 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/error_response.py +91 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigate_alert_request.py +91 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigate_response.py +91 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation.py +167 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_evidence.py +114 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_evidence_kind.py +44 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_evidence_source.py +39 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_hypothesis.py +112 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_plain_summary.py +104 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_prior_incident.py +109 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_recommended_next_step.py +101 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_ref.py +118 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_status.py +42 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_structured_hypothesis_entry.py +115 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_structured_limitation_entry.py +120 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_structured_rca.py +139 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/investigation_triage_metadata.py +134 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/json_error.py +91 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/list_alerts_response.py +119 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/list_investigations_response.py +101 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/pagination.py +91 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/schedule_run_status.py +39 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/schedule_target_type.py +38 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/session.py +141 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/session_responder_kind.py +40 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/signal_severity.py +40 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/sync_response.py +136 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/trigger_webhook202_response.py +98 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/webhook_run.py +129 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/webhook_run_detail.py +136 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/models/webhook_run_list_response.py +97 -0
- stackgen_sdk-0.1.1/src/stackgen/generated/rest.py +258 -0
- stackgen_sdk-0.1.1/src/stackgen/sre/__init__.py +5 -0
- stackgen_sdk-0.1.1/src/stackgen/sre/namespace.py +57 -0
- stackgen_sdk-0.1.1/src/stackgen/vault/__init__.py +5 -0
- stackgen_sdk-0.1.1/src/stackgen/vault/namespace.py +17 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.venv/**
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
|
|
15
|
+
# Go
|
|
16
|
+
clients/go/generated/.openapi-generator/
|
|
17
|
+
*.exe
|
|
18
|
+
*.test
|
|
19
|
+
coverage.out
|
|
20
|
+
|
|
21
|
+
# OpenAPI generator scratch
|
|
22
|
+
**/deleteme/
|
|
23
|
+
**/.openapi-generator-ignore.bak
|
|
24
|
+
|
|
25
|
+
# Editors / OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
|
|
31
|
+
# Local env
|
|
32
|
+
.env
|
|
33
|
+
.env.*
|
|
34
|
+
|
|
35
|
+
# hatch-vcs generated version module (written at build time)
|
|
36
|
+
clients/python/src/stackgen/_version.py
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stackgen-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: StackGen SDK — Aiden, SRE, and Vault from one client
|
|
5
|
+
Project-URL: Homepage, https://github.com/appcd-dev/stackgen-sdk
|
|
6
|
+
Project-URL: Documentation, https://github.com/appcd-dev/stackgen-sdk/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/appcd-dev/stackgen-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/appcd-dev/stackgen-sdk/issues
|
|
9
|
+
Author: StackGen
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: aiden,openapi,sre,stackgen
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: python-dateutil>=2.8
|
|
15
|
+
Requires-Dist: typing-extensions>=4.0
|
|
16
|
+
Requires-Dist: urllib3>=2.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# StackGen Python SDK
|
|
23
|
+
|
|
24
|
+
Install (PyPI):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install stackgen-sdk==0.1.1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Developers with private repo access:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install "git+ssh://git@github.com/appcd-dev/stackgen-sdk.git@v0.1.1#subdirectory=clients/python"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Quick start:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from stackgen import StackgenClient
|
|
40
|
+
|
|
41
|
+
client = StackgenClient.from_env()
|
|
42
|
+
result = client.aiden.run_webhook_and_download_report(open("alert.json").read())
|
|
43
|
+
print(result.session_id, result.output_path)
|
|
44
|
+
|
|
45
|
+
alerts = client.sre.list_alerts()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Environment:
|
|
49
|
+
|
|
50
|
+
| Variable | Purpose |
|
|
51
|
+
|----------|---------|
|
|
52
|
+
| `STACKGEN_URL` | Mothership URL (no `/guild`) |
|
|
53
|
+
| `STACKGEN_TOKEN` | API token (`stackgen_…`) |
|
|
54
|
+
| `WEBHOOK_TOKEN` | Webhook token (`sg_aios_…`) for triggers |
|
|
55
|
+
| `ORG_ID` | Organization UUID |
|
|
56
|
+
| `WEBHOOK_ID` | Optional; older trigger 202 bodies |
|
|
57
|
+
|
|
58
|
+
See [docs/PRODUCT.md](../../docs/PRODUCT.md), [docs/python/IMPLEMENTATION.md](../../docs/python/IMPLEMENTATION.md), and [docs/PUBLISH.md](../../docs/PUBLISH.md).
|
|
59
|
+
|
|
60
|
+
Generated OpenAPI client code lives under `stackgen.generated` (unstable; prefer `StackgenClient`).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# StackGen Python SDK
|
|
2
|
+
|
|
3
|
+
Install (PyPI):
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install stackgen-sdk==0.1.1
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Developers with private repo access:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install "git+ssh://git@github.com/appcd-dev/stackgen-sdk.git@v0.1.1#subdirectory=clients/python"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Quick start:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from stackgen import StackgenClient
|
|
19
|
+
|
|
20
|
+
client = StackgenClient.from_env()
|
|
21
|
+
result = client.aiden.run_webhook_and_download_report(open("alert.json").read())
|
|
22
|
+
print(result.session_id, result.output_path)
|
|
23
|
+
|
|
24
|
+
alerts = client.sre.list_alerts()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Environment:
|
|
28
|
+
|
|
29
|
+
| Variable | Purpose |
|
|
30
|
+
|----------|---------|
|
|
31
|
+
| `STACKGEN_URL` | Mothership URL (no `/guild`) |
|
|
32
|
+
| `STACKGEN_TOKEN` | API token (`stackgen_…`) |
|
|
33
|
+
| `WEBHOOK_TOKEN` | Webhook token (`sg_aios_…`) for triggers |
|
|
34
|
+
| `ORG_ID` | Organization UUID |
|
|
35
|
+
| `WEBHOOK_ID` | Optional; older trigger 202 bodies |
|
|
36
|
+
|
|
37
|
+
See [docs/PRODUCT.md](../../docs/PRODUCT.md), [docs/python/IMPLEMENTATION.md](../../docs/python/IMPLEMENTATION.md), and [docs/PUBLISH.md](../../docs/PUBLISH.md).
|
|
38
|
+
|
|
39
|
+
Generated OpenAPI client code lives under `stackgen.generated` (unstable; prefer `StackgenClient`).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25", "hatch-vcs>=0.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "stackgen-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "StackGen SDK — Aiden, SRE, and Vault from one client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{ name = "StackGen" }]
|
|
13
|
+
keywords = ["stackgen", "aiden", "sre", "openapi"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"urllib3>=2.0",
|
|
16
|
+
"pydantic>=2.0",
|
|
17
|
+
"python-dateutil>=2.8",
|
|
18
|
+
"typing-extensions>=4.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/appcd-dev/stackgen-sdk"
|
|
23
|
+
Documentation = "https://github.com/appcd-dev/stackgen-sdk/tree/main/docs"
|
|
24
|
+
Repository = "https://github.com/appcd-dev/stackgen-sdk"
|
|
25
|
+
Issues = "https://github.com/appcd-dev/stackgen-sdk/issues"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=8.0", "build>=1.2"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
stackgen-aiden-webhook = "stackgen.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
source = "vcs"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version.raw-options]
|
|
37
|
+
# Repo root is two levels above clients/python/pyproject.toml
|
|
38
|
+
root = "../.."
|
|
39
|
+
# Prefer annotated release tags like v0.1.0 → version 0.1.0
|
|
40
|
+
tag_regex = "^v(?P<version>[0-9].*)$"
|
|
41
|
+
# PyPI rejects local version segments (+gHASH)
|
|
42
|
+
local_scheme = "no-local-version"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.hooks.vcs]
|
|
45
|
+
version-file = "src/stackgen/_version.py"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/stackgen"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = [
|
|
52
|
+
"src/stackgen",
|
|
53
|
+
"README.md",
|
|
54
|
+
"pyproject.toml",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
pythonpath = ["src"]
|
|
60
|
+
markers = [
|
|
61
|
+
"unit: fast isolated unit tests",
|
|
62
|
+
"integration: tests that need network or Docker",
|
|
63
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""StackGen Python SDK.
|
|
2
|
+
|
|
3
|
+
Public entrypoint::
|
|
4
|
+
|
|
5
|
+
from stackgen import StackgenClient, StackgenConfig
|
|
6
|
+
|
|
7
|
+
client = StackgenClient.from_env()
|
|
8
|
+
result = client.aiden.run_webhook_and_download_report(payload)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from stackgen.client import StackgenClient
|
|
14
|
+
from stackgen.config import StackgenConfig
|
|
15
|
+
from stackgen.errors import HttpError, NotAllowlistedError, StackgenError, TimeoutError
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"HttpError",
|
|
19
|
+
"NotAllowlistedError",
|
|
20
|
+
"StackgenClient",
|
|
21
|
+
"StackgenConfig",
|
|
22
|
+
"StackgenError",
|
|
23
|
+
"TimeoutError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_version() -> str:
|
|
28
|
+
try:
|
|
29
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
30
|
+
except ImportError: # pragma: no cover
|
|
31
|
+
from importlib_metadata import PackageNotFoundError, version # type: ignore
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
return version("stackgen-sdk")
|
|
35
|
+
except PackageNotFoundError:
|
|
36
|
+
try:
|
|
37
|
+
return version("stackgen")
|
|
38
|
+
except PackageNotFoundError:
|
|
39
|
+
try:
|
|
40
|
+
from stackgen._version import __version__ as vcs_version
|
|
41
|
+
|
|
42
|
+
return vcs_version
|
|
43
|
+
except ImportError:
|
|
44
|
+
try:
|
|
45
|
+
from stackgen._version import version as vcs_version
|
|
46
|
+
|
|
47
|
+
return vcs_version
|
|
48
|
+
except ImportError:
|
|
49
|
+
return "0.0.0"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__version__ = _resolve_version()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Shared HTTP helpers (stdlib)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import ssl
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from stackgen.errors import HttpError, TimeoutError
|
|
14
|
+
|
|
15
|
+
_RETRYABLE = {429, 502, 503, 504}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def query(org_id: str) -> str:
|
|
19
|
+
if not org_id:
|
|
20
|
+
return ""
|
|
21
|
+
return "?" + urllib.parse.urlencode({"orgId": org_id})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def request(
|
|
25
|
+
url: str,
|
|
26
|
+
token: str,
|
|
27
|
+
*,
|
|
28
|
+
method: str = "GET",
|
|
29
|
+
body: bytes | None = None,
|
|
30
|
+
content_type: str | None = None,
|
|
31
|
+
expect_json: bool = True,
|
|
32
|
+
retries: int = 3,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""Bearer HTTP call with bounded retries on transient GET failures."""
|
|
35
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
36
|
+
if content_type:
|
|
37
|
+
headers["Content-Type"] = content_type
|
|
38
|
+
|
|
39
|
+
attempt = 0
|
|
40
|
+
while True:
|
|
41
|
+
attempt += 1
|
|
42
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(req, context=ssl.create_default_context()) as resp:
|
|
45
|
+
raw = resp.read()
|
|
46
|
+
if not expect_json:
|
|
47
|
+
return raw
|
|
48
|
+
if not raw:
|
|
49
|
+
return {}
|
|
50
|
+
return json.loads(raw.decode("utf-8"))
|
|
51
|
+
except urllib.error.HTTPError as err:
|
|
52
|
+
detail = err.read().decode("utf-8", errors="replace")
|
|
53
|
+
if method == "GET" and err.code in _RETRYABLE and attempt < retries:
|
|
54
|
+
time.sleep(min(2**attempt, 8))
|
|
55
|
+
continue
|
|
56
|
+
raise HttpError(
|
|
57
|
+
f"{method} {url} -> HTTP {err.code}: {detail}",
|
|
58
|
+
status_code=err.code,
|
|
59
|
+
) from err
|
|
60
|
+
except urllib.error.URLError as err:
|
|
61
|
+
if method == "GET" and attempt < retries:
|
|
62
|
+
time.sleep(min(2**attempt, 8))
|
|
63
|
+
continue
|
|
64
|
+
raise HttpError(f"{method} {url} -> {err}") from err
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def as_list(payload: Any) -> list[dict[str, Any]]:
|
|
68
|
+
if isinstance(payload, list):
|
|
69
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
70
|
+
if isinstance(payload, dict):
|
|
71
|
+
items = payload.get("items")
|
|
72
|
+
if isinstance(items, list):
|
|
73
|
+
return [item for item in items if isinstance(item, dict)]
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def text(value: Any) -> str:
|
|
78
|
+
if value is None:
|
|
79
|
+
return ""
|
|
80
|
+
return str(value).strip()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def poll_until(
|
|
84
|
+
deadline: float,
|
|
85
|
+
interval: float,
|
|
86
|
+
tick: Callable[[], Any | None],
|
|
87
|
+
*,
|
|
88
|
+
label: str,
|
|
89
|
+
timeout_seconds: int,
|
|
90
|
+
) -> Any:
|
|
91
|
+
while time.monotonic() < deadline:
|
|
92
|
+
result = tick()
|
|
93
|
+
if result is not None:
|
|
94
|
+
return result
|
|
95
|
+
time.sleep(interval)
|
|
96
|
+
raise TimeoutError(label, timeout_seconds)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Aiden namespace — public methods for platform APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from stackgen.aiden.webhook import WebhookReportJourney, WebhookReportResult
|
|
6
|
+
from stackgen.config import StackgenConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AidenNamespace:
|
|
10
|
+
"""Aiden platform APIs (sessions, webhooks, artifacts)."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: StackgenConfig) -> None:
|
|
13
|
+
self._config = config
|
|
14
|
+
self._webhook = WebhookReportJourney(config)
|
|
15
|
+
|
|
16
|
+
def run_webhook_and_download_report(
|
|
17
|
+
self,
|
|
18
|
+
payload: str,
|
|
19
|
+
*,
|
|
20
|
+
artifact_name: str | None = None,
|
|
21
|
+
output_path: str | None = None,
|
|
22
|
+
) -> WebhookReportResult:
|
|
23
|
+
"""Trigger a webhook, wait for the session artifact, download it."""
|
|
24
|
+
return self._webhook.run(
|
|
25
|
+
payload, artifact_name=artifact_name, output_path=output_path
|
|
26
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Pure helpers for webhook run selection (unit-testable without HTTP)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from stackgen import _http
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pick_run(
|
|
11
|
+
runs: list[dict[str, Any]],
|
|
12
|
+
dispatch_run_id: str,
|
|
13
|
+
known_run_ids: frozenset[str],
|
|
14
|
+
) -> dict[str, Any] | None:
|
|
15
|
+
"""Choose the webhook run that corresponds to a new trigger.
|
|
16
|
+
|
|
17
|
+
Preference order:
|
|
18
|
+
1. Exact match on ``id`` / ``run_id`` to ``dispatch_run_id``
|
|
19
|
+
2. First run whose id is not in ``known_run_ids`` (new since pre-trigger snapshot)
|
|
20
|
+
3. If no known ids and runs exist, the first run (best-effort on older motherships)
|
|
21
|
+
"""
|
|
22
|
+
if dispatch_run_id:
|
|
23
|
+
for item in runs:
|
|
24
|
+
if dispatch_run_id in {
|
|
25
|
+
_http.text(item.get("id")),
|
|
26
|
+
_http.text(item.get("run_id")),
|
|
27
|
+
}:
|
|
28
|
+
return item
|
|
29
|
+
for item in runs:
|
|
30
|
+
if _http.text(item.get("id")) not in known_run_ids:
|
|
31
|
+
return item
|
|
32
|
+
if not runs:
|
|
33
|
+
return None
|
|
34
|
+
if not known_run_ids:
|
|
35
|
+
return runs[0]
|
|
36
|
+
return None
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Aiden webhook → session-report journey."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from stackgen import _http
|
|
13
|
+
from stackgen.aiden.runs import pick_run
|
|
14
|
+
from stackgen.config import StackgenConfig
|
|
15
|
+
from stackgen.errors import StackgenError, TimeoutError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class WebhookReportResult:
|
|
20
|
+
"""Result of trigger → wait → download session-report."""
|
|
21
|
+
|
|
22
|
+
webhook_id: str
|
|
23
|
+
invocation_id: str
|
|
24
|
+
session_id: str
|
|
25
|
+
artifacts: tuple[dict[str, Any], ...]
|
|
26
|
+
output_path: Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WebhookReportJourney:
|
|
30
|
+
"""Trigger a webhook, wait for the session artifact, and download it.
|
|
31
|
+
|
|
32
|
+
Uses ``webhook_token`` for POST trigger and ``api_token`` for poll/download.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: StackgenConfig) -> None:
|
|
36
|
+
self._config = config
|
|
37
|
+
|
|
38
|
+
def run(
|
|
39
|
+
self,
|
|
40
|
+
payload: str,
|
|
41
|
+
*,
|
|
42
|
+
artifact_name: str | None = None,
|
|
43
|
+
output_path: str | None = None,
|
|
44
|
+
) -> WebhookReportResult:
|
|
45
|
+
cfg = self._config
|
|
46
|
+
if not cfg.webhook_token:
|
|
47
|
+
raise ValueError("webhook_token is required for run_webhook_and_download_report")
|
|
48
|
+
|
|
49
|
+
artifact = artifact_name or cfg.artifact_name
|
|
50
|
+
dest = output_path or cfg.output_path
|
|
51
|
+
|
|
52
|
+
known_run_ids: frozenset[str] = frozenset()
|
|
53
|
+
if cfg.webhook_id:
|
|
54
|
+
known_run_ids = frozenset(
|
|
55
|
+
_http.text(item.get("id"))
|
|
56
|
+
for item in self._list_runs(cfg.webhook_id)
|
|
57
|
+
if _http.text(item.get("id"))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
trigger = self._trigger(payload)
|
|
61
|
+
webhook_id = _http.text(trigger.get("webhook_id")) or _http.text(cfg.webhook_id)
|
|
62
|
+
invocation_id = _http.text(trigger.get("invocation_id"))
|
|
63
|
+
session_id = _http.text(trigger.get("session_id"))
|
|
64
|
+
|
|
65
|
+
if not session_id and not webhook_id:
|
|
66
|
+
raise StackgenError(
|
|
67
|
+
"trigger 202 did not include session_id or webhook_id; "
|
|
68
|
+
"set webhook_id for older motherships. "
|
|
69
|
+
f"body={json.dumps(trigger)}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if not session_id:
|
|
73
|
+
session_id = self._wait_for_session(
|
|
74
|
+
webhook_id,
|
|
75
|
+
invocation_id,
|
|
76
|
+
_http.text(trigger.get("run_id")),
|
|
77
|
+
known_run_ids,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
artifacts = self._wait_for_artifact(session_id, artifact)
|
|
81
|
+
path = self._download_artifact(session_id, artifact, dest)
|
|
82
|
+
return WebhookReportResult(
|
|
83
|
+
webhook_id=webhook_id,
|
|
84
|
+
invocation_id=invocation_id,
|
|
85
|
+
session_id=session_id,
|
|
86
|
+
artifacts=tuple(artifacts),
|
|
87
|
+
output_path=path,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _trigger(self, payload: str) -> dict[str, Any]:
|
|
91
|
+
url = f"{self._config.aiden_url()}/api/v1/webhooks/trigger{_http.query(self._config.org_id)}"
|
|
92
|
+
data = _http.request(
|
|
93
|
+
url,
|
|
94
|
+
self._config.webhook_token,
|
|
95
|
+
method="POST",
|
|
96
|
+
body=payload.encode("utf-8"),
|
|
97
|
+
content_type="text/plain",
|
|
98
|
+
)
|
|
99
|
+
if not isinstance(data, dict):
|
|
100
|
+
raise StackgenError(f"unexpected trigger payload: {data!r}")
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
def _list_runs(self, webhook_id: str) -> list[dict[str, Any]]:
|
|
104
|
+
url = (
|
|
105
|
+
f"{self._config.aiden_url()}/api/v1/webhooks/{webhook_id}/runs"
|
|
106
|
+
f"{_http.query(self._config.org_id)}"
|
|
107
|
+
)
|
|
108
|
+
return _http.as_list(_http.request(url, self._config.api_token))
|
|
109
|
+
|
|
110
|
+
def _get_run(self, webhook_id: str, run_id: str) -> dict[str, Any]:
|
|
111
|
+
url = (
|
|
112
|
+
f"{self._config.aiden_url()}/api/v1/webhooks/{webhook_id}/runs/{run_id}"
|
|
113
|
+
f"{_http.query(self._config.org_id)}"
|
|
114
|
+
)
|
|
115
|
+
data = _http.request(url, self._config.api_token)
|
|
116
|
+
if not isinstance(data, dict):
|
|
117
|
+
raise StackgenError(f"unexpected run payload: {data!r}")
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
def _wait_for_session(
|
|
121
|
+
self,
|
|
122
|
+
webhook_id: str,
|
|
123
|
+
invocation_id: str,
|
|
124
|
+
dispatch_run_id: str,
|
|
125
|
+
known_run_ids: frozenset[str],
|
|
126
|
+
) -> str:
|
|
127
|
+
cfg = self._config
|
|
128
|
+
deadline = time.monotonic() + cfg.timeout_seconds
|
|
129
|
+
run_id_hint = invocation_id or dispatch_run_id
|
|
130
|
+
while time.monotonic() < deadline:
|
|
131
|
+
run: dict[str, Any] | None = None
|
|
132
|
+
if run_id_hint:
|
|
133
|
+
try:
|
|
134
|
+
run = self._get_run(webhook_id, run_id_hint)
|
|
135
|
+
except StackgenError:
|
|
136
|
+
run = None
|
|
137
|
+
if run is None:
|
|
138
|
+
run = pick_run(self._list_runs(webhook_id), dispatch_run_id, known_run_ids)
|
|
139
|
+
if run is None:
|
|
140
|
+
time.sleep(cfg.poll_interval_seconds)
|
|
141
|
+
continue
|
|
142
|
+
if not run_id_hint:
|
|
143
|
+
run_id_hint = _http.text(run.get("id")) or _http.text(run.get("run_id"))
|
|
144
|
+
status = _http.text(run.get("status")).casefold()
|
|
145
|
+
session_id = _http.text(run.get("session_id"))
|
|
146
|
+
if status in {"failed", "skipped"}:
|
|
147
|
+
raise StackgenError(
|
|
148
|
+
f"webhook invocation ended with status {status}: "
|
|
149
|
+
f"{run.get('error') or 'no error detail'}"
|
|
150
|
+
)
|
|
151
|
+
if session_id:
|
|
152
|
+
return session_id
|
|
153
|
+
time.sleep(cfg.poll_interval_seconds)
|
|
154
|
+
raise TimeoutError("webhook session", cfg.timeout_seconds)
|
|
155
|
+
|
|
156
|
+
def _wait_for_artifact(
|
|
157
|
+
self, session_id: str, artifact_name: str
|
|
158
|
+
) -> list[dict[str, Any]]:
|
|
159
|
+
cfg = self._config
|
|
160
|
+
deadline = time.monotonic() + cfg.timeout_seconds
|
|
161
|
+
url = (
|
|
162
|
+
f"{cfg.aiden_url()}/api/v1/sessions/{session_id}/artifacts"
|
|
163
|
+
f"{_http.query(cfg.org_id)}"
|
|
164
|
+
)
|
|
165
|
+
want = artifact_name.casefold()
|
|
166
|
+
artifacts: list[dict[str, Any]] = []
|
|
167
|
+
while time.monotonic() < deadline:
|
|
168
|
+
artifacts = _http.as_list(_http.request(url, cfg.api_token))
|
|
169
|
+
if any(_http.text(item.get("name")).casefold() == want for item in artifacts):
|
|
170
|
+
return artifacts
|
|
171
|
+
time.sleep(cfg.poll_interval_seconds)
|
|
172
|
+
raise TimeoutError(artifact_name, cfg.timeout_seconds)
|
|
173
|
+
|
|
174
|
+
def _download_artifact(
|
|
175
|
+
self, session_id: str, artifact_name: str, output_path: str
|
|
176
|
+
) -> Path:
|
|
177
|
+
encoded = urllib.parse.quote(artifact_name, safe="")
|
|
178
|
+
url = (
|
|
179
|
+
f"{self._config.aiden_url()}/api/v1/sessions/{session_id}/artifacts/"
|
|
180
|
+
f"{encoded}/download{_http.query(self._config.org_id)}"
|
|
181
|
+
)
|
|
182
|
+
raw = _http.request(url, self._config.api_token, expect_json=False)
|
|
183
|
+
dest = Path(output_path)
|
|
184
|
+
dest.write_bytes(raw)
|
|
185
|
+
return dest
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CLI: trigger webhook and download session-report.md."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from stackgen.client import StackgenClient
|
|
9
|
+
from stackgen.config import StackgenConfig
|
|
10
|
+
from stackgen.errors import StackgenError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> None:
|
|
14
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
15
|
+
payload = "{}"
|
|
16
|
+
if args:
|
|
17
|
+
path = Path(args[0])
|
|
18
|
+
if not path.is_file():
|
|
19
|
+
print(f"error: payload file does not exist: {path}", file=sys.stderr)
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
payload = path.read_text(encoding="utf-8")
|
|
22
|
+
try:
|
|
23
|
+
client = StackgenClient(StackgenConfig.from_env())
|
|
24
|
+
result = client.aiden.run_webhook_and_download_report(payload)
|
|
25
|
+
except (StackgenError, ValueError) as err:
|
|
26
|
+
print(f"error: {err}", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
print(f"session_id={result.session_id}")
|
|
29
|
+
print(f"output_path={result.output_path}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|