barbara-api-sdk 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.
- barbara/__init__.py +28 -0
- barbara/_http.py +42 -0
- barbara/auth.py +105 -0
- barbara/client.py +135 -0
- barbara/config.py +57 -0
- barbara/exceptions.py +26 -0
- barbara/models.py +211 -0
- barbara/py.typed +0 -0
- barbara/resources/__init__.py +0 -0
- barbara/resources/alerts.py +74 -0
- barbara/resources/appconfig.py +124 -0
- barbara/resources/applications.py +230 -0
- barbara/resources/clusters.py +231 -0
- barbara/resources/devices.py +303 -0
- barbara/resources/groups.py +115 -0
- barbara/resources/models.py +214 -0
- barbara/resources/stacks.py +403 -0
- barbara/resources/users.py +53 -0
- barbara/resources/workloads.py +535 -0
- barbara/utils.py +86 -0
- barbara_api_sdk-0.1.0.dist-info/METADATA +335 -0
- barbara_api_sdk-0.1.0.dist-info/RECORD +24 -0
- barbara_api_sdk-0.1.0.dist-info/WHEEL +4 -0
- barbara_api_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
barbara/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Python SDK for the Barbara Edge AI API."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from .client import AsyncBarbaraClient, BarbaraClient
|
|
6
|
+
from .config import BarbaraConfig
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
BarbaraApiError,
|
|
9
|
+
BarbaraAuthError,
|
|
10
|
+
BarbaraNotFoundError,
|
|
11
|
+
BarbaraPermissionError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version("barbara-api-sdk")
|
|
16
|
+
except PackageNotFoundError: # running from a source checkout, not installed
|
|
17
|
+
__version__ = "0.0.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AsyncBarbaraClient",
|
|
21
|
+
"BarbaraApiError",
|
|
22
|
+
"BarbaraAuthError",
|
|
23
|
+
"BarbaraClient",
|
|
24
|
+
"BarbaraConfig",
|
|
25
|
+
"BarbaraNotFoundError",
|
|
26
|
+
"BarbaraPermissionError",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|
barbara/_http.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Internal request/response plumbing shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
Not part of the public API — resources call ``client.request(...)``, not
|
|
4
|
+
this module directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .exceptions import BarbaraApiError, BarbaraNotFoundError, BarbaraPermissionError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
17
|
+
if response.status_code < 400:
|
|
18
|
+
return
|
|
19
|
+
try:
|
|
20
|
+
body: Any = response.json()
|
|
21
|
+
except ValueError:
|
|
22
|
+
body = response.text
|
|
23
|
+
if response.status_code == 404:
|
|
24
|
+
raise BarbaraNotFoundError(f"Not found: {response.url}", status=404, body=body)
|
|
25
|
+
if response.status_code in (401, 403):
|
|
26
|
+
raise BarbaraPermissionError(
|
|
27
|
+
f"Permission denied ({response.status_code}): {response.url}",
|
|
28
|
+
status=response.status_code,
|
|
29
|
+
body=body,
|
|
30
|
+
)
|
|
31
|
+
raise BarbaraApiError(
|
|
32
|
+
f"Barbara API error {response.status_code}: {response.url}",
|
|
33
|
+
status=response.status_code,
|
|
34
|
+
body=body,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_headers(token: str, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
|
|
39
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
40
|
+
if extra:
|
|
41
|
+
headers.update(extra)
|
|
42
|
+
return headers
|
barbara/auth.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""OAuth2 password-grant authentication against Barbara's Keycloak realm."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .config import BarbaraConfig
|
|
12
|
+
from .exceptions import BarbaraAuthError
|
|
13
|
+
|
|
14
|
+
# Refresh a bit before actual expiry to avoid racing a 401 on a nearly-dead token.
|
|
15
|
+
EXPIRY_SAFETY_MARGIN_SECONDS = 15
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Token:
|
|
20
|
+
access_token: str
|
|
21
|
+
expires_at: float
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_expired(self) -> bool:
|
|
25
|
+
return time.monotonic() >= self.expires_at
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _token_request_body(config: BarbaraConfig) -> dict[str, str]:
|
|
29
|
+
body = {
|
|
30
|
+
"grant_type": "password",
|
|
31
|
+
"client_id": config.client_id,
|
|
32
|
+
"username": config.username,
|
|
33
|
+
"password": config.password,
|
|
34
|
+
}
|
|
35
|
+
if config.client_secret:
|
|
36
|
+
body["client_secret"] = config.client_secret
|
|
37
|
+
return body
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_token_response(data: dict[str, Any]) -> Token:
|
|
41
|
+
try:
|
|
42
|
+
expires_in = float(data["expires_in"])
|
|
43
|
+
access_token = data["access_token"]
|
|
44
|
+
except KeyError as exc:
|
|
45
|
+
raise BarbaraAuthError(f"Unexpected token response, missing {exc}", body=data) from exc
|
|
46
|
+
return Token(
|
|
47
|
+
access_token=access_token,
|
|
48
|
+
expires_at=time.monotonic() + expires_in - EXPIRY_SAFETY_MARGIN_SECONDS,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TokenManager:
|
|
53
|
+
"""Sync token fetch/refresh. Not thread-safe by design (kept simple)."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: BarbaraConfig, http_client: httpx.Client):
|
|
56
|
+
self._config = config
|
|
57
|
+
self._http = http_client
|
|
58
|
+
self._token: Optional[Token] = None
|
|
59
|
+
|
|
60
|
+
def get_token(self, *, force_refresh: bool = False) -> str:
|
|
61
|
+
if force_refresh or self._token is None or self._token.is_expired:
|
|
62
|
+
self._token = self._fetch()
|
|
63
|
+
return self._token.access_token
|
|
64
|
+
|
|
65
|
+
def _fetch(self) -> Token:
|
|
66
|
+
response = self._http.post(
|
|
67
|
+
self._config.token_url,
|
|
68
|
+
data=_token_request_body(self._config),
|
|
69
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
70
|
+
)
|
|
71
|
+
if response.status_code >= 400:
|
|
72
|
+
raise BarbaraAuthError(
|
|
73
|
+
f"Authentication failed ({response.status_code})",
|
|
74
|
+
status=response.status_code,
|
|
75
|
+
body=response.text,
|
|
76
|
+
)
|
|
77
|
+
return _parse_token_response(response.json())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AsyncTokenManager:
|
|
81
|
+
"""Async counterpart of :class:`TokenManager`."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: BarbaraConfig, http_client: httpx.AsyncClient):
|
|
84
|
+
self._config = config
|
|
85
|
+
self._http = http_client
|
|
86
|
+
self._token: Optional[Token] = None
|
|
87
|
+
|
|
88
|
+
async def get_token(self, *, force_refresh: bool = False) -> str:
|
|
89
|
+
if force_refresh or self._token is None or self._token.is_expired:
|
|
90
|
+
self._token = await self._fetch()
|
|
91
|
+
return self._token.access_token
|
|
92
|
+
|
|
93
|
+
async def _fetch(self) -> Token:
|
|
94
|
+
response = await self._http.post(
|
|
95
|
+
self._config.token_url,
|
|
96
|
+
data=_token_request_body(self._config),
|
|
97
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
98
|
+
)
|
|
99
|
+
if response.status_code >= 400:
|
|
100
|
+
raise BarbaraAuthError(
|
|
101
|
+
f"Authentication failed ({response.status_code})",
|
|
102
|
+
status=response.status_code,
|
|
103
|
+
body=response.text,
|
|
104
|
+
)
|
|
105
|
+
return _parse_token_response(response.json())
|
barbara/client.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Public entry points: :class:`BarbaraClient` and :class:`AsyncBarbaraClient`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any, Optional, Type
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ._http import build_headers, raise_for_status
|
|
11
|
+
from .auth import AsyncTokenManager, TokenManager
|
|
12
|
+
from .config import BarbaraConfig
|
|
13
|
+
from .resources.alerts import AlertsResource, AsyncAlertsResource
|
|
14
|
+
from .resources.appconfig import AppConfigResource, AsyncAppConfigResource
|
|
15
|
+
from .resources.applications import ApplicationsResource, AsyncApplicationsResource
|
|
16
|
+
from .resources.clusters import AsyncClustersResource, ClustersResource
|
|
17
|
+
from .resources.devices import AsyncDevicesResource, DevicesResource
|
|
18
|
+
from .resources.groups import AsyncGroupsResource, GroupsResource
|
|
19
|
+
from .resources.models import AsyncModelsResource, ModelsResource
|
|
20
|
+
from .resources.users import AsyncUsersResource, UsersResource
|
|
21
|
+
from .utils import unwrap
|
|
22
|
+
|
|
23
|
+
DEFAULT_TIMEOUT = 30.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BarbaraClient:
|
|
27
|
+
"""Synchronous client. Use as a context manager to close the underlying session.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
with BarbaraClient.from_env() as client:
|
|
31
|
+
devices = client.devices.list()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: BarbaraConfig, *, timeout: float = DEFAULT_TIMEOUT):
|
|
35
|
+
self.config = config
|
|
36
|
+
self._http = httpx.Client(
|
|
37
|
+
base_url=config.api_url, timeout=timeout, verify=config.verify_tls
|
|
38
|
+
)
|
|
39
|
+
self._auth_http = httpx.Client(timeout=timeout, verify=config.verify_tls)
|
|
40
|
+
self._tokens = TokenManager(config, self._auth_http)
|
|
41
|
+
|
|
42
|
+
self.devices = DevicesResource(self)
|
|
43
|
+
self.clusters = ClustersResource(self)
|
|
44
|
+
self.applications = ApplicationsResource(self)
|
|
45
|
+
self.models = ModelsResource(self)
|
|
46
|
+
self.appconfig = AppConfigResource(self)
|
|
47
|
+
self.groups = GroupsResource(self)
|
|
48
|
+
self.users = UsersResource(self)
|
|
49
|
+
self.alerts = AlertsResource(self)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_env(cls, **kwargs: Any) -> BarbaraClient:
|
|
53
|
+
return cls(BarbaraConfig.from_env(), **kwargs)
|
|
54
|
+
|
|
55
|
+
def request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
56
|
+
"""Issue an authenticated request, retrying once on a 401 with a fresh token."""
|
|
57
|
+
token = self._tokens.get_token()
|
|
58
|
+
response = self._http.request(method, path, headers=build_headers(token), **kwargs)
|
|
59
|
+
if response.status_code == 401:
|
|
60
|
+
token = self._tokens.get_token(force_refresh=True)
|
|
61
|
+
response = self._http.request(method, path, headers=build_headers(token), **kwargs)
|
|
62
|
+
raise_for_status(response)
|
|
63
|
+
return response.json() if response.content else None
|
|
64
|
+
|
|
65
|
+
def api_version(self) -> Any:
|
|
66
|
+
return unwrap(self.request("GET", "/api/v1/apiversion"))
|
|
67
|
+
|
|
68
|
+
def close(self) -> None:
|
|
69
|
+
self._http.close()
|
|
70
|
+
self._auth_http.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> BarbaraClient:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(
|
|
76
|
+
self,
|
|
77
|
+
exc_type: Optional[Type[BaseException]],
|
|
78
|
+
exc: Optional[BaseException],
|
|
79
|
+
tb: Optional[TracebackType],
|
|
80
|
+
) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AsyncBarbaraClient:
|
|
85
|
+
"""Async counterpart of :class:`BarbaraClient`, same resource surface."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, config: BarbaraConfig, *, timeout: float = DEFAULT_TIMEOUT):
|
|
88
|
+
self.config = config
|
|
89
|
+
self._http = httpx.AsyncClient(
|
|
90
|
+
base_url=config.api_url, timeout=timeout, verify=config.verify_tls
|
|
91
|
+
)
|
|
92
|
+
self._auth_http = httpx.AsyncClient(timeout=timeout, verify=config.verify_tls)
|
|
93
|
+
self._tokens = AsyncTokenManager(config, self._auth_http)
|
|
94
|
+
|
|
95
|
+
self.devices = AsyncDevicesResource(self)
|
|
96
|
+
self.clusters = AsyncClustersResource(self)
|
|
97
|
+
self.applications = AsyncApplicationsResource(self)
|
|
98
|
+
self.models = AsyncModelsResource(self)
|
|
99
|
+
self.appconfig = AsyncAppConfigResource(self)
|
|
100
|
+
self.groups = AsyncGroupsResource(self)
|
|
101
|
+
self.users = AsyncUsersResource(self)
|
|
102
|
+
self.alerts = AsyncAlertsResource(self)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_env(cls, **kwargs: Any) -> AsyncBarbaraClient:
|
|
106
|
+
return cls(BarbaraConfig.from_env(), **kwargs)
|
|
107
|
+
|
|
108
|
+
async def request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
109
|
+
token = await self._tokens.get_token()
|
|
110
|
+
response = await self._http.request(method, path, headers=build_headers(token), **kwargs)
|
|
111
|
+
if response.status_code == 401:
|
|
112
|
+
token = await self._tokens.get_token(force_refresh=True)
|
|
113
|
+
response = await self._http.request(
|
|
114
|
+
method, path, headers=build_headers(token), **kwargs
|
|
115
|
+
)
|
|
116
|
+
raise_for_status(response)
|
|
117
|
+
return response.json() if response.content else None
|
|
118
|
+
|
|
119
|
+
async def api_version(self) -> Any:
|
|
120
|
+
return unwrap(await self.request("GET", "/api/v1/apiversion"))
|
|
121
|
+
|
|
122
|
+
async def close(self) -> None:
|
|
123
|
+
await self._http.aclose()
|
|
124
|
+
await self._auth_http.aclose()
|
|
125
|
+
|
|
126
|
+
async def __aenter__(self) -> AsyncBarbaraClient:
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
async def __aexit__(
|
|
130
|
+
self,
|
|
131
|
+
exc_type: Optional[Type[BaseException]],
|
|
132
|
+
exc: Optional[BaseException],
|
|
133
|
+
tb: Optional[TracebackType],
|
|
134
|
+
) -> None:
|
|
135
|
+
await self.close()
|
barbara/config.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Configuration and credential loading.
|
|
2
|
+
|
|
3
|
+
Credentials follow Barbara's "Barbara API Credentials" convention: four
|
|
4
|
+
values prefixed ``BBR_API_`` (client id/secret + username/password). Other
|
|
5
|
+
config (API/auth URLs, realm) uses the plain ``BBR_`` prefix.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_API_URL = "https://prod.bap.barbara.tech"
|
|
15
|
+
DEFAULT_AUTH_URL = "https://prod.auth.barbara.tech/auth"
|
|
16
|
+
DEFAULT_REALM = "bbr_prod"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class BarbaraConfig:
|
|
21
|
+
client_id: str
|
|
22
|
+
client_secret: Optional[str]
|
|
23
|
+
username: str
|
|
24
|
+
password: str
|
|
25
|
+
api_url: str = DEFAULT_API_URL
|
|
26
|
+
auth_url: str = DEFAULT_AUTH_URL
|
|
27
|
+
realm: str = DEFAULT_REALM
|
|
28
|
+
verify_tls: bool = True
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def token_url(self) -> str:
|
|
32
|
+
return f"{self.auth_url.rstrip('/')}/realms/{self.realm}/protocol/openid-connect/token"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_env(cls) -> BarbaraConfig:
|
|
36
|
+
"""Build config from environment variables, raising on missing credentials."""
|
|
37
|
+
missing = [
|
|
38
|
+
name
|
|
39
|
+
for name in ("BBR_API_CLIENT_ID", "BBR_API_USERNAME", "BBR_API_PASSWORD")
|
|
40
|
+
if not os.environ.get(name)
|
|
41
|
+
]
|
|
42
|
+
if missing:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Missing required environment variable(s): "
|
|
45
|
+
+ ", ".join(missing)
|
|
46
|
+
+ ". See README for the Barbara API Credentials setup."
|
|
47
|
+
)
|
|
48
|
+
return cls(
|
|
49
|
+
client_id=os.environ["BBR_API_CLIENT_ID"],
|
|
50
|
+
client_secret=os.environ.get("BBR_API_CLIENT_SECRET"),
|
|
51
|
+
username=os.environ["BBR_API_USERNAME"],
|
|
52
|
+
password=os.environ["BBR_API_PASSWORD"],
|
|
53
|
+
api_url=os.environ.get("BBR_API_URL", DEFAULT_API_URL),
|
|
54
|
+
auth_url=os.environ.get("BBR_AUTH_URL", DEFAULT_AUTH_URL),
|
|
55
|
+
realm=os.environ.get("BBR_REALM", DEFAULT_REALM),
|
|
56
|
+
verify_tls=os.environ.get("BBR_VERIFY_TLS", "true").lower() != "false",
|
|
57
|
+
)
|
barbara/exceptions.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exceptions raised by the Barbara SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BarbaraApiError(RuntimeError):
|
|
9
|
+
"""Base error for any non-2xx response from the Barbara API."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, *, status: Optional[int] = None, body: Any = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status = status
|
|
14
|
+
self.body = body
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BarbaraAuthError(BarbaraApiError):
|
|
18
|
+
"""Raised when authentication (token request or refresh) fails."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BarbaraNotFoundError(BarbaraApiError):
|
|
22
|
+
"""Raised on a 404, or when a lookup by name (e.g. deviceName) has no match."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BarbaraPermissionError(BarbaraApiError):
|
|
26
|
+
"""Raised on a 401/403 for a required (non-optional) resource."""
|
barbara/models.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Lightweight dataclasses for common Barbara API entities.
|
|
2
|
+
|
|
3
|
+
These are intentionally partial: only fields the SDK actively relies on are
|
|
4
|
+
declared. Unknown fields returned by the API are kept in ``.raw`` so nothing
|
|
5
|
+
is ever silently dropped.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Device:
|
|
16
|
+
id: str
|
|
17
|
+
device_name: str
|
|
18
|
+
status: Optional[str] = None
|
|
19
|
+
cluster_id: Optional[str] = None
|
|
20
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_api(cls, data: Dict[str, Any]) -> Device:
|
|
24
|
+
return cls(
|
|
25
|
+
id=data["_id"],
|
|
26
|
+
device_name=data.get("deviceName", ""),
|
|
27
|
+
status=data.get("status"),
|
|
28
|
+
cluster_id=data.get("clusterId"),
|
|
29
|
+
raw=data,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Cluster:
|
|
35
|
+
id: str
|
|
36
|
+
name: str
|
|
37
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_api(cls, data: Dict[str, Any]) -> Cluster:
|
|
41
|
+
return cls(id=data["_id"], name=data.get("name", ""), raw=data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Application:
|
|
46
|
+
id: str
|
|
47
|
+
name: str
|
|
48
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_api(cls, data: Dict[str, Any]) -> Application:
|
|
52
|
+
return cls(id=data["_id"], name=data.get("name", ""), raw=data)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class MLModel:
|
|
57
|
+
id: str
|
|
58
|
+
name: str
|
|
59
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_api(cls, data: Dict[str, Any]) -> MLModel:
|
|
63
|
+
return cls(id=data["_id"], name=data.get("name", ""), raw=data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppConfig:
|
|
68
|
+
id: str
|
|
69
|
+
name: str
|
|
70
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_api(cls, data: Dict[str, Any]) -> AppConfig:
|
|
74
|
+
return cls(id=data.get("_id", ""), name=data.get("name", ""), raw=data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Group:
|
|
79
|
+
id: str
|
|
80
|
+
name: str
|
|
81
|
+
device_ids: List[str] = field(default_factory=list)
|
|
82
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_api(cls, data: Dict[str, Any]) -> Group:
|
|
86
|
+
return cls(
|
|
87
|
+
id=data.get("_id", ""),
|
|
88
|
+
name=data.get("name", ""),
|
|
89
|
+
device_ids=data.get("devices", []),
|
|
90
|
+
raw=data,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class User:
|
|
96
|
+
id: str
|
|
97
|
+
email: str
|
|
98
|
+
main_role: Optional[str] = None
|
|
99
|
+
enabled: Optional[bool] = None
|
|
100
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_api(cls, data: Dict[str, Any]) -> User:
|
|
104
|
+
return cls(
|
|
105
|
+
id=data["_id"],
|
|
106
|
+
email=data.get("email", ""),
|
|
107
|
+
main_role=data.get("main_role"),
|
|
108
|
+
enabled=data.get("enabled"),
|
|
109
|
+
raw=data,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class Alert:
|
|
115
|
+
id: str
|
|
116
|
+
title: Optional[str]
|
|
117
|
+
severity: Optional[str]
|
|
118
|
+
active: Optional[bool]
|
|
119
|
+
device_id: Optional[str] = None
|
|
120
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_api(cls, data: Dict[str, Any]) -> Alert:
|
|
124
|
+
return cls(
|
|
125
|
+
id=data["_id"],
|
|
126
|
+
title=data.get("title"),
|
|
127
|
+
severity=data.get("severity"),
|
|
128
|
+
active=data.get("active"),
|
|
129
|
+
device_id=data.get("deviceId"),
|
|
130
|
+
raw=data,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class Workload:
|
|
136
|
+
"""A device workload (``deviceSpace``). Unlike most entities, the API
|
|
137
|
+
doesn't return its own top-level id — it's the ``workloadId``/``spaceId``
|
|
138
|
+
the caller already knows, kept here for symmetry with the other
|
|
139
|
+
dataclasses.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
id: str
|
|
143
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_api(cls, workload_id: str, data: Dict[str, Any]) -> Workload:
|
|
147
|
+
return cls(id=workload_id, raw=data)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Secret:
|
|
152
|
+
"""A device or cluster secret reference (name only — value is never
|
|
153
|
+
returned unencrypted unless the caller has permission and asks for it).
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
id: str
|
|
157
|
+
name: str
|
|
158
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def from_api(cls, data: Dict[str, Any]) -> Secret:
|
|
162
|
+
return cls(id=data.get("_id", ""), name=data.get("name", ""), raw=data)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class DockerCredential:
|
|
167
|
+
id: str
|
|
168
|
+
server: Optional[str] = None
|
|
169
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_api(cls, data: Dict[str, Any]) -> DockerCredential:
|
|
173
|
+
return cls(id=data.get("_id", ""), server=data.get("server"), raw=data)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class ModelVersion:
|
|
178
|
+
id: str
|
|
179
|
+
name: str
|
|
180
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_api(cls, data: Dict[str, Any]) -> ModelVersion:
|
|
184
|
+
return cls(id=data.get("_id", ""), name=data.get("name", ""), raw=data)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class AlertEvent:
|
|
189
|
+
id: str
|
|
190
|
+
event_type: Optional[str]
|
|
191
|
+
device_id: Optional[str] = None
|
|
192
|
+
alert_id: Optional[str] = None
|
|
193
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_api(cls, data: Dict[str, Any]) -> AlertEvent:
|
|
197
|
+
return cls(
|
|
198
|
+
id=data["_id"],
|
|
199
|
+
event_type=data.get("eventType"),
|
|
200
|
+
device_id=data.get("deviceId"),
|
|
201
|
+
alert_id=data.get("alertId"),
|
|
202
|
+
raw=data,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class Page:
|
|
208
|
+
"""Generic page wrapper for list endpoints."""
|
|
209
|
+
|
|
210
|
+
items: List[Any]
|
|
211
|
+
total: Optional[int] = None
|
barbara/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Alerts resource (``/v1/alerts`` and ``/v1/events``). Same pattern as :mod:`devices`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..models import Alert, AlertEvent
|
|
8
|
+
from ..utils import unwrap
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ..client import AsyncBarbaraClient, BarbaraClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _event_params(*, offset: int, size: int, device_id: Optional[str]) -> Dict[str, Any]:
|
|
15
|
+
params: Dict[str, Any] = {"from": offset, "size": size}
|
|
16
|
+
if device_id is not None:
|
|
17
|
+
params["deviceId"] = device_id
|
|
18
|
+
return params
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AlertsResource:
|
|
22
|
+
def __init__(self, client: BarbaraClient):
|
|
23
|
+
self._client = client
|
|
24
|
+
|
|
25
|
+
def list(self) -> List[Alert]:
|
|
26
|
+
data = unwrap(self._client.request("GET", "/api/v1/alerts"))
|
|
27
|
+
return [Alert.from_api(item) for item in data]
|
|
28
|
+
|
|
29
|
+
def get(self, alert_id: str) -> Alert:
|
|
30
|
+
data = unwrap(self._client.request("GET", f"/api/v1/alerts/{alert_id}"))
|
|
31
|
+
return Alert.from_api(data)
|
|
32
|
+
|
|
33
|
+
def ack(self, alert_id: str) -> Alert:
|
|
34
|
+
data = unwrap(self._client.request("POST", f"/api/v1/alerts/{alert_id}/ack"))
|
|
35
|
+
return Alert.from_api(data)
|
|
36
|
+
|
|
37
|
+
def list_events(
|
|
38
|
+
self, *, offset: int = 0, size: int = 100, device_id: Optional[str] = None
|
|
39
|
+
) -> List[AlertEvent]:
|
|
40
|
+
params = _event_params(offset=offset, size=size, device_id=device_id)
|
|
41
|
+
data = unwrap(self._client.request("GET", "/api/v1/events", params=params))
|
|
42
|
+
return [AlertEvent.from_api(item) for item in data]
|
|
43
|
+
|
|
44
|
+
def get_event(self, event_id: str) -> AlertEvent:
|
|
45
|
+
data = unwrap(self._client.request("GET", f"/api/v1/events/{event_id}"))
|
|
46
|
+
return AlertEvent.from_api(data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AsyncAlertsResource:
|
|
50
|
+
def __init__(self, client: AsyncBarbaraClient):
|
|
51
|
+
self._client = client
|
|
52
|
+
|
|
53
|
+
async def list(self) -> List[Alert]:
|
|
54
|
+
data = unwrap(await self._client.request("GET", "/api/v1/alerts"))
|
|
55
|
+
return [Alert.from_api(item) for item in data]
|
|
56
|
+
|
|
57
|
+
async def get(self, alert_id: str) -> Alert:
|
|
58
|
+
data = unwrap(await self._client.request("GET", f"/api/v1/alerts/{alert_id}"))
|
|
59
|
+
return Alert.from_api(data)
|
|
60
|
+
|
|
61
|
+
async def ack(self, alert_id: str) -> Alert:
|
|
62
|
+
data = unwrap(await self._client.request("POST", f"/api/v1/alerts/{alert_id}/ack"))
|
|
63
|
+
return Alert.from_api(data)
|
|
64
|
+
|
|
65
|
+
async def list_events(
|
|
66
|
+
self, *, offset: int = 0, size: int = 100, device_id: Optional[str] = None
|
|
67
|
+
) -> List[AlertEvent]:
|
|
68
|
+
params = _event_params(offset=offset, size=size, device_id=device_id)
|
|
69
|
+
data = unwrap(await self._client.request("GET", "/api/v1/events", params=params))
|
|
70
|
+
return [AlertEvent.from_api(item) for item in data]
|
|
71
|
+
|
|
72
|
+
async def get_event(self, event_id: str) -> AlertEvent:
|
|
73
|
+
data = unwrap(await self._client.request("GET", f"/api/v1/events/{event_id}"))
|
|
74
|
+
return AlertEvent.from_api(data)
|