15
15
# specific language governing permissions and limitations
16
16
# under the License.
17
17
from enum import Enum
18
- from json import JSONDecodeError
19
18
from typing import (
20
19
TYPE_CHECKING ,
21
20
Any ,
22
21
Dict ,
23
22
List ,
24
- Literal ,
25
23
Optional ,
26
24
Set ,
27
25
Tuple ,
28
- Type ,
29
26
Union ,
30
27
)
31
28
32
- from pydantic import Field , ValidationError , field_validator
29
+ from pydantic import Field , field_validator
33
30
from requests import HTTPError , Session
34
31
from tenacity import RetryCallState , retry , retry_if_exception_type , stop_after_attempt
35
32
41
38
Catalog ,
42
39
PropertiesUpdateSummary ,
43
40
)
41
+ from pyiceberg .catalog .rest .auth import AuthManager , AuthManagerAdapter , AuthManagerFactory , LegacyOAuth2AuthManager
42
+ from pyiceberg .catalog .rest .response import _handle_non_200_response
44
43
from pyiceberg .exceptions import (
45
44
AuthorizationExpiredError ,
46
- BadRequestError ,
47
45
CommitFailedException ,
48
46
CommitStateUnknownException ,
49
- ForbiddenError ,
50
47
NamespaceAlreadyExistsError ,
51
48
NamespaceNotEmptyError ,
52
49
NoSuchIdentifierError ,
53
50
NoSuchNamespaceError ,
54
51
NoSuchTableError ,
55
52
NoSuchViewError ,
56
- OAuthError ,
57
- RESTError ,
58
- ServerError ,
59
- ServiceUnavailableError ,
60
53
TableAlreadyExistsError ,
61
54
UnauthorizedError ,
62
55
)
@@ -182,15 +175,6 @@ class RegisterTableRequest(IcebergBaseModel):
182
175
metadata_location : str = Field (..., alias = "metadata-location" )
183
176
184
177
185
- class TokenResponse (IcebergBaseModel ):
186
- access_token : str = Field ()
187
- token_type : str = Field ()
188
- expires_in : Optional [int ] = Field (default = None )
189
- issued_token_type : Optional [str ] = Field (default = None )
190
- refresh_token : Optional [str ] = Field (default = None )
191
- scope : Optional [str ] = Field (default = None )
192
-
193
-
194
178
class ConfigResponse (IcebergBaseModel ):
195
179
defaults : Properties = Field ()
196
180
overrides : Properties = Field ()
@@ -229,24 +213,6 @@ class ListViewsResponse(IcebergBaseModel):
229
213
identifiers : List [ListViewResponseEntry ] = Field ()
230
214
231
215
232
- class ErrorResponseMessage (IcebergBaseModel ):
233
- message : str = Field ()
234
- type : str = Field ()
235
- code : int = Field ()
236
-
237
-
238
- class ErrorResponse (IcebergBaseModel ):
239
- error : ErrorResponseMessage = Field ()
240
-
241
-
242
- class OAuthErrorResponse (IcebergBaseModel ):
243
- error : Literal [
244
- "invalid_request" , "invalid_client" , "invalid_grant" , "unauthorized_client" , "unsupported_grant_type" , "invalid_scope"
245
- ]
246
- error_description : Optional [str ] = None
247
- error_uri : Optional [str ] = None
248
-
249
-
250
216
class RestCatalog (Catalog ):
251
217
uri : str
252
218
_session : Session
@@ -279,8 +245,7 @@ def _create_session(self) -> Session:
279
245
elif ssl_client_cert := ssl_client .get (CERT ):
280
246
session .cert = ssl_client_cert
281
247
282
- self ._refresh_token (session , self .properties .get (TOKEN ))
283
-
248
+ session .auth = AuthManagerAdapter (self ._create_legacy_oauth2_auth_manager (session ))
284
249
# Set HTTP headers
285
250
self ._config_headers (session )
286
251
@@ -290,6 +255,26 @@ def _create_session(self) -> Session:
290
255
291
256
return session
292
257
258
+ def _create_legacy_oauth2_auth_manager (self , session : Session ) -> AuthManager :
259
+ """Create the LegacyOAuth2AuthManager by fetching required properties.
260
+
261
+ This will be removed in PyIceberg 1.0
262
+ """
263
+ client_credentials = self .properties .get (CREDENTIAL )
264
+ # We want to call `self.auth_url` only when we are using CREDENTIAL
265
+ # with the legacy OAUTH2 flow as it will raise a DeprecationWarning
266
+ auth_url = self .auth_url if client_credentials is not None else None
267
+
268
+ auth_config = {
269
+ "session" : session ,
270
+ "auth_url" : auth_url ,
271
+ "credential" : client_credentials ,
272
+ "initial_token" : self .properties .get (TOKEN ),
273
+ "optional_oauth_params" : self ._extract_optional_oauth_params (),
274
+ }
275
+
276
+ return AuthManagerFactory .create ("legacyoauth2" , auth_config )
277
+
293
278
def _check_valid_namespace_identifier (self , identifier : Union [str , Identifier ]) -> Identifier :
294
279
"""Check if the identifier has at least one element."""
295
280
identifier_tuple = Catalog .identifier_to_tuple (identifier )
@@ -352,27 +337,6 @@ def _extract_optional_oauth_params(self) -> Dict[str, str]:
352
337
353
338
return optional_oauth_param
354
339
355
- def _fetch_access_token (self , session : Session , credential : str ) -> str :
356
- if SEMICOLON in credential :
357
- client_id , client_secret = credential .split (SEMICOLON )
358
- else :
359
- client_id , client_secret = None , credential
360
-
361
- data = {GRANT_TYPE : CLIENT_CREDENTIALS , CLIENT_ID : client_id , CLIENT_SECRET : client_secret }
362
-
363
- optional_oauth_params = self ._extract_optional_oauth_params ()
364
- data .update (optional_oauth_params )
365
-
366
- response = session .post (
367
- url = self .auth_url , data = data , headers = {** session .headers , "Content-type" : "application/x-www-form-urlencoded" }
368
- )
369
- try :
370
- response .raise_for_status ()
371
- except HTTPError as exc :
372
- self ._handle_non_200_response (exc , {400 : OAuthError , 401 : OAuthError })
373
-
374
- return TokenResponse .model_validate_json (response .text ).access_token
375
-
376
340
def _fetch_config (self ) -> None :
377
341
params = {}
378
342
if warehouse_location := self .properties .get (WAREHOUSE_LOCATION ):
@@ -383,7 +347,7 @@ def _fetch_config(self) -> None:
383
347
try :
384
348
response .raise_for_status ()
385
349
except HTTPError as exc :
386
- self . _handle_non_200_response (exc , {})
350
+ _handle_non_200_response (exc , {})
387
351
config_response = ConfigResponse .model_validate_json (response .text )
388
352
389
353
config = config_response .defaults
@@ -413,58 +377,6 @@ def _split_identifier_for_json(self, identifier: Union[str, Identifier]) -> Dict
413
377
identifier_tuple = self ._identifier_to_validated_tuple (identifier )
414
378
return {"namespace" : identifier_tuple [:- 1 ], "name" : identifier_tuple [- 1 ]}
415
379
416
- def _handle_non_200_response (self , exc : HTTPError , error_handler : Dict [int , Type [Exception ]]) -> None :
417
- exception : Type [Exception ]
418
-
419
- if exc .response is None :
420
- raise ValueError ("Did not receive a response" )
421
-
422
- code = exc .response .status_code
423
- if code in error_handler :
424
- exception = error_handler [code ]
425
- elif code == 400 :
426
- exception = BadRequestError
427
- elif code == 401 :
428
- exception = UnauthorizedError
429
- elif code == 403 :
430
- exception = ForbiddenError
431
- elif code == 422 :
432
- exception = RESTError
433
- elif code == 419 :
434
- exception = AuthorizationExpiredError
435
- elif code == 501 :
436
- exception = NotImplementedError
437
- elif code == 503 :
438
- exception = ServiceUnavailableError
439
- elif 500 <= code < 600 :
440
- exception = ServerError
441
- else :
442
- exception = RESTError
443
-
444
- try :
445
- if exception == OAuthError :
446
- # The OAuthErrorResponse has a different format
447
- error = OAuthErrorResponse .model_validate_json (exc .response .text )
448
- response = str (error .error )
449
- if description := error .error_description :
450
- response += f": { description } "
451
- if uri := error .error_uri :
452
- response += f" ({ uri } )"
453
- else :
454
- error = ErrorResponse .model_validate_json (exc .response .text ).error
455
- response = f"{ error .type } : { error .message } "
456
- except JSONDecodeError :
457
- # In the case we don't have a proper response
458
- response = f"RESTError { exc .response .status_code } : Could not decode json payload: { exc .response .text } "
459
- except ValidationError as e :
460
- # In the case we don't have a proper response
461
- errs = ", " .join (err ["msg" ] for err in e .errors ())
462
- response = (
463
- f"RESTError { exc .response .status_code } : Received unexpected JSON Payload: { exc .response .text } , errors: { errs } "
464
- )
465
-
466
- raise exception (response ) from exc
467
-
468
380
def _init_sigv4 (self , session : Session ) -> None :
469
381
from urllib import parse
470
382
@@ -534,16 +446,13 @@ def _response_to_staged_table(self, identifier_tuple: Tuple[str, ...], table_res
534
446
catalog = self ,
535
447
)
536
448
537
- def _refresh_token (self , session : Optional [Session ] = None , initial_token : Optional [str ] = None ) -> None :
538
- session = session or self ._session
539
- if initial_token is not None :
540
- self .properties [TOKEN ] = initial_token
541
- elif CREDENTIAL in self .properties :
542
- self .properties [TOKEN ] = self ._fetch_access_token (session , self .properties [CREDENTIAL ])
543
-
544
- # Set Auth token for subsequent calls in the session
545
- if token := self .properties .get (TOKEN ):
546
- session .headers [AUTHORIZATION_HEADER ] = f"{ BEARER_PREFIX } { token } "
449
+ def _refresh_token (self ) -> None :
450
+ # Reactive token refresh is atypical - we should proactively refresh tokens in a separate thread
451
+ # instead of retrying on Auth Exceptions. Keeping refresh behavior for the LegacyOAuth2AuthManager
452
+ # for backward compatibility
453
+ auth_manager = self ._session .auth .auth_manager # type: ignore[union-attr]
454
+ if isinstance (auth_manager , LegacyOAuth2AuthManager ):
455
+ auth_manager ._refresh_token ()
547
456
548
457
def _config_headers (self , session : Session ) -> None :
549
458
header_properties = get_header_properties (self .properties )
@@ -588,7 +497,7 @@ def _create_table(
588
497
try :
589
498
response .raise_for_status ()
590
499
except HTTPError as exc :
591
- self . _handle_non_200_response (exc , {409 : TableAlreadyExistsError })
500
+ _handle_non_200_response (exc , {409 : TableAlreadyExistsError })
592
501
return TableResponse .model_validate_json (response .text )
593
502
594
503
@retry (** _RETRY_ARGS )
@@ -661,7 +570,7 @@ def register_table(self, identifier: Union[str, Identifier], metadata_location:
661
570
try :
662
571
response .raise_for_status ()
663
572
except HTTPError as exc :
664
- self . _handle_non_200_response (exc , {409 : TableAlreadyExistsError })
573
+ _handle_non_200_response (exc , {409 : TableAlreadyExistsError })
665
574
666
575
table_response = TableResponse .model_validate_json (response .text )
667
576
return self ._response_to_table (self .identifier_to_tuple (identifier ), table_response )
@@ -674,7 +583,7 @@ def list_tables(self, namespace: Union[str, Identifier]) -> List[Identifier]:
674
583
try :
675
584
response .raise_for_status ()
676
585
except HTTPError as exc :
677
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
586
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
678
587
return [(* table .namespace , table .name ) for table in ListTablesResponse .model_validate_json (response .text ).identifiers ]
679
588
680
589
@retry (** _RETRY_ARGS )
@@ -692,7 +601,7 @@ def load_table(self, identifier: Union[str, Identifier]) -> Table:
692
601
try :
693
602
response .raise_for_status ()
694
603
except HTTPError as exc :
695
- self . _handle_non_200_response (exc , {404 : NoSuchTableError })
604
+ _handle_non_200_response (exc , {404 : NoSuchTableError })
696
605
697
606
table_response = TableResponse .model_validate_json (response .text )
698
607
return self ._response_to_table (self .identifier_to_tuple (identifier ), table_response )
@@ -705,7 +614,7 @@ def drop_table(self, identifier: Union[str, Identifier], purge_requested: bool =
705
614
try :
706
615
response .raise_for_status ()
707
616
except HTTPError as exc :
708
- self . _handle_non_200_response (exc , {404 : NoSuchTableError })
617
+ _handle_non_200_response (exc , {404 : NoSuchTableError })
709
618
710
619
@retry (** _RETRY_ARGS )
711
620
def purge_table (self , identifier : Union [str , Identifier ]) -> None :
@@ -721,7 +630,7 @@ def rename_table(self, from_identifier: Union[str, Identifier], to_identifier: U
721
630
try :
722
631
response .raise_for_status ()
723
632
except HTTPError as exc :
724
- self . _handle_non_200_response (exc , {404 : NoSuchTableError , 409 : TableAlreadyExistsError })
633
+ _handle_non_200_response (exc , {404 : NoSuchTableError , 409 : TableAlreadyExistsError })
725
634
726
635
return self .load_table (to_identifier )
727
636
@@ -744,7 +653,7 @@ def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
744
653
try :
745
654
response .raise_for_status ()
746
655
except HTTPError as exc :
747
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
656
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
748
657
return [(* view .namespace , view .name ) for view in ListViewsResponse .model_validate_json (response .text ).identifiers ]
749
658
750
659
@retry (** _RETRY_ARGS )
@@ -782,7 +691,7 @@ def commit_table(
782
691
try :
783
692
response .raise_for_status ()
784
693
except HTTPError as exc :
785
- self . _handle_non_200_response (
694
+ _handle_non_200_response (
786
695
exc ,
787
696
{
788
697
409 : CommitFailedException ,
@@ -801,7 +710,7 @@ def create_namespace(self, namespace: Union[str, Identifier], properties: Proper
801
710
try :
802
711
response .raise_for_status ()
803
712
except HTTPError as exc :
804
- self . _handle_non_200_response (exc , {409 : NamespaceAlreadyExistsError })
713
+ _handle_non_200_response (exc , {409 : NamespaceAlreadyExistsError })
805
714
806
715
@retry (** _RETRY_ARGS )
807
716
def drop_namespace (self , namespace : Union [str , Identifier ]) -> None :
@@ -811,7 +720,7 @@ def drop_namespace(self, namespace: Union[str, Identifier]) -> None:
811
720
try :
812
721
response .raise_for_status ()
813
722
except HTTPError as exc :
814
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError , 409 : NamespaceNotEmptyError })
723
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError , 409 : NamespaceNotEmptyError })
815
724
816
725
@retry (** _RETRY_ARGS )
817
726
def list_namespaces (self , namespace : Union [str , Identifier ] = ()) -> List [Identifier ]:
@@ -826,7 +735,7 @@ def list_namespaces(self, namespace: Union[str, Identifier] = ()) -> List[Identi
826
735
try :
827
736
response .raise_for_status ()
828
737
except HTTPError as exc :
829
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
738
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
830
739
831
740
return ListNamespaceResponse .model_validate_json (response .text ).namespaces
832
741
@@ -838,7 +747,7 @@ def load_namespace_properties(self, namespace: Union[str, Identifier]) -> Proper
838
747
try :
839
748
response .raise_for_status ()
840
749
except HTTPError as exc :
841
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
750
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
842
751
843
752
return NamespaceResponse .model_validate_json (response .text ).properties
844
753
@@ -853,7 +762,7 @@ def update_namespace_properties(
853
762
try :
854
763
response .raise_for_status ()
855
764
except HTTPError as exc :
856
- self . _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
765
+ _handle_non_200_response (exc , {404 : NoSuchNamespaceError })
857
766
parsed_response = UpdateNamespacePropertiesResponse .model_validate_json (response .text )
858
767
return PropertiesUpdateSummary (
859
768
removed = parsed_response .removed ,
@@ -875,7 +784,7 @@ def namespace_exists(self, namespace: Union[str, Identifier]) -> bool:
875
784
try :
876
785
response .raise_for_status ()
877
786
except HTTPError as exc :
878
- self . _handle_non_200_response (exc , {})
787
+ _handle_non_200_response (exc , {})
879
788
880
789
return False
881
790
@@ -901,7 +810,7 @@ def table_exists(self, identifier: Union[str, Identifier]) -> bool:
901
810
try :
902
811
response .raise_for_status ()
903
812
except HTTPError as exc :
904
- self . _handle_non_200_response (exc , {})
813
+ _handle_non_200_response (exc , {})
905
814
906
815
return False
907
816
@@ -926,7 +835,7 @@ def view_exists(self, identifier: Union[str, Identifier]) -> bool:
926
835
try :
927
836
response .raise_for_status ()
928
837
except HTTPError as exc :
929
- self . _handle_non_200_response (exc , {})
838
+ _handle_non_200_response (exc , {})
930
839
931
840
return False
932
841
@@ -938,4 +847,4 @@ def drop_view(self, identifier: Union[str]) -> None:
938
847
try :
939
848
response .raise_for_status ()
940
849
except HTTPError as exc :
941
- self . _handle_non_200_response (exc , {404 : NoSuchViewError })
850
+ _handle_non_200_response (exc , {404 : NoSuchViewError })
0 commit comments