Skip to content

Commit 1fcbea0

Browse files
committed
Add ErrorHandler class, ViewAlreadyExistsError, fix unit tests
1 parent 19ba343 commit 1fcbea0

File tree

5 files changed

+156
-59
lines changed

5 files changed

+156
-59
lines changed

pyiceberg/catalog/rest/__init__.py

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,11 @@
4040
PropertiesUpdateSummary,
4141
)
4242
from pyiceberg.catalog.rest.auth import AuthManager, AuthManagerAdapter, AuthManagerFactory, LegacyOAuth2AuthManager
43-
from pyiceberg.catalog.rest.response import _handle_non_200_response
43+
from pyiceberg.catalog.rest.response import ErrorHandlers
4444
from pyiceberg.exceptions import (
4545
AuthorizationExpiredError,
46-
CommitFailedException,
47-
CommitStateUnknownException,
48-
NamespaceAlreadyExistsError,
49-
NamespaceNotEmptyError,
5046
NoSuchIdentifierError,
5147
NoSuchNamespaceError,
52-
NoSuchTableError,
53-
NoSuchViewError,
54-
TableAlreadyExistsError,
5548
UnauthorizedError,
5649
)
5750
from pyiceberg.io import AWS_ACCESS_KEY_ID, AWS_REGION, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
@@ -69,6 +62,7 @@
6962
from pyiceberg.table.metadata import TableMetadata
7063
from pyiceberg.table.sorting import UNSORTED_SORT_ORDER, SortOrder, assign_fresh_sort_order_ids
7164
from pyiceberg.table.update import (
65+
AssertCreate,
7266
TableRequirement,
7367
TableUpdate,
7468
)
@@ -366,7 +360,7 @@ def _fetch_config(self) -> None:
366360
try:
367361
response.raise_for_status()
368362
except HTTPError as exc:
369-
_handle_non_200_response(exc, {})
363+
ErrorHandlers.default_error_handler(exc)
370364
config_response = ConfigResponse.model_validate_json(response.text)
371365

