hyperlake 0.1.1__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.
hyperlake/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ from . import auth
13
+ from . import client
14
+ from . import constants
15
+ from . import dbapi
16
+ from . import exceptions
17
+ from . import logging
18
+ from ._version import __author__
19
+ from ._version import __author_email__
20
+ from ._version import __description__
21
+ from ._version import __license__
22
+ from ._version import __title__
23
+ from ._version import __url__
24
+ from ._version import __version__
25
+
26
+ __all__ = [
27
+ "auth",
28
+ "client",
29
+ "constants",
30
+ "dbapi",
31
+ "exceptions",
32
+ "logging",
33
+ "__author__",
34
+ "__author_email__",
35
+ "__description__",
36
+ "__license__",
37
+ "__title__",
38
+ "__url__",
39
+ "__version__",
40
+ ]
hyperlake/_version.py ADDED
@@ -0,0 +1,7 @@
1
+ __title__ = "hyperlake"
2
+ __description__ = "Python Client for the Hyperlake distributed SQL Engine"
3
+ __url__ = "https://github.com/ockhamlabs/hyperlake-python-client"
4
+ __version__ = "0.1.1"
5
+ __author__ = "Ockham Labs Team"
6
+ __author_email__ = "pythonclient+hello@ockhamlabs.ai"
7
+ __license__ = "Apache 2.0"
hyperlake/auth.py ADDED
@@ -0,0 +1,609 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ import abc
13
+ import importlib
14
+ import json
15
+ import os
16
+ import re
17
+ import threading
18
+ import webbrowser
19
+ from collections.abc import Mapping
20
+ from typing import Any
21
+ from typing import Callable
22
+ from typing import Dict
23
+ from typing import List
24
+ from typing import Optional
25
+ from typing import Tuple
26
+ from urllib.parse import urlparse
27
+
28
+ from requests import PreparedRequest
29
+ from requests import Request
30
+ from requests import Response
31
+ from requests import Session
32
+ from requests.auth import AuthBase
33
+ from requests.auth import extract_cookies_to_jar
34
+
35
+ import hyperlake.logging
36
+ from hyperlake import exceptions
37
+ from hyperlake.constants import HEADER_USER
38
+ from hyperlake.constants import MAX_NT_PASSWORD_SIZE
39
+
40
+ logger = hyperlake.logging.get_logger(__name__)
41
+
42
+
43
+ class Authentication(metaclass=abc.ABCMeta):
44
+ @abc.abstractmethod
45
+ def set_http_session(self, http_session: Session) -> Session:
46
+ pass
47
+
48
+ def get_exceptions(self) -> Tuple[Any, ...]:
49
+ return tuple()
50
+
51
+
52
+ class KerberosAuthentication(Authentication):
53
+ def __init__(
54
+ self,
55
+ config: Optional[str] = None,
56
+ service_name: Optional[str] = None,
57
+ mutual_authentication: bool = False,
58
+ force_preemptive: bool = False,
59
+ hostname_override: Optional[str] = None,
60
+ sanitize_mutual_error_response: bool = True,
61
+ principal: Optional[str] = None,
62
+ delegate: bool = False,
63
+ ca_bundle: Optional[str] = None,
64
+ ) -> None:
65
+ self._config = config
66
+ self._service_name = service_name
67
+ self._mutual_authentication = mutual_authentication
68
+ self._force_preemptive = force_preemptive
69
+ self._hostname_override = hostname_override
70
+ self._sanitize_mutual_error_response = sanitize_mutual_error_response
71
+ self._principal = principal
72
+ self._delegate = delegate
73
+ self._ca_bundle = ca_bundle
74
+
75
+ def set_http_session(self, http_session: Session) -> Session:
76
+ try:
77
+ import requests_kerberos
78
+ except ImportError:
79
+ raise RuntimeError("unable to import requests_kerberos")
80
+
81
+ if self._config:
82
+ os.environ["KRB5_CONFIG"] = self._config
83
+ http_session.trust_env = False
84
+ http_session.auth = requests_kerberos.HTTPKerberosAuth(
85
+ mutual_authentication=self._mutual_authentication,
86
+ force_preemptive=self._force_preemptive,
87
+ hostname_override=self._hostname_override,
88
+ sanitize_mutual_error_response=self._sanitize_mutual_error_response,
89
+ principal=self._principal,
90
+ delegate=self._delegate,
91
+ service=self._service_name,
92
+ )
93
+ if self._ca_bundle:
94
+ http_session.verify = self._ca_bundle
95
+ return http_session
96
+
97
+ def get_exceptions(self) -> Tuple[Any, ...]:
98
+ try:
99
+ from requests_kerberos.exceptions import KerberosExchangeError
100
+
101
+ return KerberosExchangeError,
102
+ except ImportError:
103
+ raise RuntimeError("unable to import requests_kerberos")
104
+
105
+ def __eq__(self, other: object) -> bool:
106
+ if not isinstance(other, KerberosAuthentication):
107
+ return False
108
+ return (self._config == other._config
109
+ and self._service_name == other._service_name
110
+ and self._mutual_authentication == other._mutual_authentication
111
+ and self._force_preemptive == other._force_preemptive
112
+ and self._hostname_override == other._hostname_override
113
+ and self._sanitize_mutual_error_response == other._sanitize_mutual_error_response
114
+ and self._principal == other._principal
115
+ and self._delegate == other._delegate
116
+ and self._ca_bundle == other._ca_bundle)
117
+
118
+
119
+ class GSSAPIAuthentication(Authentication):
120
+ def __init__(
121
+ self,
122
+ config: Optional[str] = None,
123
+ service_name: Optional[str] = None,
124
+ mutual_authentication: bool = False,
125
+ force_preemptive: bool = False,
126
+ hostname_override: Optional[str] = None,
127
+ sanitize_mutual_error_response: bool = True,
128
+ principal: Optional[str] = None,
129
+ delegate: bool = False,
130
+ ca_bundle: Optional[str] = None,
131
+ ) -> None:
132
+ self._config = config
133
+ self._service_name = service_name
134
+ self._mutual_authentication = mutual_authentication
135
+ self._force_preemptive = force_preemptive
136
+ self._hostname_override = hostname_override
137
+ self._sanitize_mutual_error_response = sanitize_mutual_error_response
138
+ self._principal = principal
139
+ self._delegate = delegate
140
+ self._ca_bundle = ca_bundle
141
+
142
+ def set_http_session(self, http_session: Session) -> Session:
143
+ try:
144
+ import requests_gssapi
145
+ except ImportError:
146
+ raise RuntimeError("unable to import requests_gssapi")
147
+
148
+ if self._config:
149
+ os.environ["KRB5_CONFIG"] = self._config
150
+ http_session.trust_env = False
151
+ http_session.auth = requests_gssapi.HTTPSPNEGOAuth(
152
+ mutual_authentication=self._mutual_authentication,
153
+ opportunistic_auth=self._force_preemptive,
154
+ target_name=self._get_target_name(self._hostname_override, self._service_name),
155
+ sanitize_mutual_error_response=self._sanitize_mutual_error_response,
156
+ creds=self._get_credentials(self._principal),
157
+ delegate=self._delegate,
158
+ )
159
+ if self._ca_bundle:
160
+ http_session.verify = self._ca_bundle
161
+ return http_session
162
+
163
+ def _get_credentials(self, principal: Optional[str] = None) -> Any:
164
+ if principal:
165
+ try:
166
+ import gssapi
167
+ except ImportError:
168
+ raise RuntimeError("unable to import gssapi")
169
+
170
+ name = gssapi.Name(principal, gssapi.NameType.user)
171
+ return gssapi.Credentials(name=name, usage="initiate")
172
+
173
+ return None
174
+
175
+ def _get_target_name(
176
+ self,
177
+ hostname_override: Optional[str] = None,
178
+ service_name: Optional[str] = None,
179
+ ) -> Any:
180
+ if service_name is not None:
181
+ try:
182
+ import gssapi
183
+ except ImportError:
184
+ raise RuntimeError("unable to import gssapi")
185
+
186
+ if hostname_override is None:
187
+ raise ValueError("service name must be used together with hostname_override")
188
+
189
+ kerb_spn = "{0}@{1}".format(service_name, hostname_override)
190
+ return gssapi.Name(kerb_spn, gssapi.NameType.hostbased_service)
191
+
192
+ return hostname_override
193
+
194
+ def get_exceptions(self) -> Tuple[Any, ...]:
195
+ try:
196
+ from requests_gssapi.exceptions import SPNEGOExchangeError
197
+
198
+ return SPNEGOExchangeError,
199
+ except ImportError:
200
+ raise RuntimeError("unable to import requests_kerberos")
201
+
202
+ def __eq__(self, other: object) -> bool:
203
+ if not isinstance(other, GSSAPIAuthentication):
204
+ return False
205
+ return (self._config == other._config
206
+ and self._service_name == other._service_name
207
+ and self._mutual_authentication == other._mutual_authentication
208
+ and self._force_preemptive == other._force_preemptive
209
+ and self._hostname_override == other._hostname_override
210
+ and self._sanitize_mutual_error_response == other._sanitize_mutual_error_response
211
+ and self._principal == other._principal
212
+ and self._delegate == other._delegate
213
+ and self._ca_bundle == other._ca_bundle)
214
+
215
+
216
+ class BasicAuthentication(Authentication):
217
+ def __init__(self, username: str, password: str):
218
+ self._username = username
219
+ self._password = password
220
+
221
+ def set_http_session(self, http_session: Session) -> Session:
222
+ try:
223
+ import requests.auth
224
+ except ImportError:
225
+ raise RuntimeError("unable to import requests.auth")
226
+
227
+ http_session.auth = requests.auth.HTTPBasicAuth(self._username, self._password)
228
+ return http_session
229
+
230
+ def get_exceptions(self) -> Tuple[Any, ...]:
231
+ return ()
232
+
233
+ def __eq__(self, other: object) -> bool:
234
+ if not isinstance(other, BasicAuthentication):
235
+ return False
236
+ return self._username == other._username and self._password == other._password
237
+
238
+
239
+ class _BearerAuth(AuthBase):
240
+ """
241
+ Custom implementation of Authentication class for bearer token
242
+ """
243
+
244
+ def __init__(self, token: str):
245
+ self.token = token
246
+
247
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
248
+ r.headers["Authorization"] = "Bearer " + self.token
249
+ return r
250
+
251
+
252
+ class JWTAuthentication(Authentication):
253
+
254
+ def __init__(self, token: str):
255
+ self.token = token
256
+
257
+ def set_http_session(self, http_session: Session) -> Session:
258
+ http_session.auth = _BearerAuth(self.token)
259
+ return http_session
260
+
261
+ def get_exceptions(self) -> Tuple[Any, ...]:
262
+ return ()
263
+
264
+ def __eq__(self, other: object) -> bool:
265
+ if not isinstance(other, JWTAuthentication):
266
+ return False
267
+ return self.token == other.token
268
+
269
+
270
+ class RedirectHandler(metaclass=abc.ABCMeta):
271
+ """
272
+ Abstract class for OAuth redirect handlers, inherit from this class to implement your own redirect handler.
273
+ """
274
+
275
+ @abc.abstractmethod
276
+ def __call__(self, url: str) -> None:
277
+ raise NotImplementedError()
278
+
279
+
280
+ class ConsoleRedirectHandler(RedirectHandler):
281
+ """
282
+ Handler for OAuth redirections to log to console.
283
+ """
284
+
285
+ def __call__(self, url: str) -> None:
286
+ print("Open the following URL in browser for the external authentication:")
287
+ print(url)
288
+
289
+
290
+ class WebBrowserRedirectHandler(RedirectHandler):
291
+ """
292
+ Handler for OAuth redirections to open in web browser.
293
+ """
294
+
295
+ def __call__(self, url: str) -> None:
296
+ webbrowser.open_new(url)
297
+
298
+
299
+ class CompositeRedirectHandler(RedirectHandler):
300
+ """
301
+ Composite handler for OAuth redirect handlers.
302
+ """
303
+
304
+ def __init__(self, handlers: List[Callable[[str], None]]):
305
+ self.handlers = handlers
306
+
307
+ def __call__(self, url: str) -> None:
308
+ for handler in self.handlers:
309
+ handler(url)
310
+
311
+
312
+ class _OAuth2TokenCache(metaclass=abc.ABCMeta):
313
+ """
314
+ Abstract class for OAuth token cache, inherit from this class to implement your own token cache.
315
+ """
316
+
317
+ @abc.abstractmethod
318
+ def get_token_from_cache(self, key: Optional[str]) -> Optional[str]:
319
+ pass
320
+
321
+ @abc.abstractmethod
322
+ def store_token_to_cache(self, key: Optional[str], token: str) -> None:
323
+ pass
324
+
325
+
326
+ class _OAuth2TokenInMemoryCache(_OAuth2TokenCache):
327
+ """
328
+ Multiple clients can share the same cache only if each connection explicitly specifies
329
+ a user otherwise the first cached token will be used to authenticate all other users.
330
+ """
331
+
332
+ def __init__(self) -> None:
333
+ self._cache: Dict[Optional[str], str] = {}
334
+
335
+ def get_token_from_cache(self, key: Optional[str]) -> Optional[str]:
336
+ return self._cache.get(key)
337
+
338
+ def store_token_to_cache(self, key: Optional[str], token: str) -> None:
339
+ self._cache[key] = token
340
+
341
+
342
+ class _OAuth2KeyRingTokenCache(_OAuth2TokenCache):
343
+ """
344
+ Keyring token cache implementation
345
+ """
346
+
347
+ def __init__(self) -> None:
348
+ super().__init__()
349
+ try:
350
+ self._keyring = importlib.import_module("keyring")
351
+ except ImportError:
352
+ self._keyring = None # type: ignore
353
+ logger.info("keyring module not found. OAuth2 token will not be stored in keyring.")
354
+
355
+ def is_keyring_available(self) -> bool:
356
+ return self._keyring is not None \
357
+ and not isinstance(self._keyring.get_keyring(), self._keyring.backends.fail.Keyring)
358
+
359
+ def get_token_from_cache(self, key: Optional[str]) -> Optional[str]:
360
+ password = self._keyring.get_password(key, "token")
361
+
362
+ try:
363
+ password_as_dict = json.loads(str(password))
364
+ if password_as_dict.get("sharded_password"):
365
+ # if password was stored shared, reconstruct it
366
+ shard_count = int(password_as_dict.get("shard_count"))
367
+
368
+ password = ""
369
+ for i in range(shard_count):
370
+ password += str(self._keyring.get_password(key, f"token__{i}"))
371
+
372
+ except self._keyring.errors.NoKeyringError as e:
373
+ raise hyperlake.exceptions.NotSupportedError("Although keyring module is installed no backend has been "
374
+ "detected, check https://pypi.org/project/keyring/ for more "
375
+ "information.") from e
376
+ except ValueError:
377
+ pass
378
+
379
+ return password
380
+
381
+ def store_token_to_cache(self, key: Optional[str], token: str) -> None:
382
+ # keyring is installed, so we can store the token for reuse within multiple threads
383
+ try:
384
+ # if not Windows or "small" password, stick to the default
385
+ if os.name != "nt" or len(token) < MAX_NT_PASSWORD_SIZE:
386
+ self._keyring.set_password(key, "token", token)
387
+ else:
388
+ logger.debug(f"password is {len(token)} characters, sharding it.")
389
+
390
+ password_shards = [
391
+ token[i: i + MAX_NT_PASSWORD_SIZE] for i in range(0, len(token), MAX_NT_PASSWORD_SIZE)
392
+ ]
393
+ shard_info = {
394
+ "sharded_password": True,
395
+ "shard_count": len(password_shards),
396
+ }
397
+
398
+ # store the "shard info" as the "base" password
399
+ self._keyring.set_password(key, "token", json.dumps(shard_info))
400
+ # then store all shards with the shard number as postfix
401
+ for i, s in enumerate(password_shards):
402
+ self._keyring.set_password(key, f"token__{i}", s)
403
+ except self._keyring.errors.NoKeyringError as e:
404
+ raise hyperlake.exceptions.NotSupportedError("Although keyring module is installed no backend has been "
405
+ "detected, check https://pypi.org/project/keyring/ for more "
406
+ "information.") from e
407
+
408
+
409
+ class _OAuth2TokenBearer(AuthBase):
410
+ """
411
+ Custom implementation of hyperlake Trino OAuth2 based authentication to get the token
412
+ """
413
+ MAX_OAUTH_ATTEMPTS = 5
414
+ _BEARER_PREFIX = re.compile(r"bearer", flags=re.IGNORECASE)
415
+
416
+ def __init__(self, redirect_auth_url_handler: Callable[[str], None]):
417
+ self._redirect_auth_url = redirect_auth_url_handler
418
+ keyring_cache = _OAuth2KeyRingTokenCache()
419
+ self._token_cache = keyring_cache if keyring_cache.is_keyring_available() else _OAuth2TokenInMemoryCache()
420
+ self._token_lock = threading.Lock()
421
+ self._inside_oauth_attempt_lock = threading.Lock()
422
+ self._inside_oauth_attempt_blocker = threading.Event()
423
+
424
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
425
+ host = self._determine_host(r.url)
426
+ user = self._determine_user(r.headers)
427
+ key = self._construct_cache_key(host, user)
428
+ token = self._get_token_from_cache(key)
429
+
430
+ if token is not None:
431
+ r.headers['Authorization'] = "Bearer " + token
432
+
433
+ r.register_hook('response', self._authenticate)
434
+
435
+ return r
436
+
437
+ def _authenticate(self, response: Response, **kwargs: Any) -> Optional[Response]:
438
+ if not 400 <= response.status_code < 500:
439
+ return response
440
+
441
+ acquired = self._inside_oauth_attempt_lock.acquire(blocking=False)
442
+ if acquired:
443
+ try:
444
+ # Lock is acquired, attempt the OAuth2 flow
445
+ self._attempt_oauth(response, **kwargs)
446
+ self._inside_oauth_attempt_blocker.set()
447
+ finally:
448
+ self._inside_oauth_attempt_lock.release()
449
+ self._inside_oauth_attempt_blocker.clear()
450
+ else:
451
+ # Lock is not acquired, we are already in the OAuth2 flow, so we block until OAuth2 flow is finished.
452
+ self._inside_oauth_attempt_blocker.wait()
453
+
454
+ return self._retry_request(response, **kwargs)
455
+
456
+ def _attempt_oauth(self, response: Response, **kwargs: Any) -> None:
457
+ # we have to handle the authentication, may be token the token expired, or it wasn't there at all
458
+ auth_info = response.headers.get('WWW-Authenticate')
459
+ if not auth_info:
460
+ raise exceptions.HyperlakeAuthError("Error: header WWW-Authenticate not available in the response.")
461
+
462
+ if not _OAuth2TokenBearer._BEARER_PREFIX.search(auth_info):
463
+ raise exceptions.HyperlakeAuthError(f"Error: header info didn't match {auth_info}")
464
+
465
+ # Example www-authenticate header value:
466
+ # 'Basic realm="Trino", Bearer realm="Trino", token_type="JWT",
467
+ # Bearer x_redirect_server="https://trino.com/oauth2/token/uuid4",
468
+ # x_token_server="https://trino.com/oauth2/token/uuid4"'
469
+ auth_info_headers = self._parse_authenticate_header(auth_info)
470
+
471
+ auth_server = auth_info_headers.get('bearer x_redirect_server', auth_info_headers.get('x_redirect_server'))
472
+ token_server = auth_info_headers.get('bearer x_token_server', auth_info_headers.get('x_token_server'))
473
+ if token_server is None:
474
+ raise exceptions.HyperlakeAuthError("Error: header info didn't have x_token_server")
475
+
476
+ if auth_server is not None:
477
+ # tell app that use this url to proceed with the authentication
478
+ self._redirect_auth_url(auth_server)
479
+
480
+ # Consume content and release the original connection
481
+ # to allow our new request to reuse the same one.
482
+ response.content
483
+ response.close()
484
+
485
+ token = self._get_token(token_server, response, **kwargs)
486
+
487
+ request = response.request
488
+ host = self._determine_host(request.url)
489
+ user = self._determine_user(request.headers)
490
+ key = self._construct_cache_key(host, user)
491
+ self._store_token_to_cache(key, token)
492
+
493
+ def _retry_request(self, response: Response, **kwargs: Any) -> Optional[Response]:
494
+ request = response.request.copy()
495
+ extract_cookies_to_jar(request._cookies, response.request, response.raw)
496
+ request.prepare_cookies(request._cookies)
497
+
498
+ host = self._determine_host(response.request.url)
499
+ user = self._determine_user(request.headers)
500
+ key = self._construct_cache_key(host, user)
501
+ token = self._get_token_from_cache(key)
502
+ if token is not None:
503
+ request.headers['Authorization'] = "Bearer " + token
504
+ retry_response = response.connection.send(request, **kwargs)
505
+ retry_response.history.append(response)
506
+ retry_response.request = request
507
+ return retry_response
508
+
509
+ def _get_token(self, token_server: str, response: Response, **kwargs: Any) -> str:
510
+ attempts = 0
511
+ while attempts < self.MAX_OAUTH_ATTEMPTS:
512
+ attempts += 1
513
+ with response.connection.send(Request(
514
+ method='GET', url=token_server).prepare(), **kwargs) as response:
515
+ if response.status_code == 200:
516
+ token_response = json.loads(response.text)
517
+ token = token_response.get('token')
518
+ if token:
519
+ return token
520
+ error = token_response.get('error')
521
+ if error:
522
+ raise exceptions.HyperlakeAuthError(f"Error while getting the token: {error}")
523
+ else:
524
+ token_server = token_response.get('nextUri')
525
+ logger.debug(f"nextURi auth token server: {token_server}")
526
+ else:
527
+ raise exceptions.HyperlakeAuthError(
528
+ f"Error while getting the token response "
529
+ f"status code: {response.status_code}, "
530
+ f"body: {response.text}")
531
+
532
+ raise exceptions.HyperlakeAuthError("Exceeded max attempts while getting the token")
533
+
534
+ def _get_token_from_cache(self, key: Optional[str]) -> Optional[str]:
535
+ with self._token_lock:
536
+ return self._token_cache.get_token_from_cache(key)
537
+
538
+ def _store_token_to_cache(self, key: Optional[str], token: str) -> None:
539
+ with self._token_lock:
540
+ self._token_cache.store_token_to_cache(key, token)
541
+
542
+ @staticmethod
543
+ def _determine_host(url: Optional[str]) -> Any:
544
+ return urlparse(url).hostname
545
+
546
+ @staticmethod
547
+ def _determine_user(headers: Mapping[Any, Any]) -> Optional[Any]:
548
+ return headers.get(HEADER_USER)
549
+
550
+ @staticmethod
551
+ def _construct_cache_key(host: Optional[str], user: Optional[str]) -> Optional[str]:
552
+ if user is None:
553
+ return host
554
+ else:
555
+ return f"{host}@{user}"
556
+
557
+ @staticmethod
558
+ def _parse_authenticate_header(header: str) -> Dict[str, str]:
559
+ logger.debug(f"Authentication header: {header}")
560
+ components = header.split(",")
561
+ auth_info_headers = {}
562
+
563
+ for component in components:
564
+ component = component.strip()
565
+ if "=" in component:
566
+ key, value = component.split("=", 1)
567
+ if value[0] == '"' and value[-1] == '"':
568
+ value = value[1:-1]
569
+ auth_info_headers[key.lower()] = value
570
+ return auth_info_headers
571
+
572
+
573
+ class OAuth2Authentication(Authentication):
574
+ def __init__(self, redirect_auth_url_handler: CompositeRedirectHandler = CompositeRedirectHandler([
575
+ WebBrowserRedirectHandler(),
576
+ ConsoleRedirectHandler()
577
+ ])):
578
+ self._redirect_auth_url = redirect_auth_url_handler
579
+ self._bearer = _OAuth2TokenBearer(self._redirect_auth_url)
580
+
581
+ def set_http_session(self, http_session: Session) -> Session:
582
+ http_session.auth = self._bearer
583
+ return http_session
584
+
585
+ def get_exceptions(self) -> Tuple[Any, ...]:
586
+ return ()
587
+
588
+ def __eq__(self, other: object) -> bool:
589
+ if not isinstance(other, OAuth2Authentication):
590
+ return False
591
+ return self._redirect_auth_url == other._redirect_auth_url
592
+
593
+
594
+ class CertificateAuthentication(Authentication):
595
+ def __init__(self, cert: str, key: str):
596
+ self._cert = cert
597
+ self._key = key
598
+
599
+ def set_http_session(self, http_session: Session) -> Session:
600
+ http_session.cert = (self._cert, self._key)
601
+ return http_session
602
+
603
+ def get_exceptions(self) -> Tuple[Any, ...]:
604
+ return ()
605
+
606
+ def __eq__(self, other: object) -> bool:
607
+ if not isinstance(other, CertificateAuthentication):
608
+ return False
609
+ return self._cert == other._cert and self._key == other._key