1
1
# coding: utf-8
2
2
3
- # © Copyright IBM Corporation 2020, 2022 .
3
+ # © Copyright IBM Corporation 2020, 2024 .
4
4
#
5
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
6
# you may not use this file except in compliance with the License.
15
15
# limitations under the License.
16
16
"""
17
17
Module to patch sdk core base service for session authentication
18
+ and other helpful features.
18
19
"""
19
20
from collections import namedtuple
20
21
from typing import Dict , Optional , Union , Tuple , List
21
22
from urllib .parse import urlsplit , unquote
23
+ from json import dumps
24
+ from json .decoder import JSONDecodeError
25
+ from io import BytesIO
22
26
23
27
from ibm_cloud_sdk_core .authenticators import Authenticator
28
+ from requests import Response , Session
24
29
from requests .cookies import RequestsCookieJar
25
30
26
31
from .common import get_sdk_headers
@@ -67,12 +72,10 @@ def __hash__(self):
67
72
# Since Py3.6 dict is ordered so use a key only dict for our set
68
73
rules_by_operation .setdefault (operation_id , dict ()).setdefault (rule )
69
74
70
-
71
- old_init = CloudantV1 .__init__
72
-
75
+ _old_init = CloudantV1 .__init__
73
76
74
77
def new_init (self , authenticator : Authenticator = None ):
75
- old_init (self , authenticator )
78
+ _old_init (self , authenticator )
76
79
# Overwrite default read timeout to 2.5 minutes
77
80
if not ('timeout' in self .http_config ):
78
81
new_http_config = self .http_config .copy ()
@@ -83,25 +86,22 @@ def new_init(self, authenticator: Authenticator = None):
83
86
# Replacing BaseService's http.cookiejar.CookieJar as RequestsCookieJar supports update(CookieJar)
84
87
self .jar = RequestsCookieJar (self .jar )
85
88
self .authenticator .set_jar (self .jar ) # Authenticators don't have access to cookie jars by default
89
+ add_hooks (self )
86
90
87
-
88
- old_set_service_url = CloudantV1 .set_service_url
89
-
91
+ _old_set_service_url = CloudantV1 .set_service_url
90
92
91
93
def new_set_service_url (self , service_url : str ):
92
- old_set_service_url (self , service_url )
94
+ _old_set_service_url (self , service_url )
93
95
try :
94
96
if isinstance (self .authenticator , CouchDbSessionAuthenticator ):
95
97
self .authenticator .token_manager .set_service_url (service_url )
96
98
except AttributeError :
97
99
pass # in case no authenticator is configured yet, pass
98
100
99
-
100
- old_set_default_headers = CloudantV1 .set_default_headers
101
-
101
+ _old_set_default_headers = CloudantV1 .set_default_headers
102
102
103
103
def new_set_default_headers (self , headers : Dict [str , str ]):
104
- old_set_default_headers (self , headers )
104
+ _old_set_default_headers (self , headers )
105
105
if isinstance (self .authenticator , CouchDbSessionAuthenticator ):
106
106
combined_headers = {}
107
107
combined_headers .update (headers )
@@ -112,18 +112,82 @@ def new_set_default_headers(self, headers: Dict[str, str]):
112
112
)
113
113
self .authenticator .token_manager .set_default_headers (combined_headers )
114
114
115
+ _old_set_disable_ssl_verification = CloudantV1 .set_disable_ssl_verification
115
116
116
- old_set_disable_ssl_verification = CloudantV1 . set_disable_ssl_verification
117
-
118
-
117
+ # Note this is currently unused, but probably should be enabled.
118
+ # To enable it we need to resolve whether CouchDbSessionAuthenticator
119
+ # should ever be allowed to have a different value from the service client.
119
120
def new_set_disable_ssl_verification (self , status : bool = False ) -> None :
120
- old_set_disable_ssl_verification (self , status )
121
+ _old_set_disable_ssl_verification (self , status )
121
122
if isinstance (self .authenticator , CouchDbSessionAuthenticator ):
122
123
self .authenticator .token_manager .set_disable_ssl_verification (status )
123
124
124
-
125
- old_prepare_request = CloudantV1 .prepare_request
126
-
125
+ def _error_response_hook (response :Response , * args , ** kwargs ) -> Optional [Response ]:
126
+ # pylint: disable=W0613
127
+ # unused args and kwargs required by requests event hook interface
128
+ """Function for augmenting error responses.
129
+ Converts the Cloudant response to better match the
130
+ standard error response formats including adding a
131
+ trace ID and appending the Cloudant/CouchDB error
132
+ reason to the message.
133
+
134
+ Follows the requests event hook pattern.
135
+
136
+ :param response: the requests Response object
137
+ :type response: Response
138
+
139
+ :return: A new response object, defaults to the existing response
140
+ :rtype: Response,optional
141
+ """
142
+ # Only hook into error responses
143
+ # Ignore HEAD request responses because there is no body to read
144
+ if not response .ok and response .request .method != 'HEAD' :
145
+ content_type = response .headers .get ('content-type' )
146
+ # If it isn't JSON don't mess with it!
147
+ if content_type is not None and content_type .startswith ('application/json' ):
148
+ try :
149
+ error_json : dict = response .json ()
150
+ # Only augment if there isn't a trace or errors already
151
+ send_augmented_response = False
152
+ if 'trace' not in error_json :
153
+ if 'errors' not in error_json :
154
+ error = error_json .get ('error' )
155
+ reason = error_json .get ('reason' )
156
+ if error is not None :
157
+ error_model : dict = {'code' : error , 'message' : f'{ error } ' }
158
+ if reason :
159
+ error_model ['message' ] += f': { reason } '
160
+ error_json ['errors' ] = [error_model ]
161
+ send_augmented_response = True
162
+ if 'errors' in error_json :
163
+ trace = response .headers .get ('x-couch-request-id' )
164
+ if trace is not None :
165
+ # Augment trace if there was a value
166
+ error_json ['trace' ] = trace
167
+ send_augmented_response = True
168
+ if send_augmented_response :
169
+ # It'd be nice to just change content on response, but it's internal.
170
+ # Instead copy the named attributes to a new Response and then set
171
+ # the encoding and bytes of the modified error body.
172
+ error_response = Response ()
173
+ error_response .status_code = response .status_code
174
+ error_response .headers = response .headers
175
+ error_response .url = response .url
176
+ error_response .history = response .history
177
+ error_response .reason = response .reason
178
+ error_response .cookies = response .cookies
179
+ error_response .elapsed = response .elapsed
180
+ error_response .request = response .request
181
+ error_response .encoding = 'utf-8'
182
+ error_response .raw = BytesIO (dumps (error_json ).encode ('utf-8' ))
183
+ return error_response
184
+ except JSONDecodeError :
185
+ # If we couldn't read the JSON we just return the response as-is
186
+ # so the exception can surface elsewhere.
187
+ pass
188
+ return response
189
+
190
+ _old_prepare_request = CloudantV1 .prepare_request
127
191
128
192
def new_prepare_request (self ,
129
193
method : str ,
@@ -159,4 +223,15 @@ def new_prepare_request(self,
159
223
if segment_to_validate .startswith ('_' ):
160
224
raise ValueError ('{0} {1} starts with the invalid _ character.' .format (rule .error_parameter_name ,
161
225
unquote (segment_to_validate )))
162
- return old_prepare_request (self , method , url , * args , headers = headers , params = params , data = data , files = files , ** kwargs )
226
+ return _old_prepare_request (self , method , url , * args , headers = headers , params = params , data = data , files = files , ** kwargs )
227
+
228
+ def add_hooks (self ):
229
+ response_hooks = self .get_http_client ().hooks ['response' ]
230
+ if _error_response_hook not in response_hooks :
231
+ response_hooks .append (_error_response_hook )
232
+
233
+ _old_set_http_client = CloudantV1 .set_http_client
234
+
235
+ def new_set_http_client (self , http_client : Session ) -> None :
236
+ _old_set_http_client (self , http_client )
237
+ add_hooks (self )
0 commit comments