osdu-api 1.0.3__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.
- osdu_api/__init__.py +15 -0
- osdu_api/auth/__init__.py +15 -0
- osdu_api/auth/authorization.py +174 -0
- osdu_api/auth/refresh_token.py +62 -0
- osdu_api/clients/__init__.py +15 -0
- osdu_api/clients/base_client.py +389 -0
- osdu_api/clients/crs_catalog/__init__.py +13 -0
- osdu_api/clients/crs_catalog/crs_catalog_client.py +78 -0
- osdu_api/clients/crs_conversion/__init__.py +13 -0
- osdu_api/clients/crs_conversion/crs_conversion_client.py +82 -0
- osdu_api/clients/data_workflow/__init__.py +15 -0
- osdu_api/clients/data_workflow/data_workflow_client.py +53 -0
- osdu_api/clients/data_workflow/data_workflow_scheduling_client.py +58 -0
- osdu_api/clients/dataset/__init__.py +15 -0
- osdu_api/clients/dataset/dataset_dms_client.py +90 -0
- osdu_api/clients/dataset/dataset_registry_client.py +57 -0
- osdu_api/clients/entitlements/__init__.py +15 -0
- osdu_api/clients/entitlements/entitlements_client.py +84 -0
- osdu_api/clients/ingestion_workflow/__init__.py +0 -0
- osdu_api/clients/ingestion_workflow/ingestion_workflow_client.py +79 -0
- osdu_api/clients/legal/__init__.py +15 -0
- osdu_api/clients/legal/legal_client.py +73 -0
- osdu_api/clients/partition/__init__.py +12 -0
- osdu_api/clients/partition/partition_client.py +96 -0
- osdu_api/clients/schema/__init__.py +15 -0
- osdu_api/clients/schema/schema_client.py +49 -0
- osdu_api/clients/search/__init__.py +15 -0
- osdu_api/clients/search/search_client.py +66 -0
- osdu_api/clients/storage/__init__.py +15 -0
- osdu_api/clients/storage/record_client.py +228 -0
- osdu_api/clients/storage/schema_client.py +48 -0
- osdu_api/clients/unit/__init__.py +13 -0
- osdu_api/clients/unit/unit_client.py +264 -0
- osdu_api/configuration/__init__.py +14 -0
- osdu_api/configuration/base_config_manager.py +82 -0
- osdu_api/configuration/config_manager.py +164 -0
- osdu_api/constants.py +20 -0
- osdu_api/examples/create_and_search_example.py +69 -0
- osdu_api/examples/data_workflow_examples.py +99 -0
- osdu_api/examples/dataset_example.py +8 -0
- osdu_api/examples/ingestion_workflow_examples.py +41 -0
- osdu_api/examples/legal_examples.py +31 -0
- osdu_api/examples/search_with_cursor_example.py +11 -0
- osdu_api/exceptions/__init__.py +14 -0
- osdu_api/exceptions/exceptions.py +28 -0
- osdu_api/model/__init__.py +15 -0
- osdu_api/model/acl.py +23 -0
- osdu_api/model/base.py +34 -0
- osdu_api/model/crs_catalog/__init__.py +13 -0
- osdu_api/model/crs_catalog/base_crs.py +20 -0
- osdu_api/model/crs_catalog/datum.py +21 -0
- osdu_api/model/crs_catalog/extent.py +19 -0
- osdu_api/model/crs_catalog/post_crs_catalog_coordinate_reference_system.py +63 -0
- osdu_api/model/crs_catalog/post_crs_catalog_coordinate_transformation.py +48 -0
- osdu_api/model/crs_conversion/__init__.py +13 -0
- osdu_api/model/crs_conversion/abcd_bin_grid_spatial_location.py +21 -0
- osdu_api/model/crs_conversion/as_ingested_coordinates.py +30 -0
- osdu_api/model/crs_conversion/convert_base.py +20 -0
- osdu_api/model/crs_conversion/convert_bin_grid.py +22 -0
- osdu_api/model/crs_conversion/convert_geo_json.py +25 -0
- osdu_api/model/crs_conversion/convert_points.py +25 -0
- osdu_api/model/crs_conversion/convert_trajectory.py +44 -0
- osdu_api/model/crs_conversion/feature.py +24 -0
- osdu_api/model/crs_conversion/feature_collection.py +39 -0
- osdu_api/model/crs_conversion/geometry.py +20 -0
- osdu_api/model/crs_conversion/in_bin_grid.py +36 -0
- osdu_api/model/crs_conversion/point.py +22 -0
- osdu_api/model/crs_conversion/station.py +21 -0
- osdu_api/model/data_workflow/__init__.py +13 -0
- osdu_api/model/data_workflow/get_workflow_schedules_request.py +23 -0
- osdu_api/model/data_workflow/start_workflow.py +25 -0
- osdu_api/model/data_workflow/update_status_request.py +20 -0
- osdu_api/model/data_workflow/workflow_schedule.py +27 -0
- osdu_api/model/dataset/__init__.py +13 -0
- osdu_api/model/dataset/create_dataset_registries_request.py +27 -0
- osdu_api/model/dataset/get_dataset_registry_request.py +22 -0
- osdu_api/model/entitlements/__init__.py +13 -0
- osdu_api/model/entitlements/group.py +24 -0
- osdu_api/model/entitlements/group_member.py +24 -0
- osdu_api/model/http_method.py +24 -0
- osdu_api/model/ingestion_workflow/__init__.py +0 -0
- osdu_api/model/ingestion_workflow/create_workflow_request.py +24 -0
- osdu_api/model/ingestion_workflow/trigger_workflow_request.py +22 -0
- osdu_api/model/ingestion_workflow/update_workflow_run_request.py +22 -0
- osdu_api/model/legal/__init__.py +13 -0
- osdu_api/model/legal/legal.py +40 -0
- osdu_api/model/legal/legal_compliance.py +28 -0
- osdu_api/model/legal/legal_tag.py +22 -0
- osdu_api/model/legal/legal_tag_names.py +21 -0
- osdu_api/model/legal/legal_tag_properties.py +29 -0
- osdu_api/model/legal/update_legal_tag.py +22 -0
- osdu_api/model/record.py +72 -0
- osdu_api/model/record_ancestry.py +22 -0
- osdu_api/model/search/__init__.py +15 -0
- osdu_api/model/search/aggregation_response.py +19 -0
- osdu_api/model/search/by_bounding_box.py +22 -0
- osdu_api/model/search/by_distance.py +22 -0
- osdu_api/model/search/by_geo_polygon.py +18 -0
- osdu_api/model/search/coordinate.py +20 -0
- osdu_api/model/search/query_request.py +61 -0
- osdu_api/model/search/query_response.py +21 -0
- osdu_api/model/search/sort_order.py +20 -0
- osdu_api/model/search/sort_query.py +22 -0
- osdu_api/model/search/spatial_filter.py +25 -0
- osdu_api/model/storage/__init__.py +13 -0
- osdu_api/model/storage/acl.py +23 -0
- osdu_api/model/storage/legal.py +36 -0
- osdu_api/model/storage/query_records_request.py +24 -0
- osdu_api/model/storage/record.py +84 -0
- osdu_api/model/storage/record_ancestry.py +22 -0
- osdu_api/model/storage/schema/__init__.py +13 -0
- osdu_api/model/storage/schema/schema.py +25 -0
- osdu_api/model/storage/schema/schema_attribute.py +23 -0
- osdu_api/model/unit/__init__.py +13 -0
- osdu_api/model/unit/abcd.py +30 -0
- osdu_api/model/unit/catalog.py +54 -0
- osdu_api/model/unit/catalog_last_modified.py +24 -0
- osdu_api/model/unit/connected_outer_service.py +26 -0
- osdu_api/model/unit/conversion_abcd_request.py +31 -0
- osdu_api/model/unit/conversion_result.py +33 -0
- osdu_api/model/unit/conversion_scale_offset_request.py +31 -0
- osdu_api/model/unit/map_state.py +28 -0
- osdu_api/model/unit/measurement.py +58 -0
- osdu_api/model/unit/measurement_deprecation_info.py +28 -0
- osdu_api/model/unit/measurement_essence.py +26 -0
- osdu_api/model/unit/measurement_map.py +31 -0
- osdu_api/model/unit/measurement_map_item.py +35 -0
- osdu_api/model/unit/measurement_request.py +27 -0
- osdu_api/model/unit/query_result.py +43 -0
- osdu_api/model/unit/response_entity.py +29 -0
- osdu_api/model/unit/scale_offset.py +26 -0
- osdu_api/model/unit/search_request.py +24 -0
- osdu_api/model/unit/status_code.py +88 -0
- osdu_api/model/unit/unit.py +42 -0
- osdu_api/model/unit/unit_assignment.py +30 -0
- osdu_api/model/unit/unit_deprecation_info.py +28 -0
- osdu_api/model/unit/unit_essence.py +35 -0
- osdu_api/model/unit/unit_map.py +31 -0
- osdu_api/model/unit/unit_map_item.py +35 -0
- osdu_api/model/unit/unit_request.py +27 -0
- osdu_api/model/unit/unit_system.py +47 -0
- osdu_api/model/unit/unit_system_essence.py +24 -0
- osdu_api/model/unit/unit_system_info.py +30 -0
- osdu_api/model/unit/unit_system_info_response.py +31 -0
- osdu_api/model/unit/unit_system_request.py +27 -0
- osdu_api/model/unit/version_info.py +39 -0
- osdu_api/providers/__init__.py +14 -0
- osdu_api/providers/aws/__init__.py +13 -0
- osdu_api/providers/aws/aws_blob_storage_client.py +113 -0
- osdu_api/providers/aws/aws_credentials.py +42 -0
- osdu_api/providers/aws/boto_client_factory.py +35 -0
- osdu_api/providers/aws/entitlements_client.py +47 -0
- osdu_api/providers/aws/partition_converter_aws.py +41 -0
- osdu_api/providers/aws/partition_info_aws.py +6 -0
- osdu_api/providers/aws/service_principal_util.py +94 -0
- osdu_api/providers/azure/__init__.py +13 -0
- osdu_api/providers/azure/azure_auth_enums.py +13 -0
- osdu_api/providers/azure/azure_blob_storage_client.py +130 -0
- osdu_api/providers/azure/azure_credentials.py +213 -0
- osdu_api/providers/baremetal/__init__.py +14 -0
- osdu_api/providers/baremetal/baremetal_blob_storage_client.py +197 -0
- osdu_api/providers/baremetal/baremetal_credentials.py +101 -0
- osdu_api/providers/baremetal/partition_converter_baremetal.py +50 -0
- osdu_api/providers/baremetal/partition_info_baremetal.py +21 -0
- osdu_api/providers/blob_storage.py +59 -0
- osdu_api/providers/constants.py +23 -0
- osdu_api/providers/credentials.py +55 -0
- osdu_api/providers/exceptions.py +35 -0
- osdu_api/providers/factory.py +85 -0
- osdu_api/providers/gc/__init__.py +14 -0
- osdu_api/providers/gc/gc_blob_storage_client.py +181 -0
- osdu_api/providers/gc/gc_credentials.py +188 -0
- osdu_api/providers/ibm/__init__.py +14 -0
- osdu_api/providers/ibm/ibm_blob_storage_client.py +155 -0
- osdu_api/providers/ibm/ibm_credentials.py +87 -0
- osdu_api/providers/types.py +96 -0
- osdu_api/utils/__init__.py +14 -0
- osdu_api/utils/env.py +50 -0
- osdu_api/utils/request.py +63 -0
- osdu_api/version.py +24 -0
- osdu_api-1.0.3.dist-info/METADATA +184 -0
- osdu_api-1.0.3.dist-info/RECORD +185 -0
- osdu_api-1.0.3.dist-info/WHEEL +5 -0
- osdu_api-1.0.3.dist-info/licenses/LICENSE +202 -0
- osdu_api-1.0.3.dist-info/top_level.txt +1 -0
osdu_api/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
# Copyright © 2020 Amazon Web Services
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
# Copyright 2020 EPAM Systems
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC
|
|
2
|
+
# Copyright 2020 EPAM Systems
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from functools import partial
|
|
19
|
+
from http import HTTPStatus
|
|
20
|
+
from typing import Any, Callable, Optional, Union
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from osdu_api.exceptions.exceptions import TokenRefresherNotPresentError
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TokenRefresher(ABC):
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._access_token = ""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def refresh_token(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Implement logics of refreshing token here.
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def authorize(self):
|
|
42
|
+
self._access_token = self.refresh_token()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def access_token(self) -> str:
|
|
46
|
+
return self._access_token
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def authorization_header(self) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
Must return authorization header for updating headers dict.
|
|
52
|
+
E.g. return {"Authorization": "Bearer <access_token>"}
|
|
53
|
+
"""
|
|
54
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_callable_request(obj: Union[object, None], request_function: Callable, headers: dict,
|
|
58
|
+
*args, **kwargs) -> Callable:
|
|
59
|
+
"""
|
|
60
|
+
Create send_request_with_auth function.
|
|
61
|
+
"""
|
|
62
|
+
if obj: # if wrapped function is an object's method
|
|
63
|
+
callable_request = partial(
|
|
64
|
+
request_function, obj, headers, *args, **kwargs)
|
|
65
|
+
else:
|
|
66
|
+
callable_request = partial(request_function, headers, *args, **kwargs)
|
|
67
|
+
return callable_request
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_headers_type(headers: Any):
|
|
71
|
+
if not isinstance(headers, dict):
|
|
72
|
+
logger.error(f"Got headers {headers}")
|
|
73
|
+
raise TypeError(
|
|
74
|
+
f"Request's headers type expected to be 'dict'. Got {dict}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_response_type(response: requests.Response, request_function: Callable):
|
|
78
|
+
if not isinstance(response, requests.Response):
|
|
79
|
+
logger.error(f"Function or method {request_function}"
|
|
80
|
+
f" must return values of type 'requests.Response'. "
|
|
81
|
+
f"Got {type(response)} instead")
|
|
82
|
+
raise TypeError
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_token_refresher_type(token_refresher: TokenRefresher):
|
|
86
|
+
if not isinstance(token_refresher, TokenRefresher):
|
|
87
|
+
raise TypeError(
|
|
88
|
+
f"Token refresher must be of type {TokenRefresher}. Got {type(token_refresher)}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_object_token_refresher(
|
|
93
|
+
obj: object
|
|
94
|
+
) -> TokenRefresher:
|
|
95
|
+
"""
|
|
96
|
+
Check if token refresher passed into decorator or specified in object's as 'token_refresher'
|
|
97
|
+
property.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
obj.__getattribute__("token_refresher")
|
|
101
|
+
except AttributeError:
|
|
102
|
+
raise TokenRefresherNotPresentError("Token refresher must be passed into decorator or "
|
|
103
|
+
"set as object's 'refresh_token' attribute.")
|
|
104
|
+
else:
|
|
105
|
+
token_refresher = obj.token_refresher # type: ignore
|
|
106
|
+
return token_refresher
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def send_request_with_auth_header(token_refresher: TokenRefresher, *args,
|
|
110
|
+
**kwargs) -> requests.Response:
|
|
111
|
+
"""
|
|
112
|
+
Send request with authorization token. If response status is in HTTPStatus.UNAUTHORIZED or
|
|
113
|
+
HTTPStatus.FORBIDDEN, then refresh token and send request once again.
|
|
114
|
+
"""
|
|
115
|
+
obj = kwargs.pop("obj", None)
|
|
116
|
+
request_function = kwargs.pop("request_function")
|
|
117
|
+
headers = kwargs.pop("headers")
|
|
118
|
+
_validate_headers_type(headers)
|
|
119
|
+
headers.update(token_refresher.authorization_header) # type: ignore
|
|
120
|
+
|
|
121
|
+
send_request_with_auth = make_callable_request(
|
|
122
|
+
obj, request_function, headers, *args, **kwargs)
|
|
123
|
+
response = send_request_with_auth()
|
|
124
|
+
_validate_response_type(response, request_function)
|
|
125
|
+
|
|
126
|
+
if not response.ok:
|
|
127
|
+
if response.status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
|
128
|
+
token_refresher.authorize()
|
|
129
|
+
headers.update(token_refresher.authorization_header) # type: ignore
|
|
130
|
+
send_request_with_auth = make_callable_request(obj,
|
|
131
|
+
request_function,
|
|
132
|
+
headers,
|
|
133
|
+
*args, **kwargs)
|
|
134
|
+
response = send_request_with_auth()
|
|
135
|
+
return response
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def authorize(token_refresher: Optional[TokenRefresher] = None) -> Callable:
|
|
139
|
+
"""
|
|
140
|
+
Wrap a request function and check response. If response's error status code
|
|
141
|
+
is about Authorization, refresh token and invoke this function once again.
|
|
142
|
+
Expects function:
|
|
143
|
+
If response is not ok and not about Authorization, then raises HTTPError
|
|
144
|
+
request_func(header: dict, *args, **kwargs) -> requests.Response
|
|
145
|
+
Or method:
|
|
146
|
+
request_method(self, header: dict, *args, **kwargs) -> requests.Response
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def refresh_token_wrapper(request_function: Callable) -> Callable:
|
|
150
|
+
is_method = len(request_function.__qualname__.split(".")) > 1
|
|
151
|
+
if is_method:
|
|
152
|
+
def _wrapper(obj: object, headers: dict, *args, **kwargs) -> requests.Response:
|
|
153
|
+
_token_refresher = _get_object_token_refresher(obj)
|
|
154
|
+
_validate_token_refresher_type(_token_refresher)
|
|
155
|
+
return send_request_with_auth_header(_token_refresher,
|
|
156
|
+
request_function=request_function,
|
|
157
|
+
obj=obj,
|
|
158
|
+
headers=headers,
|
|
159
|
+
*args,
|
|
160
|
+
**kwargs)
|
|
161
|
+
else:
|
|
162
|
+
def _wrapper(headers: dict, *args, **kwargs) -> requests.Response:
|
|
163
|
+
if not isinstance(token_refresher, TokenRefresher):
|
|
164
|
+
raise TokenRefresherNotPresentError(
|
|
165
|
+
"Token refresher isn't provided or has wrong type in 'authorize' decorator."
|
|
166
|
+
)
|
|
167
|
+
_validate_token_refresher_type(token_refresher)
|
|
168
|
+
return send_request_with_auth_header(token_refresher,
|
|
169
|
+
request_function=request_function,
|
|
170
|
+
headers=headers,
|
|
171
|
+
*args, **kwargs)
|
|
172
|
+
return _wrapper
|
|
173
|
+
|
|
174
|
+
return refresh_token_wrapper
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC
|
|
2
|
+
# Copyright 2023 EPAM Systems
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Auth and refresh token utility functions."""
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from tenacity import retry, stop_after_attempt
|
|
20
|
+
|
|
21
|
+
from osdu_api.auth.authorization import TokenRefresher
|
|
22
|
+
from osdu_api.providers import credentials
|
|
23
|
+
from osdu_api.providers.types import BaseCredentials
|
|
24
|
+
|
|
25
|
+
RETRIES = 3
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseTokenRefresher(TokenRefresher):
|
|
29
|
+
"""Base Token refresher, that works with Credentials and has methods to refresh access tokens"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, creds: Optional[BaseCredentials] = None):
|
|
32
|
+
super().__init__()
|
|
33
|
+
self._credentials = creds or credentials.get_credentials()
|
|
34
|
+
|
|
35
|
+
@retry(stop=stop_after_attempt(RETRIES))
|
|
36
|
+
def refresh_token(self) -> str:
|
|
37
|
+
"""Refresh the token and cache token using airflow variables.
|
|
38
|
+
|
|
39
|
+
:return: The refreshed token
|
|
40
|
+
:rtype: str
|
|
41
|
+
"""
|
|
42
|
+
self._credentials.refresh_token()
|
|
43
|
+
self._access_token = self._credentials.access_token
|
|
44
|
+
return self._access_token
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def access_token(self) -> str:
|
|
48
|
+
"""The access token.
|
|
49
|
+
|
|
50
|
+
:return: The access token
|
|
51
|
+
:rtype: str
|
|
52
|
+
"""
|
|
53
|
+
return self._access_token
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def authorization_header(self) -> dict:
|
|
57
|
+
"""Authorization header with bearer token.
|
|
58
|
+
|
|
59
|
+
:return: Auth header as dict
|
|
60
|
+
:rtype: dict
|
|
61
|
+
"""
|
|
62
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright © 2020 Amazon Web Services
|
|
2
|
+
# Copyright 2022 Google LLC
|
|
3
|
+
# Copyright 2022 EPAM Systems
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# Copyright © 2020 Amazon Web Services
|
|
2
|
+
# Copyright 2022 Google LLC
|
|
3
|
+
# Copyright 2022 EPAM Systems
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
import configparser
|
|
18
|
+
import importlib
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
import requests
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
import tenacity
|
|
27
|
+
from httpx import Response
|
|
28
|
+
|
|
29
|
+
from osdu_api.auth.authorization import authorize, TokenRefresher
|
|
30
|
+
from osdu_api.configuration.base_config_manager import BaseConfigManager
|
|
31
|
+
from osdu_api.configuration.config_manager import DefaultConfigManager
|
|
32
|
+
from osdu_api.constants import RETRY_MIN_WAIT, RETRY_MAX_WAIT, RETRY_MULTIPLIER, \
|
|
33
|
+
RETRY_STOP_AFTER_ATTEMPT
|
|
34
|
+
from osdu_api.exceptions.exceptions import MakeRequestError
|
|
35
|
+
from osdu_api.model.http_method import HttpMethod
|
|
36
|
+
from osdu_api.utils.request import response_not_ok
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaseClient:
|
|
40
|
+
"""
|
|
41
|
+
Base client that is meant to be extended by service specific clients
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
config_manager: Optional[BaseConfigManager] = None,
|
|
47
|
+
provider: Optional[str] = None,
|
|
48
|
+
data_partition_id: Optional[str] = None,
|
|
49
|
+
token_refresher: Optional[TokenRefresher] = None,
|
|
50
|
+
logger: Optional[Any] = None,
|
|
51
|
+
user_id: Optional[str] = None,
|
|
52
|
+
async_client: httpx.AsyncClient = None
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Base client gets initialized with configuration values and a bearer token
|
|
56
|
+
based on provider-specific logic
|
|
57
|
+
"""
|
|
58
|
+
self._ssl_verify = True
|
|
59
|
+
|
|
60
|
+
self._parse_config(config_manager, provider, data_partition_id)
|
|
61
|
+
self.user_id = user_id
|
|
62
|
+
|
|
63
|
+
self.unauth_retries = 0
|
|
64
|
+
if self.use_service_principal:
|
|
65
|
+
self._refresh_service_principal_token()
|
|
66
|
+
|
|
67
|
+
self.logger = logging.getLogger(__name__) if logger is None else logger
|
|
68
|
+
|
|
69
|
+
self.token_refresher = token_refresher
|
|
70
|
+
self.async_client = async_client
|
|
71
|
+
|
|
72
|
+
def _parse_config(
|
|
73
|
+
self,
|
|
74
|
+
config_manager: Optional[BaseConfigManager] = None,
|
|
75
|
+
provider: Optional[str] = None,
|
|
76
|
+
data_partition_id: Optional[str] = None
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Parse config.
|
|
80
|
+
|
|
81
|
+
:param config_manager: ConfigManager to get configs, defaults to None
|
|
82
|
+
:type config_manager: BaseConfigManager, optional
|
|
83
|
+
"""
|
|
84
|
+
if os.getenv('OSDU_API_DISABLE_SSL_VERIFICAITON') == 'iknowthemaninthemiddle':
|
|
85
|
+
self._ssl_verify = False
|
|
86
|
+
|
|
87
|
+
self.config_manager = config_manager or DefaultConfigManager()
|
|
88
|
+
|
|
89
|
+
# self.use_service_principal is used by AWS only, so other CSPs can set this attribute to False in any way.
|
|
90
|
+
try:
|
|
91
|
+
self.provider = provider or self.config_manager.get('provider', 'name')
|
|
92
|
+
self.use_service_principal = self.config_manager.getbool('environment',
|
|
93
|
+
'use_service_principal', False)
|
|
94
|
+
if self.use_service_principal:
|
|
95
|
+
self.service_principal_module_name = self.config_manager.get('provider',
|
|
96
|
+
'service_principal_module_name')
|
|
97
|
+
except configparser.Error:
|
|
98
|
+
self.use_service_principal = False
|
|
99
|
+
|
|
100
|
+
if data_partition_id is None:
|
|
101
|
+
self.data_partition_id = self.config_manager.get(
|
|
102
|
+
'environment', 'data_partition_id')
|
|
103
|
+
else:
|
|
104
|
+
self.data_partition_id = data_partition_id
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _refresh_service_principal_token(self):
|
|
108
|
+
"""
|
|
109
|
+
The path to the logic to get a valid bearer token is dynamically injected based on
|
|
110
|
+
what provider and entitlements module name is provided in the configuration yaml
|
|
111
|
+
"""
|
|
112
|
+
entitlements_client = importlib.import_module(
|
|
113
|
+
'osdu_api.providers.%s.%s' % (self.provider, self.service_principal_module_name))
|
|
114
|
+
self.service_principal_token = entitlements_client.get_service_principal_token()
|
|
115
|
+
|
|
116
|
+
def _send_request(self, method: HttpMethod, url: str, data: str, headers: dict,
|
|
117
|
+
params: dict) -> requests.Response:
|
|
118
|
+
"""
|
|
119
|
+
Send request to OSDU
|
|
120
|
+
|
|
121
|
+
:param method: HTTP method
|
|
122
|
+
:type method: HttpMethod
|
|
123
|
+
:param url: service's URL
|
|
124
|
+
:type url: str
|
|
125
|
+
:param data: request's data
|
|
126
|
+
:type data: str
|
|
127
|
+
:param headers: request's headers
|
|
128
|
+
:type headers: dict
|
|
129
|
+
:param params: params
|
|
130
|
+
:type params: dict
|
|
131
|
+
:return: response from OSDU service
|
|
132
|
+
:rtype: requests.Response
|
|
133
|
+
"""
|
|
134
|
+
if method == HttpMethod.GET:
|
|
135
|
+
response = requests.get(url=url, params=params, headers=headers,
|
|
136
|
+
verify=self._ssl_verify)
|
|
137
|
+
elif method == HttpMethod.DELETE:
|
|
138
|
+
response = requests.delete(url=url, params=params, headers=headers,
|
|
139
|
+
verify=self._ssl_verify)
|
|
140
|
+
elif method == HttpMethod.POST:
|
|
141
|
+
response = requests.post(url=url, params=params, data=data, headers=headers,
|
|
142
|
+
verify=self._ssl_verify)
|
|
143
|
+
elif method == HttpMethod.PUT:
|
|
144
|
+
response = requests.put(url=url, params=params, data=data, headers=headers,
|
|
145
|
+
verify=self._ssl_verify)
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
async def _send_async_request(self, method: HttpMethod, url: str, data, headers: dict,
|
|
149
|
+
params: dict) -> requests.Response:
|
|
150
|
+
if method == HttpMethod.GET:
|
|
151
|
+
response = await self.async_client.get(url=url, params=params, headers=headers)
|
|
152
|
+
elif method == HttpMethod.DELETE:
|
|
153
|
+
response = await self.async_client.delete(url=url, params=params, headers=headers)
|
|
154
|
+
elif method == HttpMethod.POST:
|
|
155
|
+
response = await self.async_client.post(url=url, params=params, data=data,
|
|
156
|
+
headers=headers)
|
|
157
|
+
elif method == HttpMethod.PUT:
|
|
158
|
+
response = await self.async_client.put(url=url, params=params, data=data,
|
|
159
|
+
headers=headers)
|
|
160
|
+
return response
|
|
161
|
+
|
|
162
|
+
def _send_request_with_principle_token(
|
|
163
|
+
self,
|
|
164
|
+
method: HttpMethod,
|
|
165
|
+
url: str,
|
|
166
|
+
data: str,
|
|
167
|
+
headers: dict,
|
|
168
|
+
params: dict,
|
|
169
|
+
) -> requests.Response:
|
|
170
|
+
bearer_token = self.service_principal_token
|
|
171
|
+
if bearer_token is not None and 'Bearer ' not in bearer_token:
|
|
172
|
+
bearer_token = 'Bearer ' + bearer_token
|
|
173
|
+
|
|
174
|
+
headers["Authorization"] = bearer_token
|
|
175
|
+
|
|
176
|
+
response = self._send_request(method, url, data, headers, params)
|
|
177
|
+
|
|
178
|
+
if (response.status_code == 401 or response.status_code == 403) and self.unauth_retries < 1:
|
|
179
|
+
self.unauth_retries += 1
|
|
180
|
+
self._refresh_service_principal_token()
|
|
181
|
+
self._send_request_with_principle_token(method, url, data, headers, params)
|
|
182
|
+
|
|
183
|
+
self.unauth_retries = 0
|
|
184
|
+
return response
|
|
185
|
+
|
|
186
|
+
async def _send_async_request_with_principle_token(
|
|
187
|
+
self,
|
|
188
|
+
method: HttpMethod,
|
|
189
|
+
url: str,
|
|
190
|
+
data: str,
|
|
191
|
+
headers: dict,
|
|
192
|
+
params: dict,
|
|
193
|
+
) -> requests.Response:
|
|
194
|
+
bearer_token = self.service_principal_token
|
|
195
|
+
if bearer_token is not None and 'Bearer ' not in bearer_token:
|
|
196
|
+
bearer_token = 'Bearer ' + bearer_token
|
|
197
|
+
|
|
198
|
+
headers["Authorization"] = bearer_token
|
|
199
|
+
|
|
200
|
+
response = await self._send_async_request(method, url, data, headers, params)
|
|
201
|
+
|
|
202
|
+
if (response.status_code == 401 or response.status_code == 403) and self.unauth_retries < 1:
|
|
203
|
+
self.unauth_retries += 1
|
|
204
|
+
self._refresh_service_principal_token()
|
|
205
|
+
await self._send_async_request_with_principle_token(method, url, data, headers, params)
|
|
206
|
+
|
|
207
|
+
self.unauth_retries = 0
|
|
208
|
+
return response
|
|
209
|
+
|
|
210
|
+
def _send_request_with_bearer_token(
|
|
211
|
+
self,
|
|
212
|
+
method: HttpMethod,
|
|
213
|
+
url: str,
|
|
214
|
+
data: str,
|
|
215
|
+
headers: dict,
|
|
216
|
+
params: dict,
|
|
217
|
+
bearer_token: str
|
|
218
|
+
) -> requests.Response:
|
|
219
|
+
"""
|
|
220
|
+
Send request with bearer_token provided by SDK user.
|
|
221
|
+
|
|
222
|
+
:param method: HTTP method
|
|
223
|
+
:type method: HttpMethod
|
|
224
|
+
:param url: service's URL
|
|
225
|
+
:type url: str
|
|
226
|
+
:param data: request's data
|
|
227
|
+
:type data: str
|
|
228
|
+
:param headers: request's headers
|
|
229
|
+
:type headers: dict
|
|
230
|
+
:param params: params
|
|
231
|
+
:type params: dict
|
|
232
|
+
:param bearer_token: bearer_token
|
|
233
|
+
:type params: str
|
|
234
|
+
:return: response from OSDU service
|
|
235
|
+
:rtype: requests.Response
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
if bearer_token is not None and 'Bearer ' not in bearer_token:
|
|
239
|
+
bearer_token = 'Bearer ' + bearer_token
|
|
240
|
+
headers["Authorization"] = bearer_token
|
|
241
|
+
|
|
242
|
+
response = self._send_request(method, url, data, headers, params)
|
|
243
|
+
|
|
244
|
+
if not response.ok:
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
|
|
247
|
+
return response
|
|
248
|
+
|
|
249
|
+
async def _send_async_request_with_bearer_token(
|
|
250
|
+
self,
|
|
251
|
+
method: HttpMethod,
|
|
252
|
+
url: str,
|
|
253
|
+
data: str,
|
|
254
|
+
headers: dict,
|
|
255
|
+
params: dict,
|
|
256
|
+
bearer_token: str
|
|
257
|
+
) -> Response:
|
|
258
|
+
|
|
259
|
+
if bearer_token is not None and 'Bearer ' not in bearer_token:
|
|
260
|
+
bearer_token = 'Bearer ' + bearer_token
|
|
261
|
+
headers["Authorization"] = bearer_token
|
|
262
|
+
|
|
263
|
+
response: Response = await self._send_async_request(method, url, data, headers, params)
|
|
264
|
+
|
|
265
|
+
if response.status_code != 200:
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
|
|
268
|
+
return response
|
|
269
|
+
|
|
270
|
+
@tenacity.retry(
|
|
271
|
+
stop=tenacity.stop_after_attempt(RETRY_STOP_AFTER_ATTEMPT),
|
|
272
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_MULTIPLIER, min=RETRY_MIN_WAIT,
|
|
273
|
+
max=RETRY_MAX_WAIT),
|
|
274
|
+
retry=(tenacity.retry_if_result(response_not_ok) | tenacity.retry_if_exception_type(
|
|
275
|
+
Exception)),
|
|
276
|
+
retry_error_callback=lambda retry_state: retry_state.outcome.result()
|
|
277
|
+
)
|
|
278
|
+
@authorize()
|
|
279
|
+
def _send_request_with_token_refresher(
|
|
280
|
+
self,
|
|
281
|
+
headers: dict,
|
|
282
|
+
method: HttpMethod,
|
|
283
|
+
url: str,
|
|
284
|
+
data: str,
|
|
285
|
+
params: dict
|
|
286
|
+
) -> requests.Response:
|
|
287
|
+
return self._send_request(method, url, data, headers, params)
|
|
288
|
+
|
|
289
|
+
@tenacity.retry(
|
|
290
|
+
stop=tenacity.stop_after_attempt(RETRY_STOP_AFTER_ATTEMPT),
|
|
291
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_MULTIPLIER, min=RETRY_MIN_WAIT,
|
|
292
|
+
max=RETRY_MAX_WAIT),
|
|
293
|
+
retry=(tenacity.retry_if_result(response_not_ok) | tenacity.retry_if_exception_type(
|
|
294
|
+
Exception)),
|
|
295
|
+
retry_error_callback=lambda retry_state: retry_state.outcome.result()
|
|
296
|
+
)
|
|
297
|
+
@authorize()
|
|
298
|
+
async def _send_async_request_with_token_refresher(
|
|
299
|
+
self,
|
|
300
|
+
headers: dict,
|
|
301
|
+
method: HttpMethod,
|
|
302
|
+
url: str,
|
|
303
|
+
data: str,
|
|
304
|
+
params: dict
|
|
305
|
+
) -> requests.Response:
|
|
306
|
+
return await self._send_async_request(method, url, data, headers, params)
|
|
307
|
+
|
|
308
|
+
def make_request(
|
|
309
|
+
self,
|
|
310
|
+
method: HttpMethod,
|
|
311
|
+
url: str,
|
|
312
|
+
data: Any = '',
|
|
313
|
+
add_headers: Optional[dict] = None,
|
|
314
|
+
params: Optional[dict] = None,
|
|
315
|
+
bearer_token: Optional[str] = None,
|
|
316
|
+
no_auth: bool = False
|
|
317
|
+
) -> requests.Response:
|
|
318
|
+
"""
|
|
319
|
+
Makes a request using python's built in requests library. Takes additional headers if
|
|
320
|
+
necessary
|
|
321
|
+
"""
|
|
322
|
+
add_headers = add_headers or {}
|
|
323
|
+
params = params or {}
|
|
324
|
+
|
|
325
|
+
headers = self.prepare_headers(add_headers, self.data_partition_id, self.user_id)
|
|
326
|
+
|
|
327
|
+
if no_auth:
|
|
328
|
+
response = self._send_request(method, url, data, headers, params)
|
|
329
|
+
elif bearer_token:
|
|
330
|
+
response = self._send_request_with_bearer_token(method, url, data, headers, params,
|
|
331
|
+
bearer_token)
|
|
332
|
+
elif self.token_refresher:
|
|
333
|
+
# _send_request_with_token_refresher has other method signature to work with @authorize decorator
|
|
334
|
+
response = self._send_request_with_token_refresher(headers, method, url, data, params)
|
|
335
|
+
elif self.use_service_principal:
|
|
336
|
+
response = self._send_request_with_principle_token(method, url, data, headers, params)
|
|
337
|
+
else:
|
|
338
|
+
raise MakeRequestError("There is no strategy to get Bearer token.")
|
|
339
|
+
return response
|
|
340
|
+
|
|
341
|
+
async def make_async_request(
|
|
342
|
+
self,
|
|
343
|
+
method: HttpMethod,
|
|
344
|
+
url: str,
|
|
345
|
+
data: Any = '',
|
|
346
|
+
add_headers: Optional[dict] = None,
|
|
347
|
+
params: Optional[dict] = None,
|
|
348
|
+
bearer_token: Optional[str] = None,
|
|
349
|
+
no_auth: bool = False
|
|
350
|
+
) -> requests.Response:
|
|
351
|
+
|
|
352
|
+
add_headers = add_headers or {}
|
|
353
|
+
params = params or {}
|
|
354
|
+
|
|
355
|
+
headers = self.prepare_headers(add_headers, self.data_partition_id, self.user_id)
|
|
356
|
+
|
|
357
|
+
if no_auth:
|
|
358
|
+
response = await self._send_async_request(method, url, data, headers, params)
|
|
359
|
+
elif bearer_token:
|
|
360
|
+
response = await self._send_async_request_with_bearer_token(method, url, data, headers,
|
|
361
|
+
params,
|
|
362
|
+
bearer_token)
|
|
363
|
+
elif self.token_refresher:
|
|
364
|
+
response = await self._send_async_request_with_token_refresher(headers, method, url, data,
|
|
365
|
+
params)
|
|
366
|
+
elif self.use_service_principal:
|
|
367
|
+
response = await self._send_async_request_with_principle_token(method, url, data, headers,
|
|
368
|
+
params)
|
|
369
|
+
else:
|
|
370
|
+
raise MakeRequestError("There is no strategy to get Bearer token.")
|
|
371
|
+
return response
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def prepare_headers(add_headers, data_partition, user_id):
|
|
375
|
+
headers = {
|
|
376
|
+
'content-type': 'application/json',
|
|
377
|
+
'data-partition-id': data_partition,
|
|
378
|
+
}
|
|
379
|
+
if user_id is not None:
|
|
380
|
+
# TODO: come up with the single header for all CSP
|
|
381
|
+
if os.getenv("ENTITLEMENTS_IMPERSONATION") in ("True", "true"):
|
|
382
|
+
# it's for GC and Baremetal implementations
|
|
383
|
+
headers["on-behalf-of"] = user_id
|
|
384
|
+
else:
|
|
385
|
+
# it's for Azure implementation
|
|
386
|
+
headers['x-on-behalf-of'] = user_id
|
|
387
|
+
for key, value in add_headers.items():
|
|
388
|
+
headers[key] = value
|
|
389
|
+
return headers
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2023 Geosiris
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|