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 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)