ds-protocol-http-py-lib 0.1.0a1__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.
@@ -0,0 +1,30 @@
1
+ """
2
+ A Python package from the ds-protocol library collection.
3
+
4
+ **File:** ``__init__.py``
5
+ **Region:** ``ds-protocol-http-py-lib``
6
+
7
+ Example:
8
+
9
+ .. code-block:: python
10
+
11
+ from ds_protocol_http_py_lib import __version__
12
+
13
+ print(f"Package version: {__version__}")
14
+ """
15
+
16
+ from importlib.metadata import version
17
+
18
+ PACKAGE_NAME = "ds-protocol-http-py-lib"
19
+ __version__ = version(PACKAGE_NAME)
20
+
21
+ from .dataset import HttpDataset, HttpDatasetTypedProperties # noqa: E402
22
+ from .linked_service import HttpLinkedService, HttpLinkedServiceTypedProperties # noqa: E402
23
+
24
+ __all__ = [
25
+ "HttpDataset",
26
+ "HttpDatasetTypedProperties",
27
+ "HttpLinkedService",
28
+ "HttpLinkedServiceTypedProperties",
29
+ "__version__",
30
+ ]
@@ -0,0 +1,33 @@
1
+ """
2
+ **File:** ``__init__.py``
3
+ **Region:** ``ds_protocol_http_py_lib/dataset``
4
+
5
+ HTTP Dataset
6
+
7
+ This module implements a dataset for HTTP APIs.
8
+
9
+ Example:
10
+ >>> dataset = HttpDataset(
11
+ ... deserializer=PandasDeserializer(format=DatasetStorageFormatType.JSON),
12
+ ... serializer=PandasSerializer(format=DatasetStorageFormatType.JSON),
13
+ ... typed_properties=HttpDatasetTypedProperties(
14
+ ... url="https://api.example.com/data",
15
+ ... method="GET",
16
+ ... ),
17
+ ... linked_service=HttpLinkedService(
18
+ ... typed_properties=HttpLinkedServiceTypedProperties(
19
+ ... host="https://api.example.com",
20
+ ... auth_type="OAuth2",
21
+ ... ),
22
+ ... ),
23
+ ... )
24
+ >>> dataset.read()
25
+ >>> data = dataset.content
26
+ """
27
+
28
+ from .http import HttpDataset, HttpDatasetTypedProperties
29
+
30
+ __all__ = [
31
+ "HttpDataset",
32
+ "HttpDatasetTypedProperties",
33
+ ]
@@ -0,0 +1,177 @@
1
+ """
2
+ **File:** ``http.py``
3
+ **Region:** ``ds_protocol_http_py_lib/dataset/http``
4
+
5
+ HTTP Dataset
6
+
7
+ This module implements a dataset for HTTP APIs.
8
+
9
+ Example:
10
+ >>> dataset = HttpDataset(
11
+ ... deserializer=PandasDeserializer(format=DatasetStorageFormatType.JSON),
12
+ ... serializer=PandasSerializer(format=DatasetStorageFormatType.JSON),
13
+ ... typed_properties=HttpDatasetTypedProperties(
14
+ ... url="https://api.example.com/data",
15
+ ... method="GET",
16
+ ... ),
17
+ ... linked_service=HttpLinkedService(
18
+ ... typed_properties=HttpLinkedServiceTypedProperties(
19
+ ... host="https://api.example.com",
20
+ ... auth_type="OAuth2",
21
+ ... ),
22
+ ... ),
23
+ ... )
24
+ >>> dataset.read()
25
+ >>> data = dataset.content
26
+ """
27
+
28
+ from dataclasses import dataclass, field
29
+ from typing import Any, Generic, Literal, NoReturn, TypeVar
30
+
31
+ import pandas as pd
32
+ from ds_resource_plugin_py_lib.common.resource.dataset import (
33
+ DatasetStorageFormatType,
34
+ DatasetTypedProperties,
35
+ TabularDataset,
36
+ )
37
+ from ds_resource_plugin_py_lib.common.resource.linked_service.errors import (
38
+ ConnectionError,
39
+ )
40
+ from ds_resource_plugin_py_lib.common.serde.deserialize import (
41
+ DataDeserializer,
42
+ PandasDeserializer,
43
+ )
44
+ from ds_resource_plugin_py_lib.common.serde.serialize import (
45
+ DataSerializer,
46
+ PandasSerializer,
47
+ )
48
+
49
+ from ..enums import ResourceKind
50
+ from ..linked_service.http import HttpLinkedService
51
+ from ..utils.http.provider import Http
52
+
53
+
54
+ @dataclass(kw_only=True)
55
+ class HttpDatasetTypedProperties(DatasetTypedProperties):
56
+ method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"] = "GET"
57
+
58
+ url: str
59
+ data: Any | None = None
60
+ json: dict[str, Any] | None = None
61
+ params: dict[str, Any] | None = None
62
+ files: list[Any] | None = None
63
+ headers: dict[str, Any] | None = None
64
+
65
+
66
+ HttpDatasetTypedPropertiesType = TypeVar(
67
+ "HttpDatasetTypedPropertiesType",
68
+ bound=HttpDatasetTypedProperties,
69
+ )
70
+ HttpLinkedServiceType = TypeVar(
71
+ "HttpLinkedServiceType",
72
+ bound=HttpLinkedService[Any],
73
+ )
74
+
75
+
76
+ @dataclass(kw_only=True)
77
+ class HttpDataset(
78
+ TabularDataset[
79
+ HttpLinkedServiceType,
80
+ HttpDatasetTypedPropertiesType,
81
+ DataSerializer,
82
+ DataDeserializer,
83
+ ],
84
+ Generic[HttpLinkedServiceType, HttpDatasetTypedPropertiesType],
85
+ ):
86
+ linked_service: HttpLinkedServiceType
87
+ typed_properties: HttpDatasetTypedPropertiesType
88
+
89
+ serializer: DataSerializer | None = field(
90
+ default_factory=lambda: PandasSerializer(format=DatasetStorageFormatType.JSON),
91
+ )
92
+ deserializer: DataDeserializer | None = field(
93
+ default_factory=lambda: PandasDeserializer(format=DatasetStorageFormatType.JSON),
94
+ )
95
+ connection: Http | None = field(default=None, init=False)
96
+
97
+ def __post_init__(self) -> None:
98
+ if self.linked_service is not None:
99
+ self.connection = self.linked_service.connect()
100
+
101
+ @property
102
+ def kind(self) -> ResourceKind:
103
+ return ResourceKind.DATASET
104
+
105
+ def create(self, **kwargs: Any) -> None:
106
+ """
107
+ Create data at the specified endpoint.
108
+
109
+ Args:
110
+ kwargs: Additional keyword arguments to pass to the request.
111
+ """
112
+ if self.connection is None:
113
+ raise ConnectionError(
114
+ message="Connection is not initialized.",
115
+ code="NOT_INITIALIZED",
116
+ status_code=503,
117
+ )
118
+
119
+ self.log.info(f"Sending {self.typed_properties.method} request to {self.typed_properties.url}")
120
+
121
+ response = self.connection.request(
122
+ method=self.typed_properties.method,
123
+ url=self.typed_properties.url,
124
+ data=self.typed_properties.data,
125
+ json=self.typed_properties.json,
126
+ files=self.typed_properties.files,
127
+ params=self.typed_properties.params,
128
+ headers=self.typed_properties.headers,
129
+ **kwargs,
130
+ )
131
+
132
+ if response.content and self.deserializer:
133
+ self.content = self.deserializer(response.content)
134
+ else:
135
+ self.content = pd.DataFrame()
136
+
137
+ def read(self, **kwargs: Any) -> None:
138
+ """
139
+ Read data from the specified endpoint.
140
+
141
+ Args:
142
+ kwargs: Additional keyword arguments to pass to the request.
143
+ """
144
+ if self.connection is None:
145
+ raise ConnectionError(message="Connection is not initialized.")
146
+
147
+ self.log.info(f"Sending {self.typed_properties.method} request to {self.typed_properties.url}")
148
+
149
+ response = self.connection.request(
150
+ method=self.typed_properties.method,
151
+ url=self.typed_properties.url,
152
+ data=self.typed_properties.data,
153
+ json=self.typed_properties.json,
154
+ files=self.typed_properties.files,
155
+ params=self.typed_properties.params,
156
+ headers=self.typed_properties.headers,
157
+ **kwargs,
158
+ )
159
+
160
+ if response.content and self.deserializer:
161
+ self.content = self.deserializer(response.content)
162
+ self.next = self.deserializer.get_next(response.content)
163
+ if self.next:
164
+ self.cursor = self.deserializer.get_end_cursor(response.content)
165
+ else:
166
+ self.next = False
167
+ self.cursor = None
168
+ self.content = pd.DataFrame()
169
+
170
+ def delete(self, **kwargs: Any) -> NoReturn:
171
+ raise NotImplementedError("Delete operation is not supported for Http datasets")
172
+
173
+ def update(self, **kwargs: Any) -> NoReturn:
174
+ raise NotImplementedError("Update operation is not supported for Http datasets")
175
+
176
+ def rename(self, **kwargs: Any) -> NoReturn:
177
+ raise NotImplementedError("Rename operation is not supported for Http datasets")
@@ -0,0 +1,23 @@
1
+ """
2
+ **File:** ``enums.py``
3
+ **Region:** ``ds_protocol_http_py_lib/enums``
4
+
5
+ Constants for HTTP protocol.
6
+
7
+ Example:
8
+ >>> ResourceKind.LINKED_SERVICE
9
+ 'DS.RESOURCE.LINKED_SERVICE.HTTP'
10
+ >>> ResourceKind.DATASET
11
+ 'DS.RESOURCE.DATASET.HTTP'
12
+ """
13
+
14
+ from enum import StrEnum
15
+
16
+
17
+ class ResourceKind(StrEnum):
18
+ """
19
+ Constants for HTTP protocol.
20
+ """
21
+
22
+ LINKED_SERVICE = "DS.RESOURCE.LINKED_SERVICE.HTTP"
23
+ DATASET = "DS.RESOURCE.DATASET.HTTP"
@@ -0,0 +1,27 @@
1
+ """
2
+ **File:** ``__init__.py``
3
+ **Region:** ``ds_protocol_http_py_lib/linked_service``
4
+
5
+ HTTP Linked Service
6
+
7
+ This module implements a linked service for HTTP APIs.
8
+
9
+ Example:
10
+ >>> linked_service = HttpLinkedService(
11
+ ... typed_properties=HttpLinkedServiceTypedProperties(
12
+ ... host="https://api.example.com",
13
+ ... auth_type="OAuth2",
14
+ ... client_id="",
15
+ ... client_secret="",
16
+ ... token_endpoint="https://api.example.com/token",
17
+ ... ),
18
+ ... )
19
+ >>> linked_service.connect()
20
+ """
21
+
22
+ from .http import HttpLinkedService, HttpLinkedServiceTypedProperties
23
+
24
+ __all__ = [
25
+ "HttpLinkedService",
26
+ "HttpLinkedServiceTypedProperties",
27
+ ]
@@ -0,0 +1,399 @@
1
+ """
2
+ **File:** ``http.py``
3
+ **Region:** ``ds_protocol_http_py_lib/linked_service/http``
4
+
5
+ HTTP Linked Service
6
+
7
+ This module implements a linked service for HTTP APIs.
8
+
9
+ Example:
10
+ >>> linked_service = HttpLinkedService(
11
+ ... typed_properties=HttpLinkedServiceTypedProperties(
12
+ ... host="https://api.example.com",
13
+ ... auth_type="OAuth2",
14
+ ... ),
15
+ ... )
16
+ >>> linked_service.connect()
17
+ """
18
+
19
+ import base64
20
+ from dataclasses import dataclass, field
21
+ from typing import Generic, Literal, TypeVar
22
+
23
+ from ds_resource_plugin_py_lib.common.resource.linked_service import (
24
+ LinkedService,
25
+ LinkedServiceTypedProperties,
26
+ )
27
+ from ds_resource_plugin_py_lib.common.resource.linked_service.errors import (
28
+ AuthenticationError,
29
+ )
30
+ from requests import HTTPError
31
+
32
+ from .. import PACKAGE_NAME, __version__
33
+ from ..enums import ResourceKind
34
+ from ..utils import find_keys_in_json
35
+ from ..utils.http.config import HttpConfig, RetryConfig
36
+ from ..utils.http.provider import Http
37
+ from ..utils.http.token_bucket import TokenBucket
38
+
39
+
40
+ @dataclass(kw_only=True)
41
+ class HttpLinkedServiceTypedProperties(LinkedServiceTypedProperties):
42
+ """
43
+ The object containing the HTTP linked service properties.
44
+ """
45
+
46
+ host: str
47
+ auth_type: Literal[
48
+ "OAuth2",
49
+ "Basic",
50
+ "APIKey",
51
+ "Bearer",
52
+ "NoAuth",
53
+ "Custom",
54
+ ]
55
+ schema: str = "https"
56
+ port: int | None = None
57
+ api_key_name: str | None = None
58
+ api_key_value: str | None = None
59
+ username_key_name: str | None = "email"
60
+ username_key_value: str | None = None
61
+ password_key_name: str | None = "password"
62
+ password_key_value: str | None = None
63
+ client_id: str | None = None
64
+ client_secret: str | None = None
65
+ token_endpoint: str | None = None
66
+ scope: str | None = None
67
+ headers: dict[str, str] | None = None
68
+ data: dict[str, str] | None = None
69
+
70
+
71
+ HttpLinkedServiceTypedPropertiesType = TypeVar(
72
+ "HttpLinkedServiceTypedPropertiesType",
73
+ bound=HttpLinkedServiceTypedProperties,
74
+ )
75
+
76
+
77
+ @dataclass(kw_only=True)
78
+ class HttpLinkedService(
79
+ LinkedService[HttpLinkedServiceTypedPropertiesType],
80
+ Generic[HttpLinkedServiceTypedPropertiesType],
81
+ ):
82
+ """
83
+ The class is used to connect with HTTP API.
84
+ """
85
+
86
+ typed_properties: HttpLinkedServiceTypedPropertiesType
87
+ _http: Http | None = field(default=None, init=False)
88
+ _auth_configured: bool = field(default=False, init=False)
89
+
90
+ def __post_init__(self) -> None:
91
+ self.base_uri = (
92
+ self.typed_properties.host
93
+ if self.typed_properties.host and "://" in self.typed_properties.host
94
+ else f"{self.typed_properties.schema}://{self.typed_properties.host}"
95
+ )
96
+
97
+ if self.typed_properties.port:
98
+ self.base_uri = f"{self.typed_properties.host}:{self.typed_properties.port}"
99
+
100
+ self._http = self._init_http()
101
+
102
+ @property
103
+ def kind(self) -> ResourceKind:
104
+ """
105
+ Get the kind of the linked service.
106
+ Returns:
107
+ ResourceKind
108
+ """
109
+ return ResourceKind.LINKED_SERVICE
110
+
111
+ def _init_http(self) -> Http:
112
+ """
113
+ Initialize the Http client instance with HttpConfig and TokenBucket.
114
+
115
+ Creates an Http instance with:
116
+ - HttpConfig using headers from the linked service properties
117
+ - TokenBucket with rate limiting (10 requests per second, capacity of 20)
118
+
119
+ Subclasses can override this method to customize the entire Http initialization,
120
+ including custom HttpConfig, TokenBucket, or other Http parameters.
121
+
122
+ Returns:
123
+ Http: The initialized Http client instance.
124
+ """
125
+ retry_config = RetryConfig(
126
+ total=3,
127
+ backoff_factor=0.2,
128
+ status_forcelist=(429, 500, 502, 503, 504),
129
+ allowed_methods=("GET", "POST", "PUT", "DELETE", "PATCH"),
130
+ raise_on_status=False,
131
+ respect_retry_after_header=True,
132
+ )
133
+ config = HttpConfig(
134
+ headers=dict(self.typed_properties.headers or {}),
135
+ timeout_seconds=60,
136
+ user_agent=f"{PACKAGE_NAME}/{__version__}",
137
+ retry=retry_config,
138
+ )
139
+ token_bucket = TokenBucket(rps=10, capacity=20)
140
+ return Http(config=config, bucket=token_bucket)
141
+
142
+ def _fetch_user_token(self, http: Http) -> str:
143
+ """
144
+ Fetch a user token from the token endpoint using the Http provider.
145
+
146
+ Args:
147
+ http: The Http instance to use for the request.
148
+
149
+ Returns:
150
+ str: The user token.
151
+ """
152
+ url = self.typed_properties.token_endpoint
153
+ headers = {"Content-type": "application/json"}
154
+ data = {
155
+ self.typed_properties.username_key_name: self.typed_properties.username_key_value,
156
+ self.typed_properties.password_key_name: self.typed_properties.password_key_value,
157
+ }
158
+ if not url:
159
+ raise ValueError("Token endpoint is missing in the linked service properties")
160
+
161
+ try:
162
+ response = http.post(
163
+ url=url,
164
+ headers=headers,
165
+ json=data,
166
+ timeout=30,
167
+ )
168
+ token = find_keys_in_json(response.json(), {"access_token", "accessToken", "token"})
169
+ if token is None:
170
+ raise ValueError("Token not found in response")
171
+ except HTTPError as exc:
172
+ raise AuthenticationError(
173
+ message=f"Authentication error: {exc}",
174
+ details={
175
+ "http_status_code": exc.response.status_code,
176
+ "http_response_body": exc.response.text,
177
+ },
178
+ ) from exc
179
+ except Exception as exc:
180
+ raise AuthenticationError(
181
+ message=f"Authentication error: {exc}",
182
+ details={
183
+ "error_type": type(exc).__name__,
184
+ "error_message": str(exc),
185
+ },
186
+ ) from exc
187
+
188
+ return token
189
+
190
+ def _fetch_oauth2_token(self, http: Http) -> str:
191
+ """
192
+ Fetch an OAuth2 token from the token endpoint using the Http provider.
193
+
194
+ Args:
195
+ http: The Http instance to use for the request.
196
+
197
+ Returns:
198
+ str: The OAuth2 token.
199
+ """
200
+ url = self.typed_properties.token_endpoint
201
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
202
+ data = {
203
+ "client_id": self.typed_properties.client_id,
204
+ "client_secret": self.typed_properties.client_secret,
205
+ "scope": self.typed_properties.scope,
206
+ "grant_type": "client_credentials",
207
+ }
208
+ if not url:
209
+ raise ValueError("Token endpoint is missing in the linked service properties")
210
+
211
+ try:
212
+ response = http.post(
213
+ url=url,
214
+ headers=headers,
215
+ data=data,
216
+ timeout=30,
217
+ )
218
+ token = find_keys_in_json(response.json(), {"access_token", "accessToken", "token"})
219
+ if token is None:
220
+ raise ValueError("Token not found in response")
221
+ except HTTPError as exc:
222
+ raise AuthenticationError(
223
+ message=f"Authentication error: {exc}",
224
+ details={
225
+ "http_status_code": exc.response.status_code,
226
+ "http_response_body": exc.response.text,
227
+ },
228
+ ) from exc
229
+ except Exception as exc:
230
+ raise AuthenticationError(
231
+ message=f"Authentication error: {exc}",
232
+ details={
233
+ "error_type": type(exc).__name__,
234
+ "error_message": str(exc),
235
+ },
236
+ ) from exc
237
+
238
+ return token
239
+
240
+ def _configure_bearer_auth(self, http: Http) -> None:
241
+ """
242
+ Configure Bearer authentication.
243
+
244
+ Fetches a user token via `_fetch_user_token` and sets the session's
245
+ Authorization header.
246
+
247
+ Args:
248
+ http: The Http client instance to configure.
249
+ """
250
+ user_access_token = self._fetch_user_token(http)
251
+ http.session.headers.update({"Authorization": f"Bearer {user_access_token}"})
252
+
253
+ def _configure_oauth2_auth(self, http: Http) -> None:
254
+ """
255
+ Configure OAuth2 (client credentials) authentication.
256
+
257
+ Fetches an OAuth2 token via `_fetch_oauth2_token` and sets the session's
258
+ Authorization header.
259
+
260
+ Args:
261
+ http: The Http client instance to configure.
262
+ """
263
+ oauth2_access_token = self._fetch_oauth2_token(http)
264
+ http.session.headers.update({"Authorization": f"Bearer {oauth2_access_token}"})
265
+
266
+ def _configure_basic_auth(self, http: Http) -> None:
267
+ """
268
+ Configure HTTP Basic authentication.
269
+
270
+ Uses `username_key_value` and `password_key_value` to construct a
271
+ base64-encoded `username:password` token and sets the session's
272
+ Authorization header.
273
+
274
+ Args:
275
+ http: The Http client instance to configure.
276
+
277
+ Raises:
278
+ ValueError: If username or password is missing.
279
+ """
280
+ username = self.typed_properties.username_key_value
281
+ password = self.typed_properties.password_key_value
282
+ if not username:
283
+ raise ValueError("Basic auth username is missing in the linked service")
284
+ if not password:
285
+ raise ValueError("Basic auth password is missing in the linked service")
286
+ token = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
287
+ http.session.headers.update({"Authorization": f"Basic {token}"})
288
+
289
+ def _configure_apikey_auth(self, http: Http) -> None:
290
+ """
291
+ Configure API key authentication.
292
+
293
+ Updates the session headers with the configured API key name/value.
294
+
295
+ Args:
296
+ http: The Http client instance to configure.
297
+
298
+ Raises:
299
+ ValueError: If API key name or value is missing.
300
+ """
301
+ if not self.typed_properties.api_key_name:
302
+ raise ValueError("API key name is missing in the linked service")
303
+ if not self.typed_properties.api_key_value:
304
+ raise ValueError("API key value is missing in the linked service")
305
+ http.session.headers.update({self.typed_properties.api_key_name: self.typed_properties.api_key_value})
306
+
307
+ def _configure_custom_auth(self, http: Http) -> None:
308
+ """
309
+ Configure custom authentication.
310
+
311
+ Calls the configured token endpoint and extracts an access token from the
312
+ JSON response using common token key names. The resulting token is stored
313
+ in the session Authorization header.
314
+
315
+ Args:
316
+ http: The Http client instance to configure.
317
+
318
+ Raises:
319
+ ValueError: If token endpoint is missing or the token cannot be found.
320
+ """
321
+ if not self.typed_properties.token_endpoint:
322
+ raise ValueError("Token endpoint is missing in the linked service properties")
323
+ response = http.post(
324
+ url=self.typed_properties.token_endpoint,
325
+ headers=self.typed_properties.headers,
326
+ json=self.typed_properties.data,
327
+ timeout=30,
328
+ )
329
+
330
+ access_token = find_keys_in_json(
331
+ response.json(),
332
+ {
333
+ "access_token",
334
+ "accessToken",
335
+ "token",
336
+ },
337
+ )
338
+ if not access_token:
339
+ raise ValueError("Access token is missing in the response from the token endpoint")
340
+ http.session.headers.update({"Authorization": f"Bearer {access_token}"})
341
+
342
+ def _configure_noauth(self, _http: Http) -> None:
343
+ """
344
+ Configure no authentication.
345
+
346
+ This is a no-op handler used to keep the auth dispatch table fully typed.
347
+
348
+ Args:
349
+ _http: The Http client instance to configure.
350
+ """
351
+
352
+ return
353
+
354
+ def connect(self) -> Http:
355
+ """
356
+ Connect to the REST API and configure authentication.
357
+
358
+ Returns:
359
+ Http: The Http client instance with authentication configured.
360
+ """
361
+ if self._http is None:
362
+ raise RuntimeError("Http instance not initialized. This should not happen.")
363
+
364
+ if self._auth_configured:
365
+ return self._http
366
+
367
+ handlers = {
368
+ "Bearer": self._configure_bearer_auth,
369
+ "OAuth2": self._configure_oauth2_auth,
370
+ "Basic": self._configure_basic_auth,
371
+ "APIKey": self._configure_apikey_auth,
372
+ "Custom": self._configure_custom_auth,
373
+ "NoAuth": self._configure_noauth,
374
+ }
375
+
376
+ try:
377
+ handlers[self.typed_properties.auth_type](self._http)
378
+ except KeyError as exc:
379
+ raise ValueError(f"Unsupported auth_type: {self.typed_properties.auth_type}") from exc
380
+
381
+ if self.typed_properties.headers:
382
+ self._http.session.headers.update(self.typed_properties.headers)
383
+
384
+ self._auth_configured = True
385
+ return self._http
386
+
387
+ def test_connection(self) -> tuple[bool, str]:
388
+ """
389
+ Test the connection to the HTTP API.
390
+
391
+ Returns:
392
+ tuple[bool, str]: A tuple containing a boolean indicating success and a string message.
393
+ """
394
+ try:
395
+ http = self.connect()
396
+ http.get(self.base_uri)
397
+ return True, "Connection successfully tested"
398
+ except Exception as exc:
399
+ return False, str(exc)
File without changes