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.
- ds_protocol_http_py_lib/__init__.py +30 -0
- ds_protocol_http_py_lib/dataset/__init__.py +33 -0
- ds_protocol_http_py_lib/dataset/http.py +177 -0
- ds_protocol_http_py_lib/enums.py +23 -0
- ds_protocol_http_py_lib/linked_service/__init__.py +27 -0
- ds_protocol_http_py_lib/linked_service/http.py +399 -0
- ds_protocol_http_py_lib/py.typed +0 -0
- ds_protocol_http_py_lib/resource.yaml +17 -0
- ds_protocol_http_py_lib/utils/__init__.py +7 -0
- ds_protocol_http_py_lib/utils/http/__init__.py +20 -0
- ds_protocol_http_py_lib/utils/http/config.py +64 -0
- ds_protocol_http_py_lib/utils/http/provider.py +234 -0
- ds_protocol_http_py_lib/utils/http/token_bucket.py +101 -0
- ds_protocol_http_py_lib/utils/json_utils.py +47 -0
- ds_protocol_http_py_lib-0.1.0a1.dist-info/METADATA +128 -0
- ds_protocol_http_py_lib-0.1.0a1.dist-info/RECORD +19 -0
- ds_protocol_http_py_lib-0.1.0a1.dist-info/WHEEL +5 -0
- ds_protocol_http_py_lib-0.1.0a1.dist-info/licenses/LICENSE-APACHE +201 -0
- ds_protocol_http_py_lib-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -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
|