This is an automated email from the ASF dual-hosted git repository. lzljs3620320 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push: new c913b9ed5a [Python] Reformat code for init_py file (#6159) c913b9ed5a is described below commit c913b9ed5afa5341b3fda164572369fca6d656c5 Author: ChengHui Chen <27797326+chenghuic...@users.noreply.github.com> AuthorDate: Wed Aug 27 15:22:53 2025 +0800 [Python] Reformat code for init_py file (#6159) --- paimon-python/pypaimon/api/__init__.py | 363 --------------------- paimon-python/pypaimon/api/api_request.py | 6 +- paimon-python/pypaimon/api/api_response.py | 10 +- paimon-python/pypaimon/api/auth.py | 6 +- paimon-python/pypaimon/api/client.py | 115 +------ paimon-python/pypaimon/api/resource_paths.py | 70 ++++ .../pypaimon/api/{__init__.py => rest_api.py} | 101 +----- paimon-python/pypaimon/api/rest_exception.py | 111 +++++++ paimon-python/pypaimon/api/rest_util.py | 43 +++ paimon-python/pypaimon/api/token_loader.py | 5 +- paimon-python/pypaimon/catalog/catalog.py | 2 +- .../{table => catalog}/catalog_environment.py | 7 +- paimon-python/pypaimon/catalog/catalog_factory.py | 1 + .../pypaimon/catalog/filesystem_catalog.py | 4 +- .../pypaimon/catalog/{ => rest}/property_change.py | 0 .../pypaimon/catalog/rest/rest_catalog.py | 45 +-- .../pypaimon/catalog/rest/rest_token_file_io.py | 2 +- .../pypaimon/catalog/{ => rest}/table_metadata.py | 0 paimon-python/pypaimon/common/identifier.py | 2 +- .../pypaimon/common/{rest_json.py => json_util.py} | 0 paimon-python/pypaimon/pvfs/__init__.py | 6 +- paimon-python/pypaimon/schema/schema.py | 2 +- paimon-python/pypaimon/schema/schema_manager.py | 2 +- paimon-python/pypaimon/schema/table_schema.py | 2 +- .../catalog_snapshot_commit.py | 3 +- .../renaming_snapshot_commit.py | 5 +- paimon-python/pypaimon/snapshot/snapshot.py | 2 +- .../{catalog => snapshot}/snapshot_commit.py | 4 +- .../pypaimon/snapshot/snapshot_manager.py | 2 +- paimon-python/pypaimon/table/file_store_table.py | 2 +- paimon-python/pypaimon/tests/api_test.py | 35 +- paimon-python/pypaimon/tests/predicates_test.py | 2 +- paimon-python/pypaimon/tests/pvfs_test.py | 4 +- .../pypaimon/tests/reader_append_only_test.py | 2 +- .../pypaimon/tests/reader_primary_key_test.py | 2 +- .../pypaimon/tests/rest_catalog_base_test.py | 5 +- paimon-python/pypaimon/tests/rest_server.py | 42 +-- .../pypaimon/tests/test_file_store_commit.py | 2 +- .../tests/test_rest_catalog_commit_snapshot.py | 10 +- paimon-python/pypaimon/write/commit_message.py | 1 + paimon-python/pypaimon/write/file_store_commit.py | 3 +- paimon-python/pypaimon/write/row_key_extractor.py | 1 + 42 files changed, 364 insertions(+), 668 deletions(-) diff --git a/paimon-python/pypaimon/api/__init__.py b/paimon-python/pypaimon/api/__init__.py index a384c3f123..a67d5ea255 100644 --- a/paimon-python/pypaimon/api/__init__.py +++ b/paimon-python/pypaimon/api/__init__.py @@ -14,366 +14,3 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - -import logging -from typing import Callable, Dict, List, Optional -from urllib.parse import unquote - -from pypaimon.api.api_response import (CommitTableResponse, ConfigResponse, - GetDatabaseResponse, GetTableResponse, - GetTableTokenResponse, - ListDatabasesResponse, - ListTablesResponse, PagedList, - PagedResponse) -from pypaimon.api.api_request import (AlterDatabaseRequest, - CommitTableRequest, - CreateDatabaseRequest, - CreateTableRequest, RenameTableRequest) -from pypaimon.api.auth import AuthProviderFactory, RESTAuthFunction -from pypaimon.api.client import HttpClient -from pypaimon.api.typedef import T -from pypaimon.catalog.snapshot_commit import PartitionStatistics -from pypaimon.common.config import CatalogOptions -from pypaimon.common.identifier import Identifier -from pypaimon.schema.schema import Schema -from pypaimon.snapshot.snapshot import Snapshot - - -class RESTException(Exception): - pass - - -class AlreadyExistsException(RESTException): - pass - - -class ForbiddenException(RESTException): - pass - - -class NoSuchResourceException(RESTException): - pass - - -class RESTUtil: - @staticmethod - def encode_string(value: str) -> str: - import urllib.parse - - return urllib.parse.quote(value) - - @staticmethod - def decode_string(encoded: str) -> str: - """Decode URL-encoded string""" - return unquote(encoded) - - @staticmethod - def extract_prefix_map( - options: Dict[str, str], prefix: str) -> Dict[str, str]: - result = {} - config = options - for key, value in config.items(): - if key.startswith(prefix): - new_key = key[len(prefix):] - result[new_key] = str(value) - return result - - -class ResourcePaths: - V1 = "v1" - DATABASES = "databases" - TABLES = "tables" - TABLE_DETAILS = "table-details" - - def __init__(self, prefix: str): - self.base_path = f"/{self.V1}/{prefix}".rstrip("/") - - @classmethod - def for_catalog_properties( - cls, options: dict[str, str]) -> "ResourcePaths": - prefix = options.get(CatalogOptions.PREFIX, "") - return cls(prefix) - - @staticmethod - def config() -> str: - return f"/{ResourcePaths.V1}/config" - - def databases(self) -> str: - return f"{self.base_path}/{self.DATABASES}" - - def database(self, name: str) -> str: - return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(name)}" - - def tables(self, database_name: Optional[str] = None) -> str: - if database_name: - return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}/{self.TABLES}" - return f"{self.base_path}/{self.TABLES}" - - def table(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}") - - def table_details(self, database_name: str) -> str: - return f"{self.base_path}/{self.DATABASES}/{database_name}/{self.TABLE_DETAILS}" - - def table_token(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/token") - - def rename_table(self) -> str: - return f"{self.base_path}/{self.TABLES}/rename" - - def commit_table(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/commit") - - -class RESTApi: - HEADER_PREFIX = "header." - MAX_RESULTS = "maxResults" - PAGE_TOKEN = "pageToken" - DATABASE_NAME_PATTERN = "databaseNamePattern" - TABLE_NAME_PATTERN = "tableNamePattern" - TOKEN_EXPIRATION_SAFE_TIME_MILLIS = 3_600_000 - - def __init__(self, options: Dict[str, str], config_required: bool = True): - self.logger = logging.getLogger(self.__class__.__name__) - self.client = HttpClient(options.get(CatalogOptions.URI)) - auth_provider = AuthProviderFactory.create_auth_provider(options) - base_headers = RESTUtil.extract_prefix_map(options, self.HEADER_PREFIX) - - if config_required: - warehouse = options.get(CatalogOptions.WAREHOUSE) - query_params = {} - if warehouse: - query_params[CatalogOptions.WAREHOUSE] = RESTUtil.encode_string( - warehouse) - - config_response = self.client.get_with_params( - ResourcePaths.config(), - query_params, - ConfigResponse, - RESTAuthFunction(base_headers, auth_provider), - ) - options = config_response.merge(options) - base_headers.update( - RESTUtil.extract_prefix_map(options, self.HEADER_PREFIX) - ) - - self.rest_auth_function = RESTAuthFunction(base_headers, auth_provider) - self.options = options - self.resource_paths = ResourcePaths.for_catalog_properties(options) - - def __build_paged_query_params( - max_results: Optional[int], - page_token: Optional[str], - name_patterns: Dict[str, str], - ) -> Dict[str, str]: - query_params = {} - if max_results is not None and max_results > 0: - query_params[RESTApi.MAX_RESULTS] = str(max_results) - - if page_token is not None and page_token.strip(): - query_params[RESTApi.PAGE_TOKEN] = page_token - - for key, value in name_patterns: - if key and value and key.strip() and value.strip(): - query_params[key] = value - - return query_params - - def __list_data_from_page_api( - self, page_api: Callable[[Dict[str, str]], PagedResponse[T]] - ) -> List[T]: - results = [] - query_params = {} - page_token = None - - while True: - if page_token: - query_params[RESTApi.PAGE_TOKEN] = page_token - elif RESTApi.PAGE_TOKEN in query_params: - del query_params[RESTApi.PAGE_TOKEN] - - response = page_api(query_params) - - if response.data: - results.extend(response.data()) - - page_token = response.next_page_token - - if not page_token or not response.data: - break - - return results - - def get_options(self) -> dict[str, str]: - return self.options - - def list_databases(self) -> List[str]: - return self.__list_data_from_page_api( - lambda query_params: self.client.get_with_params( - self.resource_paths.databases(), - query_params, - ListDatabasesResponse, - self.rest_auth_function, - ) - ) - - def list_databases_paged( - self, - max_results: Optional[int] = None, - page_token: Optional[str] = None, - database_name_pattern: Optional[str] = None, - ) -> PagedList[str]: - - response = self.client.get_with_params( - self.resource_paths.databases(), - self.__build_paged_query_params( - max_results, - page_token, - {self.DATABASE_NAME_PATTERN: database_name_pattern}, - ), - ListDatabasesResponse, - self.rest_auth_function, - ) - - databases = response.data() or [] - return PagedList(databases, response.get_next_page_token()) - - def create_database(self, name: str, options: Dict[str, str]) -> None: - request = CreateDatabaseRequest(name, options) - self.client.post( - self.resource_paths.databases(), request, self.rest_auth_function - ) - - def get_database(self, name: str) -> GetDatabaseResponse: - return self.client.get( - self.resource_paths.database(name), - GetDatabaseResponse, - self.rest_auth_function, - ) - - def drop_database(self, name: str) -> None: - self.client.delete( - self.resource_paths.database(name), - self.rest_auth_function) - - def alter_database( - self, - name: str, - removals: Optional[List[str]] = None, - updates: Optional[Dict[str, str]] = None, - ): - if not name or not name.strip(): - raise ValueError("Database name cannot be empty") - removals = removals or [] - updates = updates or {} - request = AlterDatabaseRequest(removals, updates) - - return self.client.post( - self.resource_paths.database(name), - request, - self.rest_auth_function) - - def list_tables(self, database_name: str) -> List[str]: - return self.__list_data_from_page_api( - lambda query_params: self.client.get_with_params( - self.resource_paths.tables(database_name), - query_params, - ListTablesResponse, - self.rest_auth_function, - ) - ) - - def list_tables_paged( - self, - database_name: str, - max_results: Optional[int] = None, - page_token: Optional[str] = None, - table_name_pattern: Optional[str] = None, - ) -> PagedList[str]: - response = self.client.get_with_params( - self.resource_paths.tables(database_name), - self.__build_paged_query_params( - max_results, page_token, {self.TABLE_NAME_PATTERN: table_name_pattern} - ), - ListTablesResponse, - self.rest_auth_function, - ) - - tables = response.data() or [] - return PagedList(tables, response.get_next_page_token()) - - def create_table(self, identifier: Identifier, schema: Schema) -> None: - request = CreateTableRequest(identifier, schema) - return self.client.post( - self.resource_paths.tables(identifier.database_name), - request, - self.rest_auth_function) - - def get_table(self, identifier: Identifier) -> GetTableResponse: - return self.client.get( - self.resource_paths.table( - identifier.database_name, - identifier.object_name), - GetTableResponse, - self.rest_auth_function, - ) - - def drop_table(self, identifier: Identifier) -> GetTableResponse: - return self.client.delete( - self.resource_paths.table( - identifier.database_name, - identifier.object_name), - self.rest_auth_function, - ) - - def rename_table(self, source_identifier: Identifier, target_identifier: Identifier) -> None: - request = RenameTableRequest(source_identifier, target_identifier) - return self.client.post( - self.resource_paths.rename_table(), - request, - self.rest_auth_function) - - def load_table_token(self, identifier: Identifier) -> GetTableTokenResponse: - return self.client.get( - self.resource_paths.table_token( - identifier.database_name, - identifier.object_name), - GetTableTokenResponse, - self.rest_auth_function, - ) - - def commit_snapshot( - self, - identifier: Identifier, - table_uuid: Optional[str], - snapshot: Snapshot, - statistics: List[PartitionStatistics] - ) -> bool: - """ - Commit snapshot for table. - - Args: - identifier: Database name and table name - table_uuid: UUID of the table to avoid wrong commit - snapshot: Snapshot for committing - statistics: Statistics for this snapshot incremental - - Returns: - True if commit success - - Raises: - NoSuchResourceException: Exception thrown on HTTP 404 means the table not exists - ForbiddenException: Exception thrown on HTTP 403 means don't have the permission for this table - """ - request = CommitTableRequest(table_uuid, snapshot, statistics) - response = self.client.post_with_response_type( - self.resource_paths.commit_table( - identifier.database_name, identifier.object_name), - request, - CommitTableResponse, - self.rest_auth_function - ) - return response.is_success() diff --git a/paimon-python/pypaimon/api/api_request.py b/paimon-python/pypaimon/api/api_request.py index cf22d9853b..d453250757 100644 --- a/paimon-python/pypaimon/api/api_request.py +++ b/paimon-python/pypaimon/api/api_request.py @@ -20,15 +20,15 @@ from abc import ABC from dataclasses import dataclass from typing import Dict, List, Optional -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.common.identifier import Identifier -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field from pypaimon.schema.schema import Schema from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import PartitionStatistics class RESTRequest(ABC): - pass + """RESTRequest""" @dataclass diff --git a/paimon-python/pypaimon/api/api_response.py b/paimon-python/pypaimon/api/api_response.py index 5658f5b351..0cc82f31b5 100644 --- a/paimon-python/pypaimon/api/api_response.py +++ b/paimon-python/pypaimon/api/api_response.py @@ -20,11 +20,9 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Dict, Generic, List, Optional -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import T, json_field from pypaimon.schema.schema import Schema -from .typedef import T - @dataclass class PagedList(Generic[T]): @@ -33,7 +31,7 @@ class PagedList(Generic[T]): class RESTResponse(ABC): - pass + """RESTResponse""" @dataclass @@ -99,11 +97,11 @@ class PagedResponse(RESTResponse, Generic[T]): @abstractmethod def data(self) -> List[T]: - pass + """data""" @abstractmethod def get_next_page_token(self) -> str: - pass + """get_next_page_token""" @dataclass diff --git a/paimon-python/pypaimon/api/auth.py b/paimon-python/pypaimon/api/auth.py index de20d96c8e..a0d62e2860 100644 --- a/paimon-python/pypaimon/api/auth.py +++ b/paimon-python/pypaimon/api/auth.py @@ -25,11 +25,11 @@ from collections import OrderedDict from datetime import datetime, timezone from typing import Dict, Optional +from pypaimon.api.token_loader import (DLFToken, DLFTokenLoader, + DLFTokenLoaderFactory) +from pypaimon.api.typedef import RESTAuthParameter from pypaimon.common.config import CatalogOptions -from .token_loader import DLFToken, DLFTokenLoader, DLFTokenLoaderFactory -from .typedef import RESTAuthParameter - class AuthProvider(ABC): diff --git a/paimon-python/pypaimon/api/client.py b/paimon-python/pypaimon/api/client.py index e7e42abe7a..9e30df598b 100644 --- a/paimon-python/pypaimon/api/client.py +++ b/paimon-python/pypaimon/api/client.py @@ -19,124 +19,39 @@ limitations under the License. import json import logging import time -import traceback import urllib.parse from abc import ABC, abstractmethod -from typing import Any, Callable, Dict, Optional, Type, TypeVar +from typing import Callable, Dict, Optional, Type, TypeVar import requests from requests.adapters import HTTPAdapter from urllib3 import Retry -from pypaimon.common.rest_json import JSON - -from .api_response import ErrorResponse -from .typedef import RESTAuthParameter +from pypaimon.api.api_response import ErrorResponse +from pypaimon.api.rest_exception import (AlreadyExistsException, + BadRequestException, + ForbiddenException, + NoSuchResourceException, + NotAuthorizedException, + NotImplementedException, + RESTException, + ServiceFailureException, + ServiceUnavailableException) +from pypaimon.api.typedef import RESTAuthParameter +from pypaimon.common.json_util import JSON T = TypeVar('T', bound='RESTResponse') class RESTRequest(ABC): - pass - - -class RESTException(Exception): - def __init__(self, message: str = None, *args: Any, cause: Optional[Exception] = None): - if message and args: - try: - formatted_message = message % args - except (TypeError, ValueError): - formatted_message = f"{message} {' '.join(str(arg) for arg in args)}" - else: - formatted_message = message or "REST API error occurred" - - super().__init__(formatted_message) - self.__cause__ = cause - - def get_cause(self) -> Optional[Exception]: - return self.__cause__ - - def get_message(self) -> str: - return str(self) - - def print_stack_trace(self) -> None: - traceback.print_exception(type(self), self, self.__traceback__) - - def get_stack_trace(self) -> str: - return ''.join(traceback.format_exception(type(self), self, self.__traceback__)) - - def __repr__(self) -> str: - if self.__cause__: - return f"{self.__class__.__name__}('{self}', caused by {type(self.__cause__).__name__}: {self.__cause__})" - return f"{self.__class__.__name__}('{self}')" - - -class BadRequestException(RESTException): - - def __init__(self, message: str = None, *args: Any): - super().__init__(message, *args) - - -class NotAuthorizedException(RESTException): - """Exception for not authorized (401)""" - - def __init__(self, message: str, *args: Any): - super().__init__(message, *args) - - -class ForbiddenException(RESTException): - """Exception for forbidden access (403)""" - - def __init__(self, message: str, *args: Any): - super().__init__(message, *args) - - -class NoSuchResourceException(RESTException): - """Exception for resource not found (404)""" - - def __init__(self, resource_type: Optional[str], resource_name: Optional[str], - message: str, *args: Any): - self.resource_type = resource_type - self.resource_name = resource_name - super().__init__(message, *args) - - -class AlreadyExistsException(RESTException): - """Exception for resource already exists (409)""" - - def __init__(self, resource_type: Optional[str], resource_name: Optional[str], - message: str, *args: Any): - self.resource_type = resource_type - self.resource_name = resource_name - super().__init__(message, *args) - - -class ServiceFailureException(RESTException): - """Exception for service failure (500)""" - - def __init__(self, message: str, *args: Any): - super().__init__(message, *args) - - -class NotImplementedException(RESTException): - """Exception for not implemented (501)""" - - def __init__(self, message: str, *args: Any): - super().__init__(message, *args) - - -class ServiceUnavailableException(RESTException): - """Exception for service unavailable (503)""" - - def __init__(self, message: str, *args: Any): - super().__init__(message, *args) + """RESTRequest""" class ErrorHandler(ABC): @abstractmethod def accept(self, error: ErrorResponse, request_id: str) -> None: - pass + """accept""" # DefaultErrorHandler implementation diff --git a/paimon-python/pypaimon/api/resource_paths.py b/paimon-python/pypaimon/api/resource_paths.py new file mode 100644 index 0000000000..0214ab0529 --- /dev/null +++ b/paimon-python/pypaimon/api/resource_paths.py @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Optional + +from pypaimon.api.rest_util import RESTUtil +from pypaimon.common.config import CatalogOptions + + +class ResourcePaths: + V1 = "v1" + DATABASES = "databases" + TABLES = "tables" + TABLE_DETAILS = "table-details" + + def __init__(self, prefix: str): + self.base_path = f"/{self.V1}/{prefix}".rstrip("/") + + @classmethod + def for_catalog_properties( + cls, options: dict[str, str]) -> "ResourcePaths": + prefix = options.get(CatalogOptions.PREFIX, "") + return cls(prefix) + + @staticmethod + def config() -> str: + return f"/{ResourcePaths.V1}/config" + + def databases(self) -> str: + return f"{self.base_path}/{self.DATABASES}" + + def database(self, name: str) -> str: + return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(name)}" + + def tables(self, database_name: Optional[str] = None) -> str: + if database_name: + return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}/{self.TABLES}" + return f"{self.base_path}/{self.TABLES}" + + def table(self, database_name: str, table_name: str) -> str: + return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" + f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}") + + def table_details(self, database_name: str) -> str: + return f"{self.base_path}/{self.DATABASES}/{database_name}/{self.TABLE_DETAILS}" + + def table_token(self, database_name: str, table_name: str) -> str: + return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" + f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/token") + + def rename_table(self) -> str: + return f"{self.base_path}/{self.TABLES}/rename" + + def commit_table(self, database_name: str, table_name: str) -> str: + return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" + f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/commit") diff --git a/paimon-python/pypaimon/api/__init__.py b/paimon-python/pypaimon/api/rest_api.py similarity index 77% copy from paimon-python/pypaimon/api/__init__.py copy to paimon-python/pypaimon/api/rest_api.py index a384c3f123..2e341733bd 100644 --- a/paimon-python/pypaimon/api/__init__.py +++ b/paimon-python/pypaimon/api/rest_api.py @@ -17,115 +17,26 @@ import logging from typing import Callable, Dict, List, Optional -from urllib.parse import unquote +from pypaimon.api.api_request import (AlterDatabaseRequest, CommitTableRequest, + CreateDatabaseRequest, + CreateTableRequest, RenameTableRequest) from pypaimon.api.api_response import (CommitTableResponse, ConfigResponse, GetDatabaseResponse, GetTableResponse, GetTableTokenResponse, ListDatabasesResponse, ListTablesResponse, PagedList, PagedResponse) -from pypaimon.api.api_request import (AlterDatabaseRequest, - CommitTableRequest, - CreateDatabaseRequest, - CreateTableRequest, RenameTableRequest) from pypaimon.api.auth import AuthProviderFactory, RESTAuthFunction from pypaimon.api.client import HttpClient +from pypaimon.api.resource_paths import ResourcePaths +from pypaimon.api.rest_util import RESTUtil from pypaimon.api.typedef import T -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.common.config import CatalogOptions from pypaimon.common.identifier import Identifier from pypaimon.schema.schema import Schema from pypaimon.snapshot.snapshot import Snapshot - - -class RESTException(Exception): - pass - - -class AlreadyExistsException(RESTException): - pass - - -class ForbiddenException(RESTException): - pass - - -class NoSuchResourceException(RESTException): - pass - - -class RESTUtil: - @staticmethod - def encode_string(value: str) -> str: - import urllib.parse - - return urllib.parse.quote(value) - - @staticmethod - def decode_string(encoded: str) -> str: - """Decode URL-encoded string""" - return unquote(encoded) - - @staticmethod - def extract_prefix_map( - options: Dict[str, str], prefix: str) -> Dict[str, str]: - result = {} - config = options - for key, value in config.items(): - if key.startswith(prefix): - new_key = key[len(prefix):] - result[new_key] = str(value) - return result - - -class ResourcePaths: - V1 = "v1" - DATABASES = "databases" - TABLES = "tables" - TABLE_DETAILS = "table-details" - - def __init__(self, prefix: str): - self.base_path = f"/{self.V1}/{prefix}".rstrip("/") - - @classmethod - def for_catalog_properties( - cls, options: dict[str, str]) -> "ResourcePaths": - prefix = options.get(CatalogOptions.PREFIX, "") - return cls(prefix) - - @staticmethod - def config() -> str: - return f"/{ResourcePaths.V1}/config" - - def databases(self) -> str: - return f"{self.base_path}/{self.DATABASES}" - - def database(self, name: str) -> str: - return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(name)}" - - def tables(self, database_name: Optional[str] = None) -> str: - if database_name: - return f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}/{self.TABLES}" - return f"{self.base_path}/{self.TABLES}" - - def table(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}") - - def table_details(self, database_name: str) -> str: - return f"{self.base_path}/{self.DATABASES}/{database_name}/{self.TABLE_DETAILS}" - - def table_token(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/token") - - def rename_table(self) -> str: - return f"{self.base_path}/{self.TABLES}/rename" - - def commit_table(self, database_name: str, table_name: str) -> str: - return (f"{self.base_path}/{self.DATABASES}/{RESTUtil.encode_string(database_name)}" - f"/{self.TABLES}/{RESTUtil.encode_string(table_name)}/commit") +from pypaimon.snapshot.snapshot_commit import PartitionStatistics class RESTApi: diff --git a/paimon-python/pypaimon/api/rest_exception.py b/paimon-python/pypaimon/api/rest_exception.py new file mode 100644 index 0000000000..738de5efdd --- /dev/null +++ b/paimon-python/pypaimon/api/rest_exception.py @@ -0,0 +1,111 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import traceback +from typing import Any, Optional + + +class RESTException(Exception): + def __init__(self, message: str = None, *args: Any, cause: Optional[Exception] = None): + if message and args: + try: + formatted_message = message % args + except (TypeError, ValueError): + formatted_message = f"{message} {' '.join(str(arg) for arg in args)}" + else: + formatted_message = message or "REST API error occurred" + + super().__init__(formatted_message) + self.__cause__ = cause + + def get_cause(self) -> Optional[Exception]: + return self.__cause__ + + def get_message(self) -> str: + return str(self) + + def print_stack_trace(self) -> None: + traceback.print_exception(type(self), self, self.__traceback__) + + def get_stack_trace(self) -> str: + return ''.join(traceback.format_exception(type(self), self, self.__traceback__)) + + def __repr__(self) -> str: + if self.__cause__: + return f"{self.__class__.__name__}('{self}', caused by {type(self.__cause__).__name__}: {self.__cause__})" + return f"{self.__class__.__name__}('{self}')" + + +class BadRequestException(RESTException): + + def __init__(self, message: str = None, *args: Any): + super().__init__(message, *args) + + +class NotAuthorizedException(RESTException): + """Exception for not authorized (401)""" + + def __init__(self, message: str, *args: Any): + super().__init__(message, *args) + + +class ForbiddenException(RESTException): + """Exception for forbidden access (403)""" + + def __init__(self, message: str, *args: Any): + super().__init__(message, *args) + + +class NoSuchResourceException(RESTException): + """Exception for resource not found (404)""" + + def __init__(self, resource_type: Optional[str], resource_name: Optional[str], + message: str, *args: Any): + self.resource_type = resource_type + self.resource_name = resource_name + super().__init__(message, *args) + + +class AlreadyExistsException(RESTException): + """Exception for resource already exists (409)""" + + def __init__(self, resource_type: Optional[str], resource_name: Optional[str], + message: str, *args: Any): + self.resource_type = resource_type + self.resource_name = resource_name + super().__init__(message, *args) + + +class ServiceFailureException(RESTException): + """Exception for service failure (500)""" + + def __init__(self, message: str, *args: Any): + super().__init__(message, *args) + + +class NotImplementedException(RESTException): + """Exception for not implemented (501)""" + + def __init__(self, message: str, *args: Any): + super().__init__(message, *args) + + +class ServiceUnavailableException(RESTException): + """Exception for service unavailable (503)""" + + def __init__(self, message: str, *args: Any): + super().__init__(message, *args) diff --git a/paimon-python/pypaimon/api/rest_util.py b/paimon-python/pypaimon/api/rest_util.py new file mode 100644 index 0000000000..6c2dd3c644 --- /dev/null +++ b/paimon-python/pypaimon/api/rest_util.py @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Dict +from urllib.parse import unquote + + +class RESTUtil: + @staticmethod + def encode_string(value: str) -> str: + import urllib.parse + + return urllib.parse.quote(value) + + @staticmethod + def decode_string(encoded: str) -> str: + """Decode URL-encoded string""" + return unquote(encoded) + + @staticmethod + def extract_prefix_map( + options: Dict[str, str], prefix: str) -> Dict[str, str]: + result = {} + config = options + for key, value in config.items(): + if key.startswith(prefix): + new_key = key[len(prefix):] + result[new_key] = str(value) + return result diff --git a/paimon-python/pypaimon/api/token_loader.py b/paimon-python/pypaimon/api/token_loader.py index 0d332c6c1f..8e65846bf8 100644 --- a/paimon-python/pypaimon/api/token_loader.py +++ b/paimon-python/pypaimon/api/token_loader.py @@ -25,10 +25,9 @@ import requests from requests.adapters import HTTPAdapter from requests.exceptions import RequestException +from pypaimon.api.client import ExponentialRetry from pypaimon.common.config import CatalogOptions -from pypaimon.common.rest_json import JSON, json_field - -from .client import ExponentialRetry +from pypaimon.common.json_util import JSON, json_field @dataclass diff --git a/paimon-python/pypaimon/catalog/catalog.py b/paimon-python/pypaimon/catalog/catalog.py index c2b1f6915e..a8e7dcd065 100644 --- a/paimon-python/pypaimon/catalog/catalog.py +++ b/paimon-python/pypaimon/catalog/catalog.py @@ -19,10 +19,10 @@ from abc import ABC, abstractmethod from typing import List, Optional, Union -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.common.identifier import Identifier from pypaimon.schema.schema import Schema from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import PartitionStatistics class Catalog(ABC): diff --git a/paimon-python/pypaimon/table/catalog_environment.py b/paimon-python/pypaimon/catalog/catalog_environment.py similarity index 93% rename from paimon-python/pypaimon/table/catalog_environment.py rename to paimon-python/pypaimon/catalog/catalog_environment.py index f025b367b3..762a42dd6a 100644 --- a/paimon-python/pypaimon/table/catalog_environment.py +++ b/paimon-python/pypaimon/catalog/catalog_environment.py @@ -15,13 +15,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ + from typing import Optional from pypaimon.catalog.catalog_loader import CatalogLoader -from pypaimon.catalog.catalog_snapshot_commit import CatalogSnapshotCommit -from pypaimon.catalog.renaming_snapshot_commit import RenamingSnapshotCommit -from pypaimon.catalog.snapshot_commit import SnapshotCommit from pypaimon.common.identifier import Identifier +from pypaimon.snapshot.catalog_snapshot_commit import CatalogSnapshotCommit +from pypaimon.snapshot.renaming_snapshot_commit import RenamingSnapshotCommit +from pypaimon.snapshot.snapshot_commit import SnapshotCommit class CatalogEnvironment: diff --git a/paimon-python/pypaimon/catalog/catalog_factory.py b/paimon-python/pypaimon/catalog/catalog_factory.py index 7bae23be7e..865ffe4766 100644 --- a/paimon-python/pypaimon/catalog/catalog_factory.py +++ b/paimon-python/pypaimon/catalog/catalog_factory.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ + from pypaimon.api.options import Options from pypaimon.catalog.catalog import Catalog from pypaimon.catalog.catalog_context import CatalogContext diff --git a/paimon-python/pypaimon/catalog/filesystem_catalog.py b/paimon-python/pypaimon/catalog/filesystem_catalog.py index 1aed6e8137..8de2850e4a 100644 --- a/paimon-python/pypaimon/catalog/filesystem_catalog.py +++ b/paimon-python/pypaimon/catalog/filesystem_catalog.py @@ -21,19 +21,19 @@ from typing import List, Optional, Union from urllib.parse import urlparse from pypaimon.catalog.catalog import Catalog +from pypaimon.catalog.catalog_environment import CatalogEnvironment from pypaimon.catalog.catalog_exception import (DatabaseAlreadyExistException, DatabaseNotExistException, TableAlreadyExistException, TableNotExistException) from pypaimon.catalog.database import Database -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.common.config import CatalogOptions from pypaimon.common.core_options import CoreOptions from pypaimon.common.file_io import FileIO from pypaimon.common.identifier import Identifier from pypaimon.schema.schema_manager import SchemaManager from pypaimon.snapshot.snapshot import Snapshot -from pypaimon.table.catalog_environment import CatalogEnvironment +from pypaimon.snapshot.snapshot_commit import PartitionStatistics from pypaimon.table.file_store_table import FileStoreTable from pypaimon.table.table import Table diff --git a/paimon-python/pypaimon/catalog/property_change.py b/paimon-python/pypaimon/catalog/rest/property_change.py similarity index 100% rename from paimon-python/pypaimon/catalog/property_change.py rename to paimon-python/pypaimon/catalog/rest/property_change.py diff --git a/paimon-python/pypaimon/catalog/rest/rest_catalog.py b/paimon-python/pypaimon/catalog/rest/rest_catalog.py index f35ee04322..31cafb2578 100644 --- a/paimon-python/pypaimon/catalog/rest/rest_catalog.py +++ b/paimon-python/pypaimon/catalog/rest/rest_catalog.py @@ -19,35 +19,36 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Union from urllib.parse import urlparse -from pypaimon.api import (CatalogOptions, - NoSuchResourceException, RESTApi) from pypaimon.api.api_response import GetTableResponse, PagedList from pypaimon.api.options import Options +from pypaimon.api.rest_api import RESTApi +from pypaimon.api.rest_exception import NoSuchResourceException from pypaimon.catalog.catalog import Catalog from pypaimon.catalog.catalog_context import CatalogContext +from pypaimon.catalog.catalog_environment import CatalogEnvironment from pypaimon.catalog.catalog_exception import TableNotExistException from pypaimon.catalog.database import Database -from pypaimon.catalog.property_change import PropertyChange +from pypaimon.catalog.rest.property_change import PropertyChange from pypaimon.catalog.rest.rest_token_file_io import RESTTokenFileIO -from pypaimon.catalog.snapshot_commit import PartitionStatistics -from pypaimon.catalog.table_metadata import TableMetadata +from pypaimon.catalog.rest.table_metadata import TableMetadata +from pypaimon.common.config import CatalogOptions from pypaimon.common.core_options import CoreOptions from pypaimon.common.file_io import FileIO from pypaimon.common.identifier import Identifier from pypaimon.schema.schema import Schema from pypaimon.schema.table_schema import TableSchema from pypaimon.snapshot.snapshot import Snapshot -from pypaimon.table.catalog_environment import CatalogEnvironment +from pypaimon.snapshot.snapshot_commit import PartitionStatistics from pypaimon.table.file_store_table import FileStoreTable class RESTCatalog(Catalog): def __init__(self, context: CatalogContext, config_required: Optional[bool] = True): self.warehouse = context.options.get(CatalogOptions.WAREHOUSE) - self.api = RESTApi(context.options.to_map(), config_required) - self.context = CatalogContext.create(Options(self.api.options), context.hadoop_conf, context.prefer_io_loader, - context.fallback_io_loader) - self.data_token_enabled = self.api.options.get(CatalogOptions.DATA_TOKEN_ENABLED) + self.rest_api = RESTApi(context.options.to_map(), config_required) + self.context = CatalogContext.create(Options(self.rest_api.options), context.hadoop_conf, + context.prefer_io_loader, context.fallback_io_loader) + self.data_token_enabled = self.rest_api.options.get(CatalogOptions.DATA_TOKEN_ENABLED) def catalog_loader(self): """ @@ -91,7 +92,7 @@ class RESTCatalog(Catalog): TableNotExistException: If the target table does not exist """ try: - return self.api.commit_snapshot(identifier, table_uuid, snapshot, statistics) + return self.rest_api.commit_snapshot(identifier, table_uuid, snapshot, statistics) except NoSuchResourceException as e: raise TableNotExistException(identifier) from e except Exception as e: @@ -99,17 +100,17 @@ class RESTCatalog(Catalog): raise RuntimeError(f"Failed to commit snapshot for table {identifier.get_full_name()}: {e}") from e def list_databases(self) -> List[str]: - return self.api.list_databases() + return self.rest_api.list_databases() def list_databases_paged(self, max_results: Optional[int] = None, page_token: Optional[str] = None, database_name_pattern: Optional[str] = None) -> PagedList[str]: - return self.api.list_databases_paged(max_results, page_token, database_name_pattern) + return self.rest_api.list_databases_paged(max_results, page_token, database_name_pattern) def create_database(self, name: str, ignore_if_exists: bool, properties: Dict[str, str] = None): - self.api.create_database(name, properties) + self.rest_api.create_database(name, properties) def get_database(self, name: str) -> Database: - response = self.api.get_database(name) + response = self.rest_api.get_database(name) options = response.options options[Catalog.DB_LOCATION_PROP] = response.location response.put_audit_options_to(options) @@ -117,14 +118,14 @@ class RESTCatalog(Catalog): return Database(name, options) def drop_database(self, name: str): - self.api.drop_database(name) + self.rest_api.drop_database(name) def alter_database(self, name: str, changes: List[PropertyChange]): set_properties, remove_keys = PropertyChange.get_set_properties_to_remove_keys(changes) - self.api.alter_database(name, list(remove_keys), set_properties) + self.rest_api.alter_database(name, list(remove_keys), set_properties) def list_tables(self, database_name: str) -> List[str]: - return self.api.list_tables(database_name) + return self.rest_api.list_tables(database_name) def list_tables_paged( self, @@ -133,7 +134,7 @@ class RESTCatalog(Catalog): page_token: Optional[str] = None, table_name_pattern: Optional[str] = None ) -> PagedList[str]: - return self.api.list_tables_paged( + return self.rest_api.list_tables_paged( database_name, max_results, page_token, @@ -153,15 +154,15 @@ class RESTCatalog(Catalog): def create_table(self, identifier: Union[str, Identifier], schema: Schema, ignore_if_exists: bool): if not isinstance(identifier, Identifier): identifier = Identifier.from_string(identifier) - self.api.create_table(identifier, schema) + self.rest_api.create_table(identifier, schema) def drop_table(self, identifier: Union[str, Identifier]): if not isinstance(identifier, Identifier): identifier = Identifier.from_string(identifier) - self.api.drop_table(identifier) + self.rest_api.drop_table(identifier) def load_table_metadata(self, identifier: Identifier) -> TableMetadata: - response = self.api.get_table(identifier) + response = self.rest_api.get_table(identifier) return self.to_table_metadata(identifier.get_database_name(), response) def to_table_metadata(self, db: str, response: GetTableResponse) -> TableMetadata: diff --git a/paimon-python/pypaimon/catalog/rest/rest_token_file_io.py b/paimon-python/pypaimon/catalog/rest/rest_token_file_io.py index 6142fa6373..b9671c8ae9 100644 --- a/paimon-python/pypaimon/catalog/rest/rest_token_file_io.py +++ b/paimon-python/pypaimon/catalog/rest/rest_token_file_io.py @@ -23,7 +23,7 @@ from typing import Optional from pyarrow._fs import FileSystem -from pypaimon.api import RESTApi +from pypaimon.api.rest_api import RESTApi from pypaimon.catalog.rest.rest_token import RESTToken from pypaimon.common.file_io import FileIO from pypaimon.common.identifier import Identifier diff --git a/paimon-python/pypaimon/catalog/table_metadata.py b/paimon-python/pypaimon/catalog/rest/table_metadata.py similarity index 100% rename from paimon-python/pypaimon/catalog/table_metadata.py rename to paimon-python/pypaimon/catalog/rest/table_metadata.py diff --git a/paimon-python/pypaimon/common/identifier.py b/paimon-python/pypaimon/common/identifier.py index 3a27870516..d3a4fcda7f 100644 --- a/paimon-python/pypaimon/common/identifier.py +++ b/paimon-python/pypaimon/common/identifier.py @@ -18,7 +18,7 @@ from dataclasses import dataclass from typing import Optional -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field SYSTEM_TABLE_SPLITTER = '$' SYSTEM_BRANCH_PREFIX = 'branch-' diff --git a/paimon-python/pypaimon/common/rest_json.py b/paimon-python/pypaimon/common/json_util.py similarity index 100% rename from paimon-python/pypaimon/common/rest_json.py rename to paimon-python/pypaimon/common/json_util.py diff --git a/paimon-python/pypaimon/pvfs/__init__.py b/paimon-python/pypaimon/pvfs/__init__.py index 78b3b73691..7e60a558d1 100644 --- a/paimon-python/pypaimon/pvfs/__init__.py +++ b/paimon-python/pypaimon/pvfs/__init__.py @@ -29,10 +29,12 @@ from fsspec import AbstractFileSystem from fsspec.implementations.local import LocalFileSystem from readerwriterlock import rwlock -from pypaimon.api import (GetTableResponse, GetTableTokenResponse, Identifier, - RESTApi, Schema) +from pypaimon.api.api_response import GetTableTokenResponse, GetTableResponse from pypaimon.api.client import AlreadyExistsException, NoSuchResourceException +from pypaimon.api.rest_api import RESTApi from pypaimon.common.config import CatalogOptions, OssOptions, PVFSOptions +from pypaimon.common.identifier import Identifier +from pypaimon.schema.schema import Schema PROTOCOL_NAME = "pvfs" diff --git a/paimon-python/pypaimon/schema/schema.py b/paimon-python/pypaimon/schema/schema.py index 20e0720087..965fe2255b 100644 --- a/paimon-python/pypaimon/schema/schema.py +++ b/paimon-python/pypaimon/schema/schema.py @@ -20,7 +20,7 @@ from typing import Dict, List, Optional import pyarrow as pa -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field from pypaimon.schema.data_types import DataField, PyarrowFieldParser diff --git a/paimon-python/pypaimon/schema/schema_manager.py b/paimon-python/pypaimon/schema/schema_manager.py index f03b9e111b..2af8481b49 100644 --- a/paimon-python/pypaimon/schema/schema_manager.py +++ b/paimon-python/pypaimon/schema/schema_manager.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Optional from pypaimon.common.file_io import FileIO -from pypaimon.common.rest_json import JSON +from pypaimon.common.json_util import JSON from pypaimon.schema.schema import Schema from pypaimon.schema.table_schema import TableSchema diff --git a/paimon-python/pypaimon/schema/table_schema.py b/paimon-python/pypaimon/schema/table_schema.py index dc1872deb6..852ef8c4ea 100644 --- a/paimon-python/pypaimon/schema/table_schema.py +++ b/paimon-python/pypaimon/schema/table_schema.py @@ -24,7 +24,7 @@ from typing import Dict, List, Optional from pypaimon.common.core_options import CoreOptions from pypaimon.common.file_io import FileIO -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field from pypaimon.schema.data_types import DataField from pypaimon.schema.schema import Schema diff --git a/paimon-python/pypaimon/catalog/catalog_snapshot_commit.py b/paimon-python/pypaimon/snapshot/catalog_snapshot_commit.py similarity index 95% rename from paimon-python/pypaimon/catalog/catalog_snapshot_commit.py rename to paimon-python/pypaimon/snapshot/catalog_snapshot_commit.py index cc1ed258f8..26796f7766 100644 --- a/paimon-python/pypaimon/catalog/catalog_snapshot_commit.py +++ b/paimon-python/pypaimon/snapshot/catalog_snapshot_commit.py @@ -19,9 +19,10 @@ from typing import List from pypaimon.catalog.catalog import Catalog -from pypaimon.catalog.snapshot_commit import PartitionStatistics, SnapshotCommit from pypaimon.common.identifier import Identifier from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import (PartitionStatistics, + SnapshotCommit) class CatalogSnapshotCommit(SnapshotCommit): diff --git a/paimon-python/pypaimon/catalog/renaming_snapshot_commit.py b/paimon-python/pypaimon/snapshot/renaming_snapshot_commit.py similarity index 95% rename from paimon-python/pypaimon/catalog/renaming_snapshot_commit.py rename to paimon-python/pypaimon/snapshot/renaming_snapshot_commit.py index b85c4200a1..27c5a54a2c 100644 --- a/paimon-python/pypaimon/catalog/renaming_snapshot_commit.py +++ b/paimon-python/pypaimon/snapshot/renaming_snapshot_commit.py @@ -18,10 +18,11 @@ from typing import List -from pypaimon.catalog.snapshot_commit import PartitionStatistics, SnapshotCommit from pypaimon.common.file_io import FileIO -from pypaimon.common.rest_json import JSON +from pypaimon.common.json_util import JSON from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import (PartitionStatistics, + SnapshotCommit) from pypaimon.snapshot.snapshot_manager import SnapshotManager diff --git a/paimon-python/pypaimon/snapshot/snapshot.py b/paimon-python/pypaimon/snapshot/snapshot.py index cc27c5a530..5bc92dcad4 100644 --- a/paimon-python/pypaimon/snapshot/snapshot.py +++ b/paimon-python/pypaimon/snapshot/snapshot.py @@ -19,7 +19,7 @@ from dataclasses import dataclass from typing import Dict, Optional -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field @dataclass diff --git a/paimon-python/pypaimon/catalog/snapshot_commit.py b/paimon-python/pypaimon/snapshot/snapshot_commit.py similarity index 98% rename from paimon-python/pypaimon/catalog/snapshot_commit.py rename to paimon-python/pypaimon/snapshot/snapshot_commit.py index f66b123343..50727b6ce3 100644 --- a/paimon-python/pypaimon/catalog/snapshot_commit.py +++ b/paimon-python/pypaimon/snapshot/snapshot_commit.py @@ -16,12 +16,12 @@ # limitations under the License. ################################################################################ +import time from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Dict, List -import time -from pypaimon.common.rest_json import json_field +from pypaimon.common.json_util import json_field from pypaimon.snapshot.snapshot import Snapshot diff --git a/paimon-python/pypaimon/snapshot/snapshot_manager.py b/paimon-python/pypaimon/snapshot/snapshot_manager.py index f23ad03dae..2ded357802 100644 --- a/paimon-python/pypaimon/snapshot/snapshot_manager.py +++ b/paimon-python/pypaimon/snapshot/snapshot_manager.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Optional from pypaimon.common.file_io import FileIO -from pypaimon.common.rest_json import JSON +from pypaimon.common.json_util import JSON from pypaimon.snapshot.snapshot import Snapshot diff --git a/paimon-python/pypaimon/table/file_store_table.py b/paimon-python/pypaimon/table/file_store_table.py index eb763d2938..74aaaff4e6 100644 --- a/paimon-python/pypaimon/table/file_store_table.py +++ b/paimon-python/pypaimon/table/file_store_table.py @@ -19,6 +19,7 @@ from pathlib import Path from typing import Optional +from pypaimon.catalog.catalog_environment import CatalogEnvironment from pypaimon.common.core_options import CoreOptions from pypaimon.common.file_io import FileIO from pypaimon.common.identifier import Identifier @@ -26,7 +27,6 @@ from pypaimon.read.read_builder import ReadBuilder from pypaimon.schema.schema_manager import SchemaManager from pypaimon.schema.table_schema import TableSchema from pypaimon.table.bucket_mode import BucketMode -from pypaimon.table.catalog_environment import CatalogEnvironment from pypaimon.table.table import Table from pypaimon.write.batch_write_builder import BatchWriteBuilder from pypaimon.write.row_key_extractor import (DynamicBucketRowKeyExtractor, diff --git a/paimon-python/pypaimon/tests/api_test.py b/paimon-python/pypaimon/tests/api_test.py index 6de26917eb..c63b912635 100644 --- a/paimon-python/pypaimon/tests/api_test.py +++ b/paimon-python/pypaimon/tests/api_test.py @@ -19,20 +19,19 @@ import logging import unittest import uuid -import pypaimon.api as api +from pypaimon.api.api_response import ConfigResponse +from pypaimon.api.auth import BearTokenAuthProvider +from pypaimon.api.rest_api import RESTApi +from pypaimon.api.token_loader import DLFToken, DLFTokenLoaderFactory +from pypaimon.catalog.rest.table_metadata import TableMetadata +from pypaimon.common.config import CatalogOptions from pypaimon.common.identifier import Identifier -from pypaimon.common.rest_json import JSON +from pypaimon.common.json_util import JSON from pypaimon.schema.data_types import (ArrayType, AtomicInteger, AtomicType, DataField, DataTypeParser, MapType, RowType) from pypaimon.schema.table_schema import TableSchema - -from ..api import RESTApi -from ..api.api_response import ConfigResponse -from ..api.auth import BearTokenAuthProvider -from ..api.token_loader import DLFToken, DLFTokenLoaderFactory -from ..catalog.table_metadata import TableMetadata -from .rest_server import RESTCatalogServer +from pypaimon.tests.rest_server import RESTCatalogServer class ApiTestCase(unittest.TestCase): @@ -170,10 +169,10 @@ class ApiTestCase(unittest.TestCase): "token.provider": "bear", 'token': token } - api = RESTApi(options) - self.assertSetEqual(set(api.list_databases()), {*test_databases}) - self.assertEqual(api.get_database('default'), test_databases.get('default')) - table = api.get_table(Identifier.from_string('default.user')) + rest_api = RESTApi(options) + self.assertSetEqual(set(rest_api.list_databases()), {*test_databases}) + self.assertEqual(rest_api.get_database('default'), test_databases.get('default')) + table = rest_api.get_table(Identifier.from_string('default.user')) self.assertEqual(table.id, str(test_tables['default.user'].uuid)) finally: @@ -204,8 +203,8 @@ class ApiTestCase(unittest.TestCase): server.start() ecs_metadata_url = f"http://localhost:{server.port}/ram/security-credential/" options = { - api.CatalogOptions.DLF_TOKEN_LOADER: 'ecs', - api.CatalogOptions.DLF_TOKEN_ECS_METADATA_URL: ecs_metadata_url + CatalogOptions.DLF_TOKEN_LOADER: 'ecs', + CatalogOptions.DLF_TOKEN_ECS_METADATA_URL: ecs_metadata_url } loader = DLFTokenLoaderFactory.create_token_loader(options) load_token = loader.load_token() @@ -214,9 +213,9 @@ class ApiTestCase(unittest.TestCase): self.assertEqual(load_token.security_token, token.security_token) self.assertEqual(load_token.expiration, token.expiration) options_with_role = { - api.CatalogOptions.DLF_TOKEN_LOADER: 'ecs', - api.CatalogOptions.DLF_TOKEN_ECS_METADATA_URL: ecs_metadata_url, - api.CatalogOptions.DLF_TOKEN_ECS_ROLE_NAME: role_name, + CatalogOptions.DLF_TOKEN_LOADER: 'ecs', + CatalogOptions.DLF_TOKEN_ECS_METADATA_URL: ecs_metadata_url, + CatalogOptions.DLF_TOKEN_ECS_ROLE_NAME: role_name, } loader = DLFTokenLoaderFactory.create_token_loader(options_with_role) token = loader.load_token() diff --git a/paimon-python/pypaimon/tests/predicates_test.py b/paimon-python/pypaimon/tests/predicates_test.py index 5ce806bbcb..19d101fb28 100644 --- a/paimon-python/pypaimon/tests/predicates_test.py +++ b/paimon-python/pypaimon/tests/predicates_test.py @@ -23,8 +23,8 @@ import unittest import pandas as pd import pyarrow as pa -from pypaimon.api import Schema from pypaimon.catalog.catalog_factory import CatalogFactory +from pypaimon.schema.schema import Schema def _check_filtered_result(read_builder, expected_df): diff --git a/paimon-python/pypaimon/tests/pvfs_test.py b/paimon-python/pypaimon/tests/pvfs_test.py index 3f80d1eb63..8bebb9855d 100644 --- a/paimon-python/pypaimon/tests/pvfs_test.py +++ b/paimon-python/pypaimon/tests/pvfs_test.py @@ -23,9 +23,9 @@ from pathlib import Path import pandas -from pypaimon.api import ConfigResponse +from pypaimon.api.api_response import ConfigResponse from pypaimon.api.auth import BearTokenAuthProvider -from pypaimon.catalog.table_metadata import TableMetadata +from pypaimon.catalog.rest.table_metadata import TableMetadata from pypaimon.pvfs import PaimonVirtualFileSystem from pypaimon.schema.data_types import AtomicType, DataField from pypaimon.schema.table_schema import TableSchema diff --git a/paimon-python/pypaimon/tests/reader_append_only_test.py b/paimon-python/pypaimon/tests/reader_append_only_test.py index f82f28d7a0..17acb9a183 100644 --- a/paimon-python/pypaimon/tests/reader_append_only_test.py +++ b/paimon-python/pypaimon/tests/reader_append_only_test.py @@ -22,8 +22,8 @@ import unittest import pyarrow as pa -from pypaimon.api import Schema from pypaimon.catalog.catalog_factory import CatalogFactory +from pypaimon.schema.schema import Schema class AoReaderTest(unittest.TestCase): diff --git a/paimon-python/pypaimon/tests/reader_primary_key_test.py b/paimon-python/pypaimon/tests/reader_primary_key_test.py index b9b115dfa4..8b9b853350 100644 --- a/paimon-python/pypaimon/tests/reader_primary_key_test.py +++ b/paimon-python/pypaimon/tests/reader_primary_key_test.py @@ -23,8 +23,8 @@ import unittest import pyarrow as pa -from pypaimon.api import Schema from pypaimon.catalog.catalog_factory import CatalogFactory +from pypaimon.schema.schema import Schema class PkReaderTest(unittest.TestCase): diff --git a/paimon-python/pypaimon/tests/rest_catalog_base_test.py b/paimon-python/pypaimon/tests/rest_catalog_base_test.py index c035957ccc..e56580e0ae 100644 --- a/paimon-python/pypaimon/tests/rest_catalog_base_test.py +++ b/paimon-python/pypaimon/tests/rest_catalog_base_test.py @@ -25,13 +25,14 @@ import uuid import pyarrow as pa -from pypaimon.api import ConfigResponse, Identifier +from pypaimon.api.api_response import ConfigResponse from pypaimon.api.auth import BearTokenAuthProvider from pypaimon.api.options import Options from pypaimon.catalog.catalog_context import CatalogContext from pypaimon.catalog.catalog_factory import CatalogFactory from pypaimon.catalog.rest.rest_catalog import RESTCatalog -from pypaimon.catalog.table_metadata import TableMetadata +from pypaimon.catalog.rest.table_metadata import TableMetadata +from pypaimon.common.identifier import Identifier from pypaimon.schema.data_types import (ArrayType, AtomicType, DataField, MapType) from pypaimon.schema.schema import Schema diff --git a/paimon-python/pypaimon/tests/rest_server.py b/paimon-python/pypaimon/tests/rest_server.py index c326f3525d..8804a4f5e2 100644 --- a/paimon-python/pypaimon/tests/rest_server.py +++ b/paimon-python/pypaimon/tests/rest_server.py @@ -27,22 +27,24 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urlparse -import pypaimon.api as api -from pypaimon.common.rest_json import JSON +from pypaimon.api.api_request import (CreateDatabaseRequest, + CreateTableRequest, RenameTableRequest) +from pypaimon.api.api_response import (ConfigResponse, GetDatabaseResponse, + GetTableResponse, ListDatabasesResponse, + ListTablesResponse, PagedList, + RESTResponse) +from pypaimon.api.resource_paths import ResourcePaths +from pypaimon.api.rest_util import RESTUtil +from pypaimon.catalog.catalog_exception import (DatabaseNoPermissionException, + DatabaseNotExistException, + TableNoPermissionException, + TableNotExistException) +from pypaimon.catalog.rest.table_metadata import TableMetadata +from pypaimon.common.identifier import Identifier +from pypaimon.common.json_util import JSON +from pypaimon.schema.schema import Schema from pypaimon.schema.table_schema import TableSchema -from ..api import (CreateDatabaseRequest, CreateTableRequest, Identifier, - RenameTableRequest) -from ..api.api_response import (ConfigResponse, GetDatabaseResponse, - GetTableResponse, ListDatabasesResponse, - ListTablesResponse, PagedList, RESTResponse) -from ..catalog.catalog_exception import (DatabaseNoPermissionException, - DatabaseNotExistException, - TableNoPermissionException, - TableNotExistException) -from ..catalog.table_metadata import TableMetadata -from ..schema.schema import Schema - @dataclass class ErrorResponse(RESTResponse): @@ -99,7 +101,7 @@ class RESTCatalogServer: # Initialize resource paths prefix = config.defaults.get("prefix") - self.resource_paths = api.ResourcePaths(prefix=prefix) + self.resource_paths = ResourcePaths(prefix=prefix) self.database_uri = self.resource_paths.databases() # Initialize storage @@ -199,7 +201,7 @@ class RESTCatalogServer: for pair in query.split('&'): if '=' in pair: key, value = pair.split('=', 1) - params[key.strip()] = api.RESTUtil.decode_string(value.strip()) + params[key.strip()] = RESTUtil.decode_string(value.strip()) return params def _authenticate(self, token: str, path: str, params: Dict[str, str], @@ -265,7 +267,7 @@ class RESTCatalogServer: """Handle database-specific resource requests""" # Extract database name and resource path path_parts = resource_path[len(self.database_uri) + 1:].split('/') - database_name = api.RESTUtil.decode_string(path_parts[0]) + database_name = RESTUtil.decode_string(path_parts[0]) # Check database permissions if database_name in self.no_permission_databases: @@ -283,16 +285,16 @@ class RESTCatalogServer: # Collection operations (tables, views, functions) resource_type = path_parts[1] - if resource_type.startswith(api.ResourcePaths.TABLES): + if resource_type.startswith(ResourcePaths.TABLES): return self._tables_handle(method, data, database_name, parameters) elif len(path_parts) >= 3: # Individual resource operations resource_type = path_parts[1] - resource_name = api.RESTUtil.decode_string(path_parts[2]) + resource_name = RESTUtil.decode_string(path_parts[2]) identifier = Identifier.create(database_name, resource_name) - if resource_type == api.ResourcePaths.TABLES: + if resource_type == ResourcePaths.TABLES: return self._handle_table_resource(method, path_parts, identifier, data, parameters) return self._mock_response(ErrorResponse(None, None, "Not Found", 404), 404) diff --git a/paimon-python/pypaimon/tests/test_file_store_commit.py b/paimon-python/pypaimon/tests/test_file_store_commit.py index ced0afbf4a..6f32894c90 100644 --- a/paimon-python/pypaimon/tests/test_file_store_commit.py +++ b/paimon-python/pypaimon/tests/test_file_store_commit.py @@ -21,8 +21,8 @@ from datetime import datetime from pathlib import Path from unittest.mock import Mock, patch -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.manifest.schema.data_file_meta import DataFileMeta +from pypaimon.snapshot.snapshot_commit import PartitionStatistics from pypaimon.write.commit_message import CommitMessage from pypaimon.write.file_store_commit import FileStoreCommit diff --git a/paimon-python/pypaimon/tests/test_rest_catalog_commit_snapshot.py b/paimon-python/pypaimon/tests/test_rest_catalog_commit_snapshot.py index 25350ebea0..84c6557c1e 100644 --- a/paimon-python/pypaimon/tests/test_rest_catalog_commit_snapshot.py +++ b/paimon-python/pypaimon/tests/test_rest_catalog_commit_snapshot.py @@ -22,15 +22,15 @@ import time import unittest from unittest.mock import Mock, patch -from pypaimon.api import NoSuchResourceException from pypaimon.api.api_response import CommitTableResponse from pypaimon.api.options import Options +from pypaimon.api.rest_exception import NoSuchResourceException from pypaimon.catalog.catalog_context import CatalogContext from pypaimon.catalog.catalog_exception import TableNotExistException from pypaimon.catalog.rest.rest_catalog import RESTCatalog -from pypaimon.catalog.snapshot_commit import PartitionStatistics from pypaimon.common.identifier import Identifier from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import PartitionStatistics class TestRESTCatalogCommitSnapshot(unittest.TestCase): @@ -120,7 +120,7 @@ class TestRESTCatalogCommitSnapshot(unittest.TestCase): # Configure mock to raise NoSuchResourceException mock_api_instance = Mock() mock_api_instance.options = self.test_options - mock_api_instance.commit_snapshot.side_effect = NoSuchResourceException("Table not found") + mock_api_instance.commit_snapshot.side_effect = NoSuchResourceException("Table", None, "not found") mock_rest_api.return_value = mock_api_instance # Create RESTCatalog @@ -188,9 +188,9 @@ class TestRESTCatalogCommitSnapshot(unittest.TestCase): def test_rest_api_commit_snapshot(self): """Test RESTApi commit_snapshot method.""" - from pypaimon.api import RESTApi + from pypaimon.api.rest_api import RESTApi - with patch('pypaimon.api.HttpClient') as mock_client_class: + with patch('pypaimon.api.client.HttpClient') as mock_client_class: # Configure mock mock_client = Mock() mock_response = CommitTableResponse(success=True) diff --git a/paimon-python/pypaimon/write/commit_message.py b/paimon-python/pypaimon/write/commit_message.py index 4e4c0f0b48..b36a1b1bbf 100644 --- a/paimon-python/pypaimon/write/commit_message.py +++ b/paimon-python/pypaimon/write/commit_message.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ + from dataclasses import dataclass from typing import List, Tuple diff --git a/paimon-python/pypaimon/write/file_store_commit.py b/paimon-python/pypaimon/write/file_store_commit.py index ca26981333..e7bf7ba534 100644 --- a/paimon-python/pypaimon/write/file_store_commit.py +++ b/paimon-python/pypaimon/write/file_store_commit.py @@ -21,12 +21,13 @@ import uuid from pathlib import Path from typing import List -from pypaimon.catalog.snapshot_commit import PartitionStatistics, SnapshotCommit from pypaimon.manifest.manifest_file_manager import ManifestFileManager from pypaimon.manifest.manifest_list_manager import ManifestListManager from pypaimon.manifest.schema.manifest_file_meta import ManifestFileMeta from pypaimon.manifest.schema.simple_stats import SimpleStats from pypaimon.snapshot.snapshot import Snapshot +from pypaimon.snapshot.snapshot_commit import (PartitionStatistics, + SnapshotCommit) from pypaimon.snapshot.snapshot_manager import SnapshotManager from pypaimon.table.row.binary_row import BinaryRow from pypaimon.write.commit_message import CommitMessage diff --git a/paimon-python/pypaimon/write/row_key_extractor.py b/paimon-python/pypaimon/write/row_key_extractor.py index bec8e08fb7..aa28aacef2 100644 --- a/paimon-python/pypaimon/write/row_key_extractor.py +++ b/paimon-python/pypaimon/write/row_key_extractor.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ################################################################################ + import hashlib import json from abc import ABC, abstractmethod