From 29e637427628dddbdab569973ea1ceaa42439575 Mon Sep 17 00:00:00 2001 From: hussein-awala Date: Mon, 12 Feb 2024 22:28:57 +0100 Subject: [PATCH 1/4] Feat: Add fail_if_exists param to create_table --- pyiceberg/catalog/__init__.py | 2 ++ pyiceberg/catalog/dynamodb.py | 5 ++- pyiceberg/catalog/glue.py | 17 ++++++++-- pyiceberg/catalog/hive.py | 7 +++- pyiceberg/catalog/noop.py | 1 + pyiceberg/catalog/rest.py | 14 +++++--- pyiceberg/catalog/sql.py | 6 ++-- tests/catalog/integration_test_dynamodb.py | 26 ++++++++++++--- tests/catalog/integration_test_glue.py | 26 ++++++++++++--- tests/catalog/test_base.py | 6 +++- tests/catalog/test_dynamodb.py | 25 ++++++++++++--- tests/catalog/test_rest.py | 37 +++++++++++++++++++--- tests/catalog/test_sql.py | 25 ++++++++++++--- 13 files changed, 164 insertions(+), 33 deletions(-) diff --git a/pyiceberg/catalog/__init__.py b/pyiceberg/catalog/__init__.py index 6e5dc2748f..9562891197 100644 --- a/pyiceberg/catalog/__init__.py +++ b/pyiceberg/catalog/__init__.py @@ -297,6 +297,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: """Create a table. @@ -307,6 +308,7 @@ def create_table( partition_spec (PartitionSpec): PartitionSpec for the table. sort_order (SortOrder): SortOrder for the table. properties (Properties): Table properties that can be a string based dictionary. + fail_if_exists (bool): If True, raise an error if the table already exists. Returns: Table: the created table instance. diff --git a/pyiceberg/catalog/dynamodb.py b/pyiceberg/catalog/dynamodb.py index d5f3b5e14c..2ae8a5eb5d 100644 --- a/pyiceberg/catalog/dynamodb.py +++ b/pyiceberg/catalog/dynamodb.py @@ -136,6 +136,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -147,6 +148,7 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. + fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -178,7 +180,8 @@ def create_table( condition_expression=f"attribute_not_exists({DYNAMODB_COL_IDENTIFIER})", ) except ConditionalCheckFailedException as e: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + if fail_if_exists: + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e return self.load_table(identifier=identifier) diff --git a/pyiceberg/catalog/glue.py b/pyiceberg/catalog/glue.py index 06484cb0e4..92569da4ea 100644 --- a/pyiceberg/catalog/glue.py +++ b/pyiceberg/catalog/glue.py @@ -302,11 +302,18 @@ def _convert_glue_to_iceberg(self, glue_table: TableTypeDef) -> Table: catalog=self, ) - def _create_glue_table(self, database_name: str, table_name: str, table_input: TableInputTypeDef) -> None: + def _create_glue_table( + self, + database_name: str, + table_name: str, + table_input: TableInputTypeDef, + fail_if_exists: bool = True, + ) -> None: try: self.glue.create_table(DatabaseName=database_name, TableInput=table_input) except self.glue.exceptions.AlreadyExistsException as e: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + if fail_if_exists: + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e except self.glue.exceptions.EntityNotFoundException as e: raise NoSuchNamespaceError(f"Database {database_name} does not exist") from e @@ -340,6 +347,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -351,6 +359,7 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. + fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -374,7 +383,9 @@ def create_table( table_input = _construct_table_input(table_name, metadata_location, properties, metadata) database_name, table_name = self.identifier_to_database_and_table(identifier) - self._create_glue_table(database_name=database_name, table_name=table_name, table_input=table_input) + self._create_glue_table( + database_name=database_name, table_name=table_name, table_input=table_input, fail_if_exists=fail_if_exists + ) return self.load_table(identifier=identifier) diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py index aba3c173e6..bf4c5fbddf 100644 --- a/pyiceberg/catalog/hive.py +++ b/pyiceberg/catalog/hive.py @@ -269,6 +269,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: """Create a table. @@ -279,6 +280,7 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. + fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -321,7 +323,10 @@ def create_table( open_client.create_table(tbl) hive_table = open_client.get_table(dbname=database_name, tbl_name=table_name) except AlreadyExistsException as e: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + if fail_if_exists: + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + else: + hive_table = self.load_table(identifier) return self._convert_hive_into_iceberg(hive_table, io) diff --git a/pyiceberg/catalog/noop.py b/pyiceberg/catalog/noop.py index a8b7154621..05f8ce94f3 100644 --- a/pyiceberg/catalog/noop.py +++ b/pyiceberg/catalog/noop.py @@ -47,6 +47,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: raise NotImplementedError diff --git a/pyiceberg/catalog/rest.py b/pyiceberg/catalog/rest.py index b4b9f722b5..f6ff25a3a5 100644 --- a/pyiceberg/catalog/rest.py +++ b/pyiceberg/catalog/rest.py @@ -446,6 +446,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: iceberg_schema = self._convert_schema_if_needed(schema) fresh_schema = assign_fresh_schema_ids(iceberg_schema) @@ -468,11 +469,16 @@ def create_table( ) try: response.raise_for_status() + table_response = TableResponse(**response.json()) + return self._response_to_table(self.identifier_to_tuple(identifier), table_response) except HTTPError as exc: - self._handle_non_200_response(exc, {409: TableAlreadyExistsError}) - - table_response = TableResponse(**response.json()) - return self._response_to_table(self.identifier_to_tuple(identifier), table_response) + try: + self._handle_non_200_response(exc, {409: TableAlreadyExistsError}) + except TableAlreadyExistsError: + if fail_if_exists: + raise + return self.load_table(identifier) + raise def register_table(self, identifier: Union[str, Identifier], metadata_location: str) -> Table: """Register a new table using existing metadata. diff --git a/pyiceberg/catalog/sql.py b/pyiceberg/catalog/sql.py index 62a2dac54a..686e9d6cd9 100644 --- a/pyiceberg/catalog/sql.py +++ b/pyiceberg/catalog/sql.py @@ -153,6 +153,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -164,6 +165,7 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. + fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -200,8 +202,8 @@ def create_table( ) session.commit() except IntegrityError as e: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e - + if fail_if_exists: + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e return self.load_table(identifier=identifier) def register_table(self, identifier: Union[str, Identifier], metadata_location: str) -> Table: diff --git a/tests/catalog/integration_test_dynamodb.py b/tests/catalog/integration_test_dynamodb.py index 5ca8767d6d..dae28a0fba 100644 --- a/tests/catalog/integration_test_dynamodb.py +++ b/tests/catalog/integration_test_dynamodb.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Generator, List +from typing import Generator, List, Type import boto3 import pytest @@ -89,11 +89,29 @@ def test_create_table_with_invalid_database(test_catalog: Catalog, table_schema_ test_catalog.create_table(identifier, table_schema_nested) -def test_create_duplicated_table(test_catalog: Catalog, table_schema_nested: Schema, database_name: str, table_name: str) -> None: +@pytest.mark.parametrize( + "fail_if_exists, expected_exception", + [ + (True, TableAlreadyExistsError), + (False, None), + ], +) +def test_create_duplicated_table( + test_catalog: Catalog, + table_schema_nested: Schema, + database_name: str, + table_name: str, + fail_if_exists: bool, + expected_exception: Type[Exception], +) -> None: test_catalog.create_namespace(database_name) test_catalog.create_table((database_name, table_name), table_schema_nested) - with pytest.raises(TableAlreadyExistsError): - test_catalog.create_table((database_name, table_name), table_schema_nested) + if expected_exception: + with pytest.raises(expected_exception): + test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) + else: + table = test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) + assert table.identifier == (test_catalog.name, database_name, table_name) def test_load_table(test_catalog: Catalog, table_schema_nested: Schema, database_name: str, table_name: str) -> None: diff --git a/tests/catalog/integration_test_glue.py b/tests/catalog/integration_test_glue.py index a56e4c6aaa..7906c929c1 100644 --- a/tests/catalog/integration_test_glue.py +++ b/tests/catalog/integration_test_glue.py @@ -16,7 +16,7 @@ # under the License. import time -from typing import Any, Dict, Generator, List +from typing import Any, Dict, Generator, List, Type from uuid import uuid4 import boto3 @@ -193,11 +193,29 @@ def test_create_table_with_invalid_database(test_catalog: Catalog, table_schema_ test_catalog.create_table(identifier, table_schema_nested) -def test_create_duplicated_table(test_catalog: Catalog, table_schema_nested: Schema, table_name: str, database_name: str) -> None: +@pytest.mark.parametrize( + "fail_if_exists, expected_exception", + [ + (True, TableAlreadyExistsError), + (False, None), + ], +) +def test_create_duplicated_table( + test_catalog: Catalog, + table_schema_nested: Schema, + table_name: str, + database_name: str, + fail_if_exists: bool, + expected_exception: Type[Exception], +) -> None: test_catalog.create_namespace(database_name) test_catalog.create_table((database_name, table_name), table_schema_nested) - with pytest.raises(TableAlreadyExistsError): - test_catalog.create_table((database_name, table_name), table_schema_nested) + if expected_exception: + with pytest.raises(expected_exception): + test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) + else: + table = test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) + assert table.identifier == (CATALOG_NAME, database_name, table_name) def test_load_table(test_catalog: Catalog, table_schema_nested: Schema, table_name: str, database_name: str) -> None: diff --git a/tests/catalog/test_base.py b/tests/catalog/test_base.py index d15c90fee3..edbd5c04fc 100644 --- a/tests/catalog/test_base.py +++ b/tests/catalog/test_base.py @@ -79,6 +79,7 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, + fail_if_exists: bool = True, ) -> Table: schema: Schema = self._convert_schema_if_needed(schema) # type: ignore @@ -86,7 +87,10 @@ def create_table( namespace = Catalog.namespace_from(identifier) if identifier in self.__tables: - raise TableAlreadyExistsError(f"Table already exists: {identifier}") + if fail_if_exists: + raise TableAlreadyExistsError(f"Table already exists: {identifier}") + else: + return self.__tables[identifier] else: if namespace not in self.__namespaces: self.__namespaces[namespace] = {} diff --git a/tests/catalog/test_dynamodb.py b/tests/catalog/test_dynamodb.py index 2cb7c46a15..32f2bf976b 100644 --- a/tests/catalog/test_dynamodb.py +++ b/tests/catalog/test_dynamodb.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import List +from typing import List, Type from unittest import mock import boto3 @@ -164,16 +164,33 @@ def test_create_table_with_no_database( test_catalog.create_table(identifier=identifier, schema=table_schema_nested) +@pytest.mark.parametrize( + "fail_if_exists, expected_exception", + [ + (True, TableAlreadyExistsError), + (False, None), + ], +) @mock_aws def test_create_duplicated_table( - _bucket_initialize: None, moto_endpoint_url: str, table_schema_nested: Schema, database_name: str, table_name: str + _bucket_initialize: None, + moto_endpoint_url: str, + table_schema_nested: Schema, + database_name: str, + table_name: str, + fail_if_exists: bool, + expected_exception: Type[Exception], ) -> None: identifier = (database_name, table_name) test_catalog = DynamoDbCatalog("test_ddb_catalog", **{"warehouse": f"s3://{BUCKET_NAME}", "s3.endpoint": moto_endpoint_url}) test_catalog.create_namespace(namespace=database_name) test_catalog.create_table(identifier, table_schema_nested) - with pytest.raises(TableAlreadyExistsError): - test_catalog.create_table(identifier, table_schema_nested) + if fail_if_exists: + with pytest.raises(expected_exception): + test_catalog.create_table(identifier, table_schema_nested, fail_if_exists=fail_if_exists) + else: + table = test_catalog.create_table(identifier, table_schema_nested, fail_if_exists=fail_if_exists) + assert table.identifier == ("test_ddb_catalog",) + identifier @mock_aws diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 248cc14d88..409e03dca5 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -16,7 +16,7 @@ # under the License. # pylint: disable=redefined-outer-name,unused-argument import os -from typing import Any, Dict, cast +from typing import Any, Dict, Type, cast from unittest import mock import pytest @@ -489,7 +489,20 @@ def test_create_table_200( assert actual == expected -def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> None: +@pytest.mark.parametrize( + "fail_if_exists, expected_exception", + [ + (True, TableAlreadyExistsError), + (False, None), + ], +) +def test_create_table_409( + rest_mock: Mocker, + example_table_metadata_with_snapshot_v1_rest_json: Dict[str, Any], + table_schema_simple: Schema, + fail_if_exists: bool, + expected_exception: Type[Exception], +) -> None: rest_mock.post( f"{TEST_URI}v1/namespaces/fokko/tables", json={ @@ -502,9 +515,15 @@ def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> Non status_code=409, request_headers=TEST_HEADERS, ) + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/tables/fokko2", + json=example_table_metadata_with_snapshot_v1_rest_json, + status_code=200, + request_headers=TEST_HEADERS, + ) - with pytest.raises(TableAlreadyExistsError) as e: - RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).create_table( + def _create_table(_fail_if_exists: bool) -> Table: + return RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).create_table( identifier=("fokko", "fokko2"), schema=table_schema_simple, location=None, @@ -513,8 +532,16 @@ def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> Non ), sort_order=SortOrder(SortField(source_id=2, transform=IdentityTransform())), properties={"owner": "fokko"}, + fail_if_exists=_fail_if_exists, ) - assert "Table already exists" in str(e.value) + + if expected_exception: + with pytest.raises(expected_exception) as e: + _create_table(_fail_if_exists=fail_if_exists) + assert "Table already exists" in str(e.value) + else: + table = _create_table(_fail_if_exists=fail_if_exists) + assert table.identifier == ("rest", "fokko", "fokko2") def test_register_table_200( diff --git a/tests/catalog/test_sql.py b/tests/catalog/test_sql.py index 1d127378be..8c737116d0 100644 --- a/tests/catalog/test_sql.py +++ b/tests/catalog/test_sql.py @@ -17,7 +17,7 @@ import os from pathlib import Path -from typing import Generator, List +from typing import Generator, List, Type import pyarrow as pa import pytest @@ -245,12 +245,29 @@ def test_create_table_with_default_warehouse_location( lazy_fixture('catalog_sqlite'), ], ) -def test_create_duplicated_table(catalog: SqlCatalog, table_schema_nested: Schema, random_identifier: Identifier) -> None: +@pytest.mark.parametrize( + 'fail_if_exists, expected_exception', + [ + (True, TableAlreadyExistsError), + (False, None), + ], +) +def test_create_duplicated_table( + catalog: SqlCatalog, + table_schema_nested: Schema, + random_identifier: Identifier, + fail_if_exists: bool, + expected_exception: Type[Exception], +) -> None: database_name, _table_name = random_identifier catalog.create_namespace(database_name) catalog.create_table(random_identifier, table_schema_nested) - with pytest.raises(TableAlreadyExistsError): - catalog.create_table(random_identifier, table_schema_nested) + if expected_exception: + with pytest.raises(expected_exception): + catalog.create_table(random_identifier, table_schema_nested, fail_if_exists=fail_if_exists) + else: + table = catalog.create_table(random_identifier, table_schema_nested, fail_if_exists=fail_if_exists) + assert table.identifier == (catalog.name,) + random_identifier @pytest.mark.parametrize( From e4620c476dec3d95554ce533f3be56747dcf4fbb Mon Sep 17 00:00:00 2001 From: hussein-awala Date: Tue, 20 Feb 2024 22:19:57 +0100 Subject: [PATCH 2/4] create create_table_if_not_exists method --- pyiceberg/catalog/__init__.py | 32 +++++++++- pyiceberg/catalog/dynamodb.py | 5 +- pyiceberg/catalog/glue.py | 17 +----- pyiceberg/catalog/hive.py | 7 +-- pyiceberg/catalog/noop.py | 1 - pyiceberg/catalog/rest.py | 14 ++--- pyiceberg/catalog/sql.py | 6 +- tests/catalog/integration_test_dynamodb.py | 35 ++++------- tests/catalog/integration_test_glue.py | 35 ++++------- tests/catalog/test_base.py | 6 +- tests/catalog/test_dynamodb.py | 37 +++++------- tests/catalog/test_rest.py | 70 ++++++++++++---------- tests/catalog/test_sql.py | 34 +++++------ 13 files changed, 139 insertions(+), 160 deletions(-) diff --git a/pyiceberg/catalog/__init__.py b/pyiceberg/catalog/__init__.py index 9562891197..f7e34ab23c 100644 --- a/pyiceberg/catalog/__init__.py +++ b/pyiceberg/catalog/__init__.py @@ -36,7 +36,7 @@ cast, ) -from pyiceberg.exceptions import NoSuchNamespaceError, NoSuchTableError, NotInstalledError +from pyiceberg.exceptions import NoSuchNamespaceError, NoSuchTableError, NotInstalledError, TableAlreadyExistsError from pyiceberg.io import FileIO, load_file_io from pyiceberg.manifest import ManifestFile from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionSpec @@ -297,7 +297,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: """Create a table. @@ -308,7 +307,6 @@ def create_table( partition_spec (PartitionSpec): PartitionSpec for the table. sort_order (SortOrder): SortOrder for the table. properties (Properties): Table properties that can be a string based dictionary. - fail_if_exists (bool): If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -317,6 +315,34 @@ def create_table( TableAlreadyExistsError: If a table with the name already exists. """ + def create_table_if_not_exists( + self, + identifier: Union[str, Identifier], + schema: Union[Schema, "pa.Schema"], + location: Optional[str] = None, + partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, + sort_order: SortOrder = UNSORTED_SORT_ORDER, + properties: Properties = EMPTY_DICT, + ) -> Table: + """Create a table if it does not exist. + + Args: + identifier (str | Identifier): Table identifier. + schema (Schema): Table's schema. + location (str | None): Location for the table. Optional Argument. + partition_spec (PartitionSpec): PartitionSpec for the table. + sort_order (SortOrder): SortOrder for the table. + properties (Properties): Table properties that can be a string based dictionary. + + Returns: + Table: the created table instance if the table does not exist, else the existing + table instance. + """ + try: + return self.create_table(identifier, schema, location, partition_spec, sort_order, properties) + except TableAlreadyExistsError: + return self.load_table(identifier) + @abstractmethod def load_table(self, identifier: Union[str, Identifier]) -> Table: """Load the table's metadata and returns the table instance. diff --git a/pyiceberg/catalog/dynamodb.py b/pyiceberg/catalog/dynamodb.py index 2ae8a5eb5d..d5f3b5e14c 100644 --- a/pyiceberg/catalog/dynamodb.py +++ b/pyiceberg/catalog/dynamodb.py @@ -136,7 +136,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -148,7 +147,6 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. - fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -180,8 +178,7 @@ def create_table( condition_expression=f"attribute_not_exists({DYNAMODB_COL_IDENTIFIER})", ) except ConditionalCheckFailedException as e: - if fail_if_exists: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e return self.load_table(identifier=identifier) diff --git a/pyiceberg/catalog/glue.py b/pyiceberg/catalog/glue.py index 92569da4ea..06484cb0e4 100644 --- a/pyiceberg/catalog/glue.py +++ b/pyiceberg/catalog/glue.py @@ -302,18 +302,11 @@ def _convert_glue_to_iceberg(self, glue_table: TableTypeDef) -> Table: catalog=self, ) - def _create_glue_table( - self, - database_name: str, - table_name: str, - table_input: TableInputTypeDef, - fail_if_exists: bool = True, - ) -> None: + def _create_glue_table(self, database_name: str, table_name: str, table_input: TableInputTypeDef) -> None: try: self.glue.create_table(DatabaseName=database_name, TableInput=table_input) except self.glue.exceptions.AlreadyExistsException as e: - if fail_if_exists: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e except self.glue.exceptions.EntityNotFoundException as e: raise NoSuchNamespaceError(f"Database {database_name} does not exist") from e @@ -347,7 +340,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -359,7 +351,6 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. - fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -383,9 +374,7 @@ def create_table( table_input = _construct_table_input(table_name, metadata_location, properties, metadata) database_name, table_name = self.identifier_to_database_and_table(identifier) - self._create_glue_table( - database_name=database_name, table_name=table_name, table_input=table_input, fail_if_exists=fail_if_exists - ) + self._create_glue_table(database_name=database_name, table_name=table_name, table_input=table_input) return self.load_table(identifier=identifier) diff --git a/pyiceberg/catalog/hive.py b/pyiceberg/catalog/hive.py index bf4c5fbddf..aba3c173e6 100644 --- a/pyiceberg/catalog/hive.py +++ b/pyiceberg/catalog/hive.py @@ -269,7 +269,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: """Create a table. @@ -280,7 +279,6 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. - fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -323,10 +321,7 @@ def create_table( open_client.create_table(tbl) hive_table = open_client.get_table(dbname=database_name, tbl_name=table_name) except AlreadyExistsException as e: - if fail_if_exists: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e - else: - hive_table = self.load_table(identifier) + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e return self._convert_hive_into_iceberg(hive_table, io) diff --git a/pyiceberg/catalog/noop.py b/pyiceberg/catalog/noop.py index 05f8ce94f3..a8b7154621 100644 --- a/pyiceberg/catalog/noop.py +++ b/pyiceberg/catalog/noop.py @@ -47,7 +47,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: raise NotImplementedError diff --git a/pyiceberg/catalog/rest.py b/pyiceberg/catalog/rest.py index f6ff25a3a5..b4b9f722b5 100644 --- a/pyiceberg/catalog/rest.py +++ b/pyiceberg/catalog/rest.py @@ -446,7 +446,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: iceberg_schema = self._convert_schema_if_needed(schema) fresh_schema = assign_fresh_schema_ids(iceberg_schema) @@ -469,16 +468,11 @@ def create_table( ) try: response.raise_for_status() - table_response = TableResponse(**response.json()) - return self._response_to_table(self.identifier_to_tuple(identifier), table_response) except HTTPError as exc: - try: - self._handle_non_200_response(exc, {409: TableAlreadyExistsError}) - except TableAlreadyExistsError: - if fail_if_exists: - raise - return self.load_table(identifier) - raise + self._handle_non_200_response(exc, {409: TableAlreadyExistsError}) + + table_response = TableResponse(**response.json()) + return self._response_to_table(self.identifier_to_tuple(identifier), table_response) def register_table(self, identifier: Union[str, Identifier], metadata_location: str) -> Table: """Register a new table using existing metadata. diff --git a/pyiceberg/catalog/sql.py b/pyiceberg/catalog/sql.py index 686e9d6cd9..62a2dac54a 100644 --- a/pyiceberg/catalog/sql.py +++ b/pyiceberg/catalog/sql.py @@ -153,7 +153,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: """ Create an Iceberg table. @@ -165,7 +164,6 @@ def create_table( partition_spec: PartitionSpec for the table. sort_order: SortOrder for the table. properties: Table properties that can be a string based dictionary. - fail_if_exists: If True, raise an error if the table already exists. Returns: Table: the created table instance. @@ -202,8 +200,8 @@ def create_table( ) session.commit() except IntegrityError as e: - if fail_if_exists: - raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + raise TableAlreadyExistsError(f"Table {database_name}.{table_name} already exists") from e + return self.load_table(identifier=identifier) def register_table(self, identifier: Union[str, Identifier], metadata_location: str) -> Table: diff --git a/tests/catalog/integration_test_dynamodb.py b/tests/catalog/integration_test_dynamodb.py index dae28a0fba..591e489b83 100644 --- a/tests/catalog/integration_test_dynamodb.py +++ b/tests/catalog/integration_test_dynamodb.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Generator, List, Type +from typing import Generator, List import boto3 import pytest @@ -89,29 +89,20 @@ def test_create_table_with_invalid_database(test_catalog: Catalog, table_schema_ test_catalog.create_table(identifier, table_schema_nested) -@pytest.mark.parametrize( - "fail_if_exists, expected_exception", - [ - (True, TableAlreadyExistsError), - (False, None), - ], -) -def test_create_duplicated_table( - test_catalog: Catalog, - table_schema_nested: Schema, - database_name: str, - table_name: str, - fail_if_exists: bool, - expected_exception: Type[Exception], -) -> None: +def test_create_duplicated_table(test_catalog: Catalog, table_schema_nested: Schema, database_name: str, table_name: str) -> None: test_catalog.create_namespace(database_name) test_catalog.create_table((database_name, table_name), table_schema_nested) - if expected_exception: - with pytest.raises(expected_exception): - test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) - else: - table = test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) - assert table.identifier == (test_catalog.name, database_name, table_name) + with pytest.raises(TableAlreadyExistsError): + test_catalog.create_table((database_name, table_name), table_schema_nested) + + +def test_create_table_if_not_exists_duplicated_table( + test_catalog: Catalog, table_schema_nested: Schema, database_name: str, table_name: str +) -> None: + test_catalog.create_namespace(database_name) + table1 = test_catalog.create_table((database_name, table_name), table_schema_nested) + table2 = test_catalog.create_table_if_not_exists((database_name, table_name), table_schema_nested) + assert table1.identifier == table2.identifier def test_load_table(test_catalog: Catalog, table_schema_nested: Schema, database_name: str, table_name: str) -> None: diff --git a/tests/catalog/integration_test_glue.py b/tests/catalog/integration_test_glue.py index 7906c929c1..a685b7da7b 100644 --- a/tests/catalog/integration_test_glue.py +++ b/tests/catalog/integration_test_glue.py @@ -16,7 +16,7 @@ # under the License. import time -from typing import Any, Dict, Generator, List, Type +from typing import Any, Dict, Generator, List from uuid import uuid4 import boto3 @@ -193,29 +193,20 @@ def test_create_table_with_invalid_database(test_catalog: Catalog, table_schema_ test_catalog.create_table(identifier, table_schema_nested) -@pytest.mark.parametrize( - "fail_if_exists, expected_exception", - [ - (True, TableAlreadyExistsError), - (False, None), - ], -) -def test_create_duplicated_table( - test_catalog: Catalog, - table_schema_nested: Schema, - table_name: str, - database_name: str, - fail_if_exists: bool, - expected_exception: Type[Exception], -) -> None: +def test_create_duplicated_table(test_catalog: Catalog, table_schema_nested: Schema, table_name: str, database_name: str) -> None: test_catalog.create_namespace(database_name) test_catalog.create_table((database_name, table_name), table_schema_nested) - if expected_exception: - with pytest.raises(expected_exception): - test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) - else: - table = test_catalog.create_table((database_name, table_name), table_schema_nested, fail_if_exists=fail_if_exists) - assert table.identifier == (CATALOG_NAME, database_name, table_name) + with pytest.raises(TableAlreadyExistsError): + test_catalog.create_table((database_name, table_name), table_schema_nested) + + +def test_create_table_if_not_exists_duplicated_table( + test_catalog: Catalog, table_schema_nested: Schema, table_name: str, database_name: str +) -> None: + test_catalog.create_namespace(database_name) + table1 = test_catalog.create_table((database_name, table_name), table_schema_nested) + table2 = test_catalog.create_table_if_not_exists((database_name, table_name), table_schema_nested) + assert table1.identifier == table2.identifier def test_load_table(test_catalog: Catalog, table_schema_nested: Schema, table_name: str, database_name: str) -> None: diff --git a/tests/catalog/test_base.py b/tests/catalog/test_base.py index edbd5c04fc..d15c90fee3 100644 --- a/tests/catalog/test_base.py +++ b/tests/catalog/test_base.py @@ -79,7 +79,6 @@ def create_table( partition_spec: PartitionSpec = UNPARTITIONED_PARTITION_SPEC, sort_order: SortOrder = UNSORTED_SORT_ORDER, properties: Properties = EMPTY_DICT, - fail_if_exists: bool = True, ) -> Table: schema: Schema = self._convert_schema_if_needed(schema) # type: ignore @@ -87,10 +86,7 @@ def create_table( namespace = Catalog.namespace_from(identifier) if identifier in self.__tables: - if fail_if_exists: - raise TableAlreadyExistsError(f"Table already exists: {identifier}") - else: - return self.__tables[identifier] + raise TableAlreadyExistsError(f"Table already exists: {identifier}") else: if namespace not in self.__namespaces: self.__namespaces[namespace] = {} diff --git a/tests/catalog/test_dynamodb.py b/tests/catalog/test_dynamodb.py index 32f2bf976b..218b0e8be7 100644 --- a/tests/catalog/test_dynamodb.py +++ b/tests/catalog/test_dynamodb.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -from typing import List, Type +from typing import List from unittest import mock import boto3 @@ -164,33 +164,28 @@ def test_create_table_with_no_database( test_catalog.create_table(identifier=identifier, schema=table_schema_nested) -@pytest.mark.parametrize( - "fail_if_exists, expected_exception", - [ - (True, TableAlreadyExistsError), - (False, None), - ], -) @mock_aws def test_create_duplicated_table( - _bucket_initialize: None, - moto_endpoint_url: str, - table_schema_nested: Schema, - database_name: str, - table_name: str, - fail_if_exists: bool, - expected_exception: Type[Exception], + _bucket_initialize: None, moto_endpoint_url: str, table_schema_nested: Schema, database_name: str, table_name: str ) -> None: identifier = (database_name, table_name) test_catalog = DynamoDbCatalog("test_ddb_catalog", **{"warehouse": f"s3://{BUCKET_NAME}", "s3.endpoint": moto_endpoint_url}) test_catalog.create_namespace(namespace=database_name) test_catalog.create_table(identifier, table_schema_nested) - if fail_if_exists: - with pytest.raises(expected_exception): - test_catalog.create_table(identifier, table_schema_nested, fail_if_exists=fail_if_exists) - else: - table = test_catalog.create_table(identifier, table_schema_nested, fail_if_exists=fail_if_exists) - assert table.identifier == ("test_ddb_catalog",) + identifier + with pytest.raises(TableAlreadyExistsError): + test_catalog.create_table(identifier, table_schema_nested) + + +@mock_aws +def test_create_table_if_not_exists_duplicated_table( + _bucket_initialize: None, moto_endpoint_url: str, table_schema_nested: Schema, database_name: str, table_name: str +) -> None: + identifier = (database_name, table_name) + test_catalog = DynamoDbCatalog("test_ddb_catalog", **{"warehouse": f"s3://{BUCKET_NAME}", "s3.endpoint": moto_endpoint_url}) + test_catalog.create_namespace(namespace=database_name) + table1 = test_catalog.create_table(identifier, table_schema_nested) + table2 = test_catalog.create_table_if_not_exists(identifier, table_schema_nested) + assert table1.identifier == table2.identifier @mock_aws diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 409e03dca5..c894dddf74 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -16,7 +16,7 @@ # under the License. # pylint: disable=redefined-outer-name,unused-argument import os -from typing import Any, Dict, Type, cast +from typing import Any, Dict, cast from unittest import mock import pytest @@ -489,20 +489,7 @@ def test_create_table_200( assert actual == expected -@pytest.mark.parametrize( - "fail_if_exists, expected_exception", - [ - (True, TableAlreadyExistsError), - (False, None), - ], -) -def test_create_table_409( - rest_mock: Mocker, - example_table_metadata_with_snapshot_v1_rest_json: Dict[str, Any], - table_schema_simple: Schema, - fail_if_exists: bool, - expected_exception: Type[Exception], -) -> None: +def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> None: rest_mock.post( f"{TEST_URI}v1/namespaces/fokko/tables", json={ @@ -515,15 +502,9 @@ def test_create_table_409( status_code=409, request_headers=TEST_HEADERS, ) - rest_mock.get( - f"{TEST_URI}v1/namespaces/fokko/tables/fokko2", - json=example_table_metadata_with_snapshot_v1_rest_json, - status_code=200, - request_headers=TEST_HEADERS, - ) - def _create_table(_fail_if_exists: bool) -> Table: - return RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).create_table( + with pytest.raises(TableAlreadyExistsError) as e: + RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).create_table( identifier=("fokko", "fokko2"), schema=table_schema_simple, location=None, @@ -532,16 +513,43 @@ def _create_table(_fail_if_exists: bool) -> Table: ), sort_order=SortOrder(SortField(source_id=2, transform=IdentityTransform())), properties={"owner": "fokko"}, - fail_if_exists=_fail_if_exists, ) + assert "Table already exists" in str(e.value) + + +def test_create_table_if_not_exists_200( + rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: Dict[str, Any] +) -> None: + rest_mock.post( + f"{TEST_URI}v1/namespaces/fokko/tables", + json={ + "error": { + "message": "Table already exists: fokko.already_exists in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", + "type": "AlreadyExistsException", + "code": 409, + } + }, + status_code=409, + request_headers=TEST_HEADERS, + ) - if expected_exception: - with pytest.raises(expected_exception) as e: - _create_table(_fail_if_exists=fail_if_exists) - assert "Table already exists" in str(e.value) - else: - table = _create_table(_fail_if_exists=fail_if_exists) - assert table.identifier == ("rest", "fokko", "fokko2") + catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) + actual = catalog.create_table_if_not_exists( + identifier=("fokko", "fokko2"), + schema=table_schema_simple, + location=None, + partition_spec=PartitionSpec(PartitionField(source_id=1, field_id=1000, transform=TruncateTransform(width=3), name="id")), + sort_order=SortOrder(SortField(source_id=2, transform=IdentityTransform())), + properties={"owner": "fokko"}, + ) + expected = Table( + identifier=("rest", "fokko", "fokko2"), + metadata_location=example_table_metadata_no_snapshot_v1_rest_json["metadata-location"], + metadata=TableMetadataV1(**example_table_metadata_no_snapshot_v1_rest_json["metadata"]), + io=load_file_io(), + catalog=catalog, + ) + assert actual == expected def test_register_table_200( diff --git a/tests/catalog/test_sql.py b/tests/catalog/test_sql.py index 4f938b76ee..bc75eb6300 100644 --- a/tests/catalog/test_sql.py +++ b/tests/catalog/test_sql.py @@ -17,7 +17,7 @@ import os from pathlib import Path -from typing import Generator, List, Type +from typing import Generator, List import pyarrow as pa import pytest @@ -239,29 +239,29 @@ def test_create_table_with_default_warehouse_location( lazy_fixture('catalog_sqlite'), ], ) +def test_create_duplicated_table(catalog: SqlCatalog, table_schema_nested: Schema, random_identifier: Identifier) -> None: + database_name, _table_name = random_identifier + catalog.create_namespace(database_name) + catalog.create_table(random_identifier, table_schema_nested) + with pytest.raises(TableAlreadyExistsError): + catalog.create_table(random_identifier, table_schema_nested) + + @pytest.mark.parametrize( - 'fail_if_exists, expected_exception', + 'catalog', [ - (True, TableAlreadyExistsError), - (False, None), + lazy_fixture('catalog_memory'), + lazy_fixture('catalog_sqlite'), ], ) -def test_create_duplicated_table( - catalog: SqlCatalog, - table_schema_nested: Schema, - random_identifier: Identifier, - fail_if_exists: bool, - expected_exception: Type[Exception], +def test_create_table_if_not_exists_duplicated_table( + catalog: SqlCatalog, table_schema_nested: Schema, random_identifier: Identifier ) -> None: database_name, _table_name = random_identifier catalog.create_namespace(database_name) - catalog.create_table(random_identifier, table_schema_nested) - if expected_exception: - with pytest.raises(expected_exception): - catalog.create_table(random_identifier, table_schema_nested, fail_if_exists=fail_if_exists) - else: - table = catalog.create_table(random_identifier, table_schema_nested, fail_if_exists=fail_if_exists) - assert table.identifier == (catalog.name,) + random_identifier + table1 = catalog.create_table(random_identifier, table_schema_nested) + table2 = catalog.create_table_if_not_exists(random_identifier, table_schema_nested) + assert table1.identifier == table2.identifier @pytest.mark.parametrize( From 405c4abc29976cfe4b1e3aa6470cb2eb3a45e325 Mon Sep 17 00:00:00 2001 From: hussein-awala Date: Tue, 20 Feb 2024 22:49:20 +0100 Subject: [PATCH 3/4] fix reset test --- tests/catalog/test_rest.py | 61 ++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index f4401d5ee8..6d0f39a2aa 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -16,7 +16,7 @@ # under the License. # pylint: disable=redefined-outer-name,unused-argument import os -from typing import Any, Dict, cast +from typing import Any, Callable, Dict, cast from unittest import mock import pytest @@ -563,36 +563,59 @@ def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> Non def test_create_table_if_not_exists_200( rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: Dict[str, Any] ) -> None: + def json_callback() -> Callable[[Any, Any], dict[str, Any]]: + call_count = 0 + + def callback(request: Any, context: Any) -> dict[str, Any]: + nonlocal call_count + call_count += 1 + + if call_count == 1: + context.status_code = 200 + return example_table_metadata_no_snapshot_v1_rest_json + else: + context.status_code = 409 + return { + "error": { + "message": "Table already exists: fokko.already_exists in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", + "type": "AlreadyExistsException", + "code": 409, + } + } + + return callback + rest_mock.post( f"{TEST_URI}v1/namespaces/fokko/tables", - json={ - "error": { - "message": "Table already exists: fokko.already_exists in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", - "type": "AlreadyExistsException", - "code": 409, - } - }, - status_code=409, + json=json_callback(), + request_headers=TEST_HEADERS, + ) + rest_mock.get( + f"{TEST_URI}v1/namespaces/fokko/tables/fokko2", + json=example_table_metadata_no_snapshot_v1_rest_json, + status_code=200, request_headers=TEST_HEADERS, ) - catalog = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN) - actual = catalog.create_table_if_not_exists( + table1 = catalog.create_table( identifier=("fokko", "fokko2"), schema=table_schema_simple, location=None, - partition_spec=PartitionSpec(PartitionField(source_id=1, field_id=1000, transform=TruncateTransform(width=3), name="id")), + partition_spec=PartitionSpec( + PartitionField(source_id=1, field_id=1000, transform=TruncateTransform(width=3), name="id"), spec_id=1 + ), sort_order=SortOrder(SortField(source_id=2, transform=IdentityTransform())), properties={"owner": "fokko"}, ) - expected = Table( - identifier=("rest", "fokko", "fokko2"), - metadata_location=example_table_metadata_no_snapshot_v1_rest_json["metadata-location"], - metadata=TableMetadataV1(**example_table_metadata_no_snapshot_v1_rest_json["metadata"]), - io=load_file_io(), - catalog=catalog, + table2 = catalog.create_table_if_not_exists( + identifier=("fokko", "fokko2"), + schema=table_schema_simple, + location=None, + partition_spec=PartitionSpec(PartitionField(source_id=1, field_id=1000, transform=TruncateTransform(width=3), name="id")), + sort_order=SortOrder(SortField(source_id=2, transform=IdentityTransform())), + properties={"owner": "fokko"}, ) - assert actual == expected + assert table1 == table2 def test_create_table_419(rest_mock: Mocker, table_schema_simple: Schema) -> None: From 21b3ee331ae53c43d028c75ed422a8ed46cdd274 Mon Sep 17 00:00:00 2001 From: hussein-awala Date: Tue, 20 Feb 2024 22:54:24 +0100 Subject: [PATCH 4/4] fix mypy check --- tests/catalog/test_rest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 6d0f39a2aa..2b698d9c1e 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -563,10 +563,10 @@ def test_create_table_409(rest_mock: Mocker, table_schema_simple: Schema) -> Non def test_create_table_if_not_exists_200( rest_mock: Mocker, table_schema_simple: Schema, example_table_metadata_no_snapshot_v1_rest_json: Dict[str, Any] ) -> None: - def json_callback() -> Callable[[Any, Any], dict[str, Any]]: + def json_callback() -> Callable[[Any, Any], Dict[str, Any]]: call_count = 0 - def callback(request: Any, context: Any) -> dict[str, Any]: + def callback(request: Any, context: Any) -> Dict[str, Any]: nonlocal call_count call_count += 1