bronto-sdk 0.2.1__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.
bronto_sdk/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """Python SDK for the Bronto observability platform.
2
+
3
+ Importing this package has no side effects: it configures no logging, reads no
4
+ environment variables, opens no connections, and holds no global mutable state.
5
+ A client instance is the only entry point.
6
+
7
+ Only the names re-exported here (and the documented `domain`, `models`,
8
+ `resources` and `mcp` subpackages) are public API; everything in a module
9
+ prefixed with `_` is an implementation detail and may change without a major
10
+ version bump.
11
+ """
12
+
13
+ from ._async_client import AsyncBrontoClient
14
+ from ._client import BrontoClient
15
+ from ._errors import (
16
+ BrontoAPIError,
17
+ BrontoAuthenticationError,
18
+ BrontoBadRequestError,
19
+ BrontoConfigError,
20
+ BrontoConnectionError,
21
+ BrontoError,
22
+ BrontoNotFoundError,
23
+ BrontoPermissionError,
24
+ BrontoRateLimitError,
25
+ BrontoServerError,
26
+ )
27
+ from ._regions import (
28
+ KNOWN_REGIONS,
29
+ ingest_base_url,
30
+ mcp_url,
31
+ rest_base_url,
32
+ validate_region,
33
+ )
34
+ from ._request_options import RequestOptions
35
+ from ._version import VERSION
36
+
37
+ __all__ = [
38
+ "KNOWN_REGIONS",
39
+ "VERSION",
40
+ "AsyncBrontoClient",
41
+ "BrontoAPIError",
42
+ "BrontoAuthenticationError",
43
+ "BrontoBadRequestError",
44
+ "BrontoClient",
45
+ "BrontoConfigError",
46
+ "BrontoConnectionError",
47
+ "BrontoError",
48
+ "BrontoNotFoundError",
49
+ "BrontoPermissionError",
50
+ "BrontoRateLimitError",
51
+ "BrontoServerError",
52
+ "RequestOptions",
53
+ "ingest_base_url",
54
+ "mcp_url",
55
+ "rest_base_url",
56
+ "validate_region",
57
+ ]
@@ -0,0 +1,245 @@
1
+ """The asynchronous Bronto client — a thin I/O shell over ``httpx.AsyncClient``.
2
+
3
+ A mirror of :class:`bronto_sdk._client.BrontoClient`: identical surface and
4
+ identical request-shaping, differing only in awaiting the HTTP call and
5
+ in the ``max_concurrency`` semaphore that caps concurrent requests to the
6
+ Bronto API gateway.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from functools import cached_property
13
+ from types import TracebackType
14
+ from typing import Any
15
+
16
+ import httpx
17
+
18
+ from ._client_config import (
19
+ client_default_options,
20
+ prepare_request,
21
+ read_env_config,
22
+ resolve_base_url,
23
+ wrap_transport_error,
24
+ )
25
+ from ._request_options import RequestOptions, client_credential
26
+ from ._transport import parse_response
27
+ from .resources._datasets import AsyncDatasetsResource
28
+ from .resources._metrics import AsyncMetricsResource
29
+ from .resources._monitors import AsyncMonitorsResource
30
+ from .resources._search import AsyncSearchResource
31
+
32
+
33
+ class AsyncBrontoClient:
34
+ """An asynchronous client for the Bronto REST API.
35
+
36
+ The client is either *client-bound* (constructed with ``api_key`` or
37
+ ``bearer_token``; every call uses it) or *credential-less* (constructed with
38
+ neither; every call must supply a credential via ``options``). The mode is
39
+ fixed at construction — see the two-mode rule in
40
+ :mod:`bronto_sdk._request_options`.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ api_key: str | None = None,
46
+ region: str | None = None,
47
+ *,
48
+ bearer_token: str | None = None,
49
+ timeout: float = 30.0,
50
+ base_url: str | None = None,
51
+ http_client: httpx.AsyncClient | None = None,
52
+ max_concurrency: int = 20,
53
+ ) -> None:
54
+ """Construct a client.
55
+
56
+ Args:
57
+ api_key: A Bronto API key to bind to every call. Omit for a
58
+ credential-less client.
59
+ region: The region slug (e.g. ``"eu"``); resolved to the REST base
60
+ URL. Ignored when ``base_url`` is given.
61
+ bearer_token: A JWT bearer token to bind to every call. Wins over
62
+ ``api_key``. Omit for a credential-less client.
63
+ timeout: The default per-request timeout in seconds.
64
+ base_url: An explicit base URL that overrides ``region`` — for
65
+ testing or a non-standard deployment. Trusted and used verbatim.
66
+ http_client: An existing ``httpx.AsyncClient`` to reuse. When given,
67
+ the SDK will not close it.
68
+ max_concurrency: The maximum number of in-flight requests.
69
+
70
+ Raises:
71
+ BrontoConfigError: When neither ``region`` nor ``base_url`` resolves
72
+ an endpoint, or the region slug is invalid.
73
+ ValueError: When ``max_concurrency`` is less than 1.
74
+ """
75
+ if max_concurrency < 1:
76
+ raise ValueError("max_concurrency must be a positive integer")
77
+ self._base_url = resolve_base_url(region=region, base_url=base_url)
78
+ self._bound = client_credential(api_key=api_key, bearer_token=bearer_token)
79
+ self._defaults = client_default_options(timeout=timeout)
80
+ self._http = http_client or httpx.AsyncClient()
81
+ self._owns_http_client = http_client is None
82
+ self._semaphore = asyncio.Semaphore(max_concurrency)
83
+
84
+ @classmethod
85
+ def from_env(cls) -> AsyncBrontoClient:
86
+ """Build a client from ``BRONTO_API_KEY`` and ``BRONTO_REGION``.
87
+
88
+ ``BRONTO_BASE_URL`` is honoured when set and wins over ``BRONTO_REGION``.
89
+
90
+ Returns:
91
+ A client-bound :class:`AsyncBrontoClient`.
92
+
93
+ Raises:
94
+ BrontoConfigError: Naming the first missing environment variable.
95
+ """
96
+ env = read_env_config()
97
+ return cls(api_key=env.api_key, region=env.region, base_url=env.base_url)
98
+
99
+ @cached_property
100
+ def search(self) -> AsyncSearchResource:
101
+ """The typed ``POST /search`` surface."""
102
+ return AsyncSearchResource(self)
103
+
104
+ @cached_property
105
+ def monitors(self) -> AsyncMonitorsResource:
106
+ """The typed monitor surface."""
107
+ return AsyncMonitorsResource(self)
108
+
109
+ @cached_property
110
+ def datasets(self) -> AsyncDatasetsResource:
111
+ """The typed dataset surface."""
112
+ return AsyncDatasetsResource(self)
113
+
114
+ @cached_property
115
+ def metrics(self) -> AsyncMetricsResource:
116
+ """The typed metric-definition surface."""
117
+ return AsyncMetricsResource(self)
118
+
119
+ async def get(
120
+ self,
121
+ path: str,
122
+ *,
123
+ params: dict[str, Any] | None = None,
124
+ options: RequestOptions | None = None,
125
+ ) -> dict[str, object]:
126
+ """Send a ``GET`` request and return the parsed JSON object.
127
+
128
+ Args:
129
+ path: The request path, absolute-style (``"/search"``) or bare
130
+ (``"monitors/m-1"``).
131
+ params: Optional query-string parameters.
132
+ options: Optional per-request overrides.
133
+
134
+ Returns:
135
+ The decoded JSON response object.
136
+
137
+ Raises:
138
+ BrontoAPIError: On any non-2xx response.
139
+ BrontoConnectionError: On a transport-level failure.
140
+ BrontoConfigError: On a credential resolution error.
141
+ """
142
+ return await self._request(
143
+ "GET", path, params=params, json_body=None, options=options
144
+ )
145
+
146
+ async def post(
147
+ self,
148
+ path: str,
149
+ *,
150
+ json: dict[str, Any] | None = None,
151
+ params: dict[str, Any] | None = None,
152
+ options: RequestOptions | None = None,
153
+ ) -> dict[str, object]:
154
+ """Send a ``POST`` request and return the parsed JSON object.
155
+
156
+ Args:
157
+ path: The request path, absolute-style or bare.
158
+ json: Optional JSON request body.
159
+ params: Optional query-string parameters.
160
+ options: Optional per-request overrides.
161
+
162
+ Returns:
163
+ The decoded JSON response object.
164
+
165
+ Raises:
166
+ BrontoAPIError: On any non-2xx response.
167
+ BrontoConnectionError: On a transport-level failure.
168
+ BrontoConfigError: On a credential resolution error.
169
+ """
170
+ return await self._request(
171
+ "POST", path, params=params, json_body=json, options=options
172
+ )
173
+
174
+ async def _request(
175
+ self,
176
+ method: str,
177
+ path: str,
178
+ *,
179
+ params: dict[str, Any] | None,
180
+ json_body: dict[str, Any] | None,
181
+ options: RequestOptions | None,
182
+ ) -> dict[str, object]:
183
+ """Perform the shared per-call flow around a single ``httpx`` request.
184
+
185
+ The concurrency semaphore is held only for the duration of the network
186
+ call, so credential resolution and option merging never occupy a slot.
187
+
188
+ Args:
189
+ method: The HTTP method.
190
+ path: The request path.
191
+ params: Optional query-string parameters.
192
+ json_body: Optional JSON request body.
193
+ options: Optional per-request overrides.
194
+
195
+ Returns:
196
+ The decoded JSON response object.
197
+
198
+ Raises:
199
+ BrontoAPIError: On any non-2xx response.
200
+ BrontoConnectionError: On a transport-level failure.
201
+ BrontoConfigError: On a credential resolution error.
202
+ """
203
+ prepared = prepare_request(
204
+ bound=self._bound,
205
+ defaults=self._defaults,
206
+ base_url=self._base_url,
207
+ path=path,
208
+ options=options,
209
+ )
210
+ timeout = (
211
+ prepared.timeout
212
+ if prepared.timeout is not None
213
+ else httpx.USE_CLIENT_DEFAULT
214
+ )
215
+ try:
216
+ async with self._semaphore:
217
+ response = await self._http.request(
218
+ method,
219
+ prepared.url,
220
+ params=params,
221
+ json=json_body,
222
+ headers=prepared.headers,
223
+ timeout=timeout,
224
+ )
225
+ except httpx.HTTPError as exc:
226
+ raise wrap_transport_error(exc) from exc
227
+ return parse_response(response.status_code, response.headers, response.text)
228
+
229
+ async def aclose(self) -> None:
230
+ """Close the underlying HTTP client, unless it was caller-supplied."""
231
+ if self._owns_http_client:
232
+ await self._http.aclose()
233
+
234
+ async def __aenter__(self) -> AsyncBrontoClient:
235
+ """Enter the async context manager, returning the client."""
236
+ return self
237
+
238
+ async def __aexit__(
239
+ self,
240
+ exc_type: type[BaseException] | None,
241
+ exc: BaseException | None,
242
+ tb: TracebackType | None,
243
+ ) -> None:
244
+ """Close the client on async context-manager exit."""
245
+ await self.aclose()
bronto_sdk/_auth.py ADDED
@@ -0,0 +1,40 @@
1
+ """Authentication and identity header construction.
2
+
3
+ Bronto accepts two schemes: a JWT bearer token (``Authorization``) and an API
4
+ key (``X-BRONTO-API-KEY``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal
10
+
11
+ from ._version import VERSION
12
+
13
+ _API_KEY_HEADER = "X-BRONTO-API-KEY"
14
+ _BEARER_HEADER = "Authorization"
15
+
16
+
17
+ def credential_header(
18
+ scheme: Literal["bearer", "api_key"], value: str
19
+ ) -> dict[str, str]:
20
+ """Build the auth header for an already-resolved single credential.
21
+
22
+ Args:
23
+ scheme: ``"bearer"`` or ``"api_key"``.
24
+ value: The credential string for that scheme.
25
+
26
+ Returns:
27
+ A single-entry dict with the matching authentication header.
28
+ """
29
+ if scheme == "bearer":
30
+ return {_BEARER_HEADER: value}
31
+ return {_API_KEY_HEADER: value}
32
+
33
+
34
+ def user_agent() -> str:
35
+ """Return the SDK's ``User-Agent`` value.
36
+
37
+ Returns:
38
+ ``bronto-sdk-python/<version>``.
39
+ """
40
+ return f"bronto-sdk-python/{VERSION}"
bronto_sdk/_client.py ADDED
@@ -0,0 +1,234 @@
1
+ """The synchronous Bronto client — a thin I/O shell over ``httpx.Client``.
2
+
3
+ Every request-shaping decision lives in the pure helpers of
4
+ :mod:`bronto_sdk._client_config` and :mod:`bronto_sdk._transport`; this module
5
+ only performs the actual HTTP call, so it stays a mirror of
6
+ :class:`bronto_sdk._async_client.AsyncBrontoClient`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from functools import cached_property
12
+ from types import TracebackType
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ from ._client_config import (
18
+ client_default_options,
19
+ prepare_request,
20
+ read_env_config,
21
+ resolve_base_url,
22
+ wrap_transport_error,
23
+ )
24
+ from ._request_options import RequestOptions, client_credential
25
+ from ._transport import parse_response
26
+ from .resources._datasets import DatasetsResource
27
+ from .resources._metrics import MetricsResource
28
+ from .resources._monitors import MonitorsResource
29
+ from .resources._search import SearchResource
30
+
31
+
32
+ class BrontoClient:
33
+ """A synchronous client for the Bronto REST API.
34
+
35
+ The client is either *client-bound* (constructed with ``api_key`` or
36
+ ``bearer_token``; every call uses it) or *credential-less* (constructed with
37
+ neither; every call must supply a credential via ``options``). The mode is
38
+ fixed at construction — see the two-mode rule in
39
+ :mod:`bronto_sdk._request_options`.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ api_key: str | None = None,
45
+ region: str | None = None,
46
+ *,
47
+ bearer_token: str | None = None,
48
+ timeout: float = 30.0,
49
+ base_url: str | None = None,
50
+ http_client: httpx.Client | None = None,
51
+ ) -> None:
52
+ """Construct a client.
53
+
54
+ Args:
55
+ api_key: A Bronto API key to bind to every call. Omit for a
56
+ credential-less client.
57
+ region: The region slug (e.g. ``"eu"``); resolved to the REST base
58
+ URL. Ignored when ``base_url`` is given.
59
+ bearer_token: A JWT bearer token to bind to every call. Wins over
60
+ ``api_key``. Omit for a credential-less client.
61
+ timeout: The default per-request timeout in seconds.
62
+ base_url: An explicit base URL that overrides ``region`` — for
63
+ testing or a non-standard deployment. Trusted and used verbatim.
64
+ http_client: An existing ``httpx.Client`` to reuse. When given, the
65
+ SDK will not close it.
66
+
67
+ Raises:
68
+ BrontoConfigError: When neither ``region`` nor ``base_url`` resolves
69
+ an endpoint, or the region slug is invalid.
70
+ """
71
+ self._base_url = resolve_base_url(region=region, base_url=base_url)
72
+ self._bound = client_credential(api_key=api_key, bearer_token=bearer_token)
73
+ self._defaults = client_default_options(timeout=timeout)
74
+ self._http = http_client or httpx.Client()
75
+ self._owns_http_client = http_client is None
76
+
77
+ @classmethod
78
+ def from_env(cls) -> BrontoClient:
79
+ """Build a client from ``BRONTO_API_KEY`` and ``BRONTO_REGION``.
80
+
81
+ ``BRONTO_BASE_URL`` is honoured when set and wins over ``BRONTO_REGION``.
82
+
83
+ Returns:
84
+ A client-bound :class:`BrontoClient`.
85
+
86
+ Raises:
87
+ BrontoConfigError: Naming the first missing environment variable.
88
+ """
89
+ env = read_env_config()
90
+ return cls(api_key=env.api_key, region=env.region, base_url=env.base_url)
91
+
92
+ @cached_property
93
+ def search(self) -> SearchResource:
94
+ """The typed ``POST /search`` surface."""
95
+ return SearchResource(self)
96
+
97
+ @cached_property
98
+ def monitors(self) -> MonitorsResource:
99
+ """The typed monitor surface."""
100
+ return MonitorsResource(self)
101
+
102
+ @cached_property
103
+ def datasets(self) -> DatasetsResource:
104
+ """The typed dataset surface."""
105
+ return DatasetsResource(self)
106
+
107
+ @cached_property
108
+ def metrics(self) -> MetricsResource:
109
+ """The typed metric-definition surface."""
110
+ return MetricsResource(self)
111
+
112
+ def get(
113
+ self,
114
+ path: str,
115
+ *,
116
+ params: dict[str, Any] | None = None,
117
+ options: RequestOptions | None = None,
118
+ ) -> dict[str, object]:
119
+ """Send a ``GET`` request and return the parsed JSON object.
120
+
121
+ Args:
122
+ path: The request path, absolute-style (``"/search"``) or bare
123
+ (``"monitors/m-1"``).
124
+ params: Optional query-string parameters.
125
+ options: Optional per-request overrides.
126
+
127
+ Returns:
128
+ The decoded JSON response object.
129
+
130
+ Raises:
131
+ BrontoAPIError: On any non-2xx response.
132
+ BrontoConnectionError: On a transport-level failure.
133
+ BrontoConfigError: On a credential resolution error.
134
+ """
135
+ return self._request(
136
+ "GET", path, params=params, json_body=None, options=options
137
+ )
138
+
139
+ def post(
140
+ self,
141
+ path: str,
142
+ *,
143
+ json: dict[str, Any] | None = None,
144
+ params: dict[str, Any] | None = None,
145
+ options: RequestOptions | None = None,
146
+ ) -> dict[str, object]:
147
+ """Send a ``POST`` request and return the parsed JSON object.
148
+
149
+ Args:
150
+ path: The request path, absolute-style or bare.
151
+ json: Optional JSON request body.
152
+ params: Optional query-string parameters.
153
+ options: Optional per-request overrides.
154
+
155
+ Returns:
156
+ The decoded JSON response object.
157
+
158
+ Raises:
159
+ BrontoAPIError: On any non-2xx response.
160
+ BrontoConnectionError: On a transport-level failure.
161
+ BrontoConfigError: On a credential resolution error.
162
+ """
163
+ return self._request(
164
+ "POST", path, params=params, json_body=json, options=options
165
+ )
166
+
167
+ def _request(
168
+ self,
169
+ method: str,
170
+ path: str,
171
+ *,
172
+ params: dict[str, Any] | None,
173
+ json_body: dict[str, Any] | None,
174
+ options: RequestOptions | None,
175
+ ) -> dict[str, object]:
176
+ """Perform the shared per-call flow around a single ``httpx`` request.
177
+
178
+ Args:
179
+ method: The HTTP method.
180
+ path: The request path.
181
+ params: Optional query-string parameters.
182
+ json_body: Optional JSON request body.
183
+ options: Optional per-request overrides.
184
+
185
+ Returns:
186
+ The decoded JSON response object.
187
+
188
+ Raises:
189
+ BrontoAPIError: On any non-2xx response.
190
+ BrontoConnectionError: On a transport-level failure.
191
+ BrontoConfigError: On a credential resolution error.
192
+ """
193
+ prepared = prepare_request(
194
+ bound=self._bound,
195
+ defaults=self._defaults,
196
+ base_url=self._base_url,
197
+ path=path,
198
+ options=options,
199
+ )
200
+ timeout = (
201
+ prepared.timeout
202
+ if prepared.timeout is not None
203
+ else httpx.USE_CLIENT_DEFAULT
204
+ )
205
+ try:
206
+ response = self._http.request(
207
+ method,
208
+ prepared.url,
209
+ params=params,
210
+ json=json_body,
211
+ headers=prepared.headers,
212
+ timeout=timeout,
213
+ )
214
+ except httpx.HTTPError as exc:
215
+ raise wrap_transport_error(exc) from exc
216
+ return parse_response(response.status_code, response.headers, response.text)
217
+
218
+ def close(self) -> None:
219
+ """Close the underlying HTTP client, unless it was caller-supplied."""
220
+ if self._owns_http_client:
221
+ self._http.close()
222
+
223
+ def __enter__(self) -> BrontoClient:
224
+ """Enter the context manager, returning the client."""
225
+ return self
226
+
227
+ def __exit__(
228
+ self,
229
+ exc_type: type[BaseException] | None,
230
+ exc: BaseException | None,
231
+ tb: TracebackType | None,
232
+ ) -> None:
233
+ """Close the client on context-manager exit."""
234
+ self.close()