372366
config = config_response.defaults
@@ -524,7 +518,7 @@ def _create_table(
524518
try:
525519
response.raise_for_status()
526520
except HTTPError as exc:
527-
_handle_non_200_response(exc, {409: TableAlreadyExistsError, 404: NoSuchNamespaceError})
521+
ErrorHandlers.table_error_handler(exc)
528522
return TableResponse.model_validate_json(response.text)
529523

530524
@retry(**_RETRY_ARGS)
@@ -597,7 +591,7 @@ def register_table(self, identifier: Union[str, Identifier], metadata_location:
597591
try:
598592
response.raise_for_status()
599593
except HTTPError as exc:
600-
_handle_non_200_response(exc, {409: TableAlreadyExistsError})
594+
ErrorHandlers.table_error_handler(exc)
601595

602596
table_response = TableResponse.model_validate_json(response.text)
603597
return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
@@ -610,7 +604,7 @@ def list_tables(self, namespace: Union[str, Identifier]) -> List[Identifier]:
610604
try:
611605
response.raise_for_status()
612606
except HTTPError as exc:
613-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
607+
ErrorHandlers.namespace_error_handler(exc)
614608
return [(*table.namespace, table.name) for table in ListTablesResponse.model_validate_json(response.text).identifiers]
615609

616610
@retry(**_RETRY_ARGS)
@@ -628,7 +622,7 @@ def load_table(self, identifier: Union[str, Identifier]) -> Table:
628622
try:
629623
response.raise_for_status()
630624
except HTTPError as exc:
631-
_handle_non_200_response(exc, {404: NoSuchTableError})
625+
ErrorHandlers.table_error_handler(exc)
632626

633627
table_response = TableResponse.model_validate_json(response.text)
634628
return self._response_to_table(self.identifier_to_tuple(identifier), table_response)
@@ -642,7 +636,7 @@ def drop_table(self, identifier: Union[str, Identifier], purge_requested: bool =
642636
try:
643637
response.raise_for_status()
644638
except HTTPError as exc:
645-
_handle_non_200_response(exc, {404: NoSuchTableError})
639+
ErrorHandlers.table_error_handler(exc)
646640

647641
@retry(**_RETRY_ARGS)
648642
def purge_table(self, identifier: Union[str, Identifier]) -> None:
@@ -658,7 +652,7 @@ def rename_table(self, from_identifier: Union[str, Identifier], to_identifier: U
658652
try:
659653
response.raise_for_status()
660654
except HTTPError as exc:
661-
_handle_non_200_response(exc, {404: NoSuchTableError, 409: TableAlreadyExistsError})
655+
ErrorHandlers.table_error_handler(exc)
662656

663657
return self.load_table(to_identifier)
664658

@@ -681,7 +675,7 @@ def list_views(self, namespace: Union[str, Identifier]) -> List[Identifier]:
681675
try:
682676
response.raise_for_status()
683677
except HTTPError as exc:
684-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
678+
ErrorHandlers.view_error_handler(exc)
685679
return [(*view.namespace, view.name) for view in ListViewsResponse.model_validate_json(response.text).identifiers]
686680

687681
@retry(**_RETRY_ARGS)
@@ -719,15 +713,10 @@ def commit_table(
719713
try:
720714
response.raise_for_status()
721715
except HTTPError as exc:
722-
_handle_non_200_response(
723-
exc,
724-
{
725-
409: CommitFailedException,
726-
500: CommitStateUnknownException,
727-
502: CommitStateUnknownException,
728-
504: CommitStateUnknownException,
729-
},
730-
)
716+
if AssertCreate() in requirements:
717+
ErrorHandlers.table_error_handler(exc)
718+
else:
719+
ErrorHandlers.commit_error_handler(exc)
731720
return CommitTableResponse.model_validate_json(response.text)
732721

733722
@retry(**_RETRY_ARGS)
@@ -738,7 +727,7 @@ def create_namespace(self, namespace: Union[str, Identifier], properties: Proper
738727
try:
739728
response.raise_for_status()
740729
except HTTPError as exc:
741-
_handle_non_200_response(exc, {409: NamespaceAlreadyExistsError})
730+
ErrorHandlers.namespace_error_handler(exc)
742731

743732
@retry(**_RETRY_ARGS)
744733
def drop_namespace(self, namespace: Union[str, Identifier]) -> None:
@@ -748,7 +737,7 @@ def drop_namespace(self, namespace: Union[str, Identifier]) -> None:
748737
try:
749738
response.raise_for_status()
750739
except HTTPError as exc:
751-
_handle_non_200_response(exc, {404: NoSuchNamespaceError, 409: NamespaceNotEmptyError})
740+
ErrorHandlers.drop_namespace_error_handler(exc)
752741

753742
@retry(**_RETRY_ARGS)
754743
def list_namespaces(self, namespace: Union[str, Identifier] = ()) -> List[Identifier]:
@@ -763,7 +752,7 @@ def list_namespaces(self, namespace: Union[str, Identifier] = ()) -> List[Identi
763752
try:
764753
response.raise_for_status()
765754
except HTTPError as exc:
766-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
755+
ErrorHandlers.namespace_error_handler(exc)
767756

768757
return ListNamespaceResponse.model_validate_json(response.text).namespaces
769758

@@ -775,7 +764,7 @@ def load_namespace_properties(self, namespace: Union[str, Identifier]) -> Proper
775764
try:
776765
response.raise_for_status()
777766
except HTTPError as exc:
778-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
767+
ErrorHandlers.namespace_error_handler(exc)
779768

780769
return NamespaceResponse.model_validate_json(response.text).properties
781770

@@ -790,7 +779,7 @@ def update_namespace_properties(
790779
try:
791780
response.raise_for_status()
792781
except HTTPError as exc:
793-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
782+
ErrorHandlers.namespace_error_handler(exc)
794783
parsed_response = UpdateNamespacePropertiesResponse.model_validate_json(response.text)
795784
return PropertiesUpdateSummary(
796785
removed=parsed_response.removed,
@@ -812,7 +801,7 @@ def namespace_exists(self, namespace: Union[str, Identifier]) -> bool:
812801
try:
813802
response.raise_for_status()
814803
except HTTPError as exc:
815-
_handle_non_200_response(exc, {})
804+
ErrorHandlers.namespace_error_handler(exc)
816805

817806
return False
818807

@@ -838,7 +827,7 @@ def table_exists(self, identifier: Union[str, Identifier]) -> bool:
838827
try:
839828
response.raise_for_status()
840829
except HTTPError as exc:
841-
_handle_non_200_response(exc, {})
830+
ErrorHandlers.table_error_handler(exc)
842831

843832
return False
844833

@@ -863,7 +852,7 @@ def view_exists(self, identifier: Union[str, Identifier]) -> bool:
863852
try:
864853
response.raise_for_status()
865854
except HTTPError as exc:
866-
_handle_non_200_response(exc, {})
855+
ErrorHandlers.view_error_handler(exc)
867856

868857
return False
869858

@@ -875,7 +864,7 @@ def drop_view(self, identifier: Union[str]) -> None:
875864
try:
876865
response.raise_for_status()
877866
except HTTPError as exc:
878-
_handle_non_200_response(exc, {404: NoSuchViewError})
867+
ErrorHandlers.view_error_handler(exc)
879868

880869
def close(self) -> None:
881870
"""Close the catalog and release Session connection adapters.

pyiceberg/catalog/rest/response.py

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
from json import JSONDecodeError
18-
from typing import Dict, Literal, Optional, Type
18+
from typing import Dict, Literal, Optional, Type, TypeAlias
1919

2020
from pydantic import Field, ValidationError
2121
from requests import HTTPError
2222

2323
from pyiceberg.exceptions import (
2424
AuthorizationExpiredError,
2525
BadRequestError,
26+
CommitFailedException,
27+
CommitStateUnknownException,
2628
ForbiddenError,
29+
NamespaceAlreadyExistsError,
30+
NamespaceNotEmptyError,
31+
NoSuchNamespaceError,
32+
NoSuchTableError,
33+
NoSuchViewError,
2734
OAuthError,
2835
RESTError,
2936
ServerError,
3037
ServiceUnavailableError,
38+
TableAlreadyExistsError,
3139
UnauthorizedError,
40+
ViewAlreadyExistsError,
3241
)
3342
from pyiceberg.typedef import IcebergBaseModel
3443

@@ -60,33 +69,92 @@ class OAuthErrorResponse(IcebergBaseModel):
6069
error_uri: Optional[str] = None
6170

6271

63-
def _handle_non_200_response(exc: HTTPError, error_handler: Dict[int, Type[Exception]]) -> None:
72+
_ErrorHandler: TypeAlias = Dict[int, Type[Exception]]
73+
74+
75+
class ErrorHandlers:
76+
"""
77+
Utility class providing static methods to handle HTTP errors for table, namespace, and view operations.
78+
79+
Maps HTTP error responses to appropriate custom exceptions, ensuring consistent error handling.
80+
"""
81+
82+
@staticmethod
83+
def default_error_handler(exc: HTTPError) -> None:
84+
_handle_non_200_response(exc, {})
85+
86+
@staticmethod
87+
def namespace_error_handler(exc: HTTPError) -> None:
88+
handler: _ErrorHandler = {
89+
400: BadRequestError,
90+
404: NoSuchNamespaceError,
91+
409: NamespaceAlreadyExistsError,
92+
422: RESTError,
93+
}
94+
95+
if "NamespaceNotEmpty" in exc.response.text:
96+
handler[400] = NamespaceNotEmptyError
97+
98+
_handle_non_200_response(exc, handler)
99+
100+
@staticmethod
101+
def drop_namespace_error_handler(exc: HTTPError) -> None:
102+
handler: _ErrorHandler = {404: NoSuchNamespaceError, 409: NamespaceNotEmptyError}
103+
104+
_handle_non_200_response(exc, handler)
105+
106+
@staticmethod
107+
def table_error_handler(exc: HTTPError) -> None:
108+
handler: _ErrorHandler = {404: NoSuchTableError, 409: TableAlreadyExistsError}
109+
110+
if "NoSuchNamespace" in exc.response.text:
111+
handler[404] = NoSuchNamespaceError
112+
113+
_handle_non_200_response(exc, handler)
114+
115+
@staticmethod
116+
def commit_error_handler(exc: HTTPError) -> None:
117+
handler: _ErrorHandler = {
118+
404: NoSuchTableError,
119+
409: CommitFailedException,
120+
500: CommitStateUnknownException,
121+
502: CommitStateUnknownException,
122+
503: CommitStateUnknownException,
123+
504: CommitStateUnknownException,
124+
}
125+
126+
_handle_non_200_response(exc, handler)
127+
128+
@staticmethod
129+
def view_error_handler(exc: HTTPError) -> None:
130+
handler: _ErrorHandler = {404: NoSuchViewError, 409: ViewAlreadyExistsError}
131+
132+
if "NoSuchNamespace" in exc.response.text:
133+
handler[404] = NoSuchNamespaceError
134+
135+
_handle_non_200_response(exc, handler)
136+
137+
138+
def _handle_non_200_response(exc: HTTPError, handler: _ErrorHandler) -> None:
64139
exception: Type[Exception]
65140

66141
if exc.response is None:
67142
raise ValueError("Did not receive a response")
68143

69144
code = exc.response.status_code
70-
if code in error_handler:
71-
exception = error_handler[code]
72-
elif code == 400:
73-
exception = BadRequestError
74-
elif code == 401:
75-
exception = UnauthorizedError
76-
elif code == 403:
77-
exception = ForbiddenError
78-
elif code == 422:
79-
exception = RESTError
80-
elif code == 419:
81-
exception = AuthorizationExpiredError
82-
elif code == 501:
83-
exception = NotImplementedError
84-
elif code == 503:
85-
exception = ServiceUnavailableError
86-
elif 500 <= code < 600:
87-
exception = ServerError
88-
else:
89-
exception = RESTError
145+
146+
default_handler: _ErrorHandler = {
147+
400: BadRequestError,
148+
401: UnauthorizedError,
149+
403: ForbiddenError,
150+
419: AuthorizationExpiredError,
151+
422: RESTError,
152+
501: NotImplementedError,
153+
503: ServiceUnavailableError,
154+
}
155+
156+
# Merge handler passed with default handler map, if no match exception will be ServerError or RESTError
157+
exception = handler.get(code, default_handler.get(code, ServerError if 500 <= code < 600 else RESTError))
90158

91159
try:
92160
if exception == OAuthError:

pyiceberg/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class NoSuchViewError(Exception):
4444
"""Raises when the view can't be found in the REST catalog."""
4545

4646

47+
class ViewAlreadyExistsError(Exception):
48+
"""Raised when creating a view with a name that already exists."""
49+
50+
4751
class NoSuchIdentifierError(Exception):
4852
"""Raises when the identifier can't be found in the REST catalog."""
4953

0 commit comments

Comments
 (0)