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.
Files changed (185) hide show
  1. osdu_api/__init__.py +15 -0
  2. osdu_api/auth/__init__.py +15 -0
  3. osdu_api/auth/authorization.py +174 -0
  4. osdu_api/auth/refresh_token.py +62 -0
  5. osdu_api/clients/__init__.py +15 -0
  6. osdu_api/clients/base_client.py +389 -0
  7. osdu_api/clients/crs_catalog/__init__.py +13 -0
  8. osdu_api/clients/crs_catalog/crs_catalog_client.py +78 -0
  9. osdu_api/clients/crs_conversion/__init__.py +13 -0
  10. osdu_api/clients/crs_conversion/crs_conversion_client.py +82 -0
  11. osdu_api/clients/data_workflow/__init__.py +15 -0
  12. osdu_api/clients/data_workflow/data_workflow_client.py +53 -0
  13. osdu_api/clients/data_workflow/data_workflow_scheduling_client.py +58 -0
  14. osdu_api/clients/dataset/__init__.py +15 -0
  15. osdu_api/clients/dataset/dataset_dms_client.py +90 -0
  16. osdu_api/clients/dataset/dataset_registry_client.py +57 -0
  17. osdu_api/clients/entitlements/__init__.py +15 -0
  18. osdu_api/clients/entitlements/entitlements_client.py +84 -0
  19. osdu_api/clients/ingestion_workflow/__init__.py +0 -0
  20. osdu_api/clients/ingestion_workflow/ingestion_workflow_client.py +79 -0
  21. osdu_api/clients/legal/__init__.py +15 -0
  22. osdu_api/clients/legal/legal_client.py +73 -0
  23. osdu_api/clients/partition/__init__.py +12 -0
  24. osdu_api/clients/partition/partition_client.py +96 -0
  25. osdu_api/clients/schema/__init__.py +15 -0
  26. osdu_api/clients/schema/schema_client.py +49 -0
  27. osdu_api/clients/search/__init__.py +15 -0
  28. osdu_api/clients/search/search_client.py +66 -0
  29. osdu_api/clients/storage/__init__.py +15 -0
  30. osdu_api/clients/storage/record_client.py +228 -0
  31. osdu_api/clients/storage/schema_client.py +48 -0
  32. osdu_api/clients/unit/__init__.py +13 -0
  33. osdu_api/clients/unit/unit_client.py +264 -0
  34. osdu_api/configuration/__init__.py +14 -0
  35. osdu_api/configuration/base_config_manager.py +82 -0
  36. osdu_api/configuration/config_manager.py +164 -0
  37. osdu_api/constants.py +20 -0
  38. osdu_api/examples/create_and_search_example.py +69 -0
  39. osdu_api/examples/data_workflow_examples.py +99 -0
  40. osdu_api/examples/dataset_example.py +8 -0
  41. osdu_api/examples/ingestion_workflow_examples.py +41 -0
  42. osdu_api/examples/legal_examples.py +31 -0
  43. osdu_api/examples/search_with_cursor_example.py +11 -0
  44. osdu_api/exceptions/__init__.py +14 -0
  45. osdu_api/exceptions/exceptions.py +28 -0
  46. osdu_api/model/__init__.py +15 -0
  47. osdu_api/model/acl.py +23 -0
  48. osdu_api/model/base.py +34 -0
  49. osdu_api/model/crs_catalog/__init__.py +13 -0
  50. osdu_api/model/crs_catalog/base_crs.py +20 -0
  51. osdu_api/model/crs_catalog/datum.py +21 -0
  52. osdu_api/model/crs_catalog/extent.py +19 -0
  53. osdu_api/model/crs_catalog/post_crs_catalog_coordinate_reference_system.py +63 -0
  54. osdu_api/model/crs_catalog/post_crs_catalog_coordinate_transformation.py +48 -0
  55. osdu_api/model/crs_conversion/__init__.py +13 -0
  56. osdu_api/model/crs_conversion/abcd_bin_grid_spatial_location.py +21 -0
  57. osdu_api/model/crs_conversion/as_ingested_coordinates.py +30 -0
  58. osdu_api/model/crs_conversion/convert_base.py +20 -0
  59. osdu_api/model/crs_conversion/convert_bin_grid.py +22 -0
  60. osdu_api/model/crs_conversion/convert_geo_json.py +25 -0
  61. osdu_api/model/crs_conversion/convert_points.py +25 -0
  62. osdu_api/model/crs_conversion/convert_trajectory.py +44 -0
  63. osdu_api/model/crs_conversion/feature.py +24 -0
  64. osdu_api/model/crs_conversion/feature_collection.py +39 -0
  65. osdu_api/model/crs_conversion/geometry.py +20 -0
  66. osdu_api/model/crs_conversion/in_bin_grid.py +36 -0
  67. osdu_api/model/crs_conversion/point.py +22 -0
  68. osdu_api/model/crs_conversion/station.py +21 -0
  69. osdu_api/model/data_workflow/__init__.py +13 -0
  70. osdu_api/model/data_workflow/get_workflow_schedules_request.py +23 -0
  71. osdu_api/model/data_workflow/start_workflow.py +25 -0
  72. osdu_api/model/data_workflow/update_status_request.py +20 -0
  73. osdu_api/model/data_workflow/workflow_schedule.py +27 -0
  74. osdu_api/model/dataset/__init__.py +13 -0
  75. osdu_api/model/dataset/create_dataset_registries_request.py +27 -0
  76. osdu_api/model/dataset/get_dataset_registry_request.py +22 -0
  77. osdu_api/model/entitlements/__init__.py +13 -0
  78. osdu_api/model/entitlements/group.py +24 -0
  79. osdu_api/model/entitlements/group_member.py +24 -0
  80. osdu_api/model/http_method.py +24 -0
  81. osdu_api/model/ingestion_workflow/__init__.py +0 -0
  82. osdu_api/model/ingestion_workflow/create_workflow_request.py +24 -0
  83. osdu_api/model/ingestion_workflow/trigger_workflow_request.py +22 -0
  84. osdu_api/model/ingestion_workflow/update_workflow_run_request.py +22 -0
  85. osdu_api/model/legal/__init__.py +13 -0
  86. osdu_api/model/legal/legal.py +40 -0
  87. osdu_api/model/legal/legal_compliance.py +28 -0
  88. osdu_api/model/legal/legal_tag.py +22 -0
  89. osdu_api/model/legal/legal_tag_names.py +21 -0
  90. osdu_api/model/legal/legal_tag_properties.py +29 -0
  91. osdu_api/model/legal/update_legal_tag.py +22 -0
  92. osdu_api/model/record.py +72 -0
  93. osdu_api/model/record_ancestry.py +22 -0
  94. osdu_api/model/search/__init__.py +15 -0
  95. osdu_api/model/search/aggregation_response.py +19 -0
  96. osdu_api/model/search/by_bounding_box.py +22 -0
  97. osdu_api/model/search/by_distance.py +22 -0
  98. osdu_api/model/search/by_geo_polygon.py +18 -0
  99. osdu_api/model/search/coordinate.py +20 -0
  100. osdu_api/model/search/query_request.py +61 -0
  101. osdu_api/model/search/query_response.py +21 -0
  102. osdu_api/model/search/sort_order.py +20 -0
  103. osdu_api/model/search/sort_query.py +22 -0
  104. osdu_api/model/search/spatial_filter.py +25 -0
  105. osdu_api/model/storage/__init__.py +13 -0
  106. osdu_api/model/storage/acl.py +23 -0
  107. osdu_api/model/storage/legal.py +36 -0
  108. osdu_api/model/storage/query_records_request.py +24 -0
  109. osdu_api/model/storage/record.py +84 -0
  110. osdu_api/model/storage/record_ancestry.py +22 -0
  111. osdu_api/model/storage/schema/__init__.py +13 -0
  112. osdu_api/model/storage/schema/schema.py +25 -0
  113. osdu_api/model/storage/schema/schema_attribute.py +23 -0
  114. osdu_api/model/unit/__init__.py +13 -0
  115. osdu_api/model/unit/abcd.py +30 -0
  116. osdu_api/model/unit/catalog.py +54 -0
  117. osdu_api/model/unit/catalog_last_modified.py +24 -0
  118. osdu_api/model/unit/connected_outer_service.py +26 -0
  119. osdu_api/model/unit/conversion_abcd_request.py +31 -0
  120. osdu_api/model/unit/conversion_result.py +33 -0
  121. osdu_api/model/unit/conversion_scale_offset_request.py +31 -0
  122. osdu_api/model/unit/map_state.py +28 -0
  123. osdu_api/model/unit/measurement.py +58 -0
  124. osdu_api/model/unit/measurement_deprecation_info.py +28 -0
  125. osdu_api/model/unit/measurement_essence.py +26 -0
  126. osdu_api/model/unit/measurement_map.py +31 -0
  127. osdu_api/model/unit/measurement_map_item.py +35 -0
  128. osdu_api/model/unit/measurement_request.py +27 -0
  129. osdu_api/model/unit/query_result.py +43 -0
  130. osdu_api/model/unit/response_entity.py +29 -0
  131. osdu_api/model/unit/scale_offset.py +26 -0
  132. osdu_api/model/unit/search_request.py +24 -0
  133. osdu_api/model/unit/status_code.py +88 -0
  134. osdu_api/model/unit/unit.py +42 -0
  135. osdu_api/model/unit/unit_assignment.py +30 -0
  136. osdu_api/model/unit/unit_deprecation_info.py +28 -0
  137. osdu_api/model/unit/unit_essence.py +35 -0
  138. osdu_api/model/unit/unit_map.py +31 -0
  139. osdu_api/model/unit/unit_map_item.py +35 -0
  140. osdu_api/model/unit/unit_request.py +27 -0
  141. osdu_api/model/unit/unit_system.py +47 -0
  142. osdu_api/model/unit/unit_system_essence.py +24 -0
  143. osdu_api/model/unit/unit_system_info.py +30 -0
  144. osdu_api/model/unit/unit_system_info_response.py +31 -0
  145. osdu_api/model/unit/unit_system_request.py +27 -0
  146. osdu_api/model/unit/version_info.py +39 -0
  147. osdu_api/providers/__init__.py +14 -0
  148. osdu_api/providers/aws/__init__.py +13 -0
  149. osdu_api/providers/aws/aws_blob_storage_client.py +113 -0
  150. osdu_api/providers/aws/aws_credentials.py +42 -0
  151. osdu_api/providers/aws/boto_client_factory.py +35 -0
  152. osdu_api/providers/aws/entitlements_client.py +47 -0
  153. osdu_api/providers/aws/partition_converter_aws.py +41 -0
  154. osdu_api/providers/aws/partition_info_aws.py +6 -0
  155. osdu_api/providers/aws/service_principal_util.py +94 -0
  156. osdu_api/providers/azure/__init__.py +13 -0
  157. osdu_api/providers/azure/azure_auth_enums.py +13 -0
  158. osdu_api/providers/azure/azure_blob_storage_client.py +130 -0
  159. osdu_api/providers/azure/azure_credentials.py +213 -0
  160. osdu_api/providers/baremetal/__init__.py +14 -0
  161. osdu_api/providers/baremetal/baremetal_blob_storage_client.py +197 -0
  162. osdu_api/providers/baremetal/baremetal_credentials.py +101 -0
  163. osdu_api/providers/baremetal/partition_converter_baremetal.py +50 -0
  164. osdu_api/providers/baremetal/partition_info_baremetal.py +21 -0
  165. osdu_api/providers/blob_storage.py +59 -0
  166. osdu_api/providers/constants.py +23 -0
  167. osdu_api/providers/credentials.py +55 -0
  168. osdu_api/providers/exceptions.py +35 -0
  169. osdu_api/providers/factory.py +85 -0
  170. osdu_api/providers/gc/__init__.py +14 -0
  171. osdu_api/providers/gc/gc_blob_storage_client.py +181 -0
  172. osdu_api/providers/gc/gc_credentials.py +188 -0
  173. osdu_api/providers/ibm/__init__.py +14 -0
  174. osdu_api/providers/ibm/ibm_blob_storage_client.py +155 -0
  175. osdu_api/providers/ibm/ibm_credentials.py +87 -0
  176. osdu_api/providers/types.py +96 -0
  177. osdu_api/utils/__init__.py +14 -0
  178. osdu_api/utils/env.py +50 -0
  179. osdu_api/utils/request.py +63 -0
  180. osdu_api/version.py +24 -0
  181. osdu_api-1.0.3.dist-info/METADATA +184 -0
  182. osdu_api-1.0.3.dist-info/RECORD +185 -0
  183. osdu_api-1.0.3.dist-info/WHEEL +5 -0
  184. osdu_api-1.0.3.dist-info/licenses/LICENSE +202 -0
  185. 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.