blob: a99c44a4aee834f1a22dca01c3470e4bda747577 [file]
#
# 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 json
from dataclasses import dataclass, field
from pydantic import StrictStr
from typing import Dict, Optional, List, cast
from apache_polaris.cli.command import Command
from apache_polaris.cli.command.utils import get_catalog_api_client
from apache_polaris.cli.exceptions import CliError
from apache_polaris.cli.constants import Subcommands, Arguments, UNIT_SEPARATOR
from apache_polaris.cli.options.option_tree import Argument
from apache_polaris.sdk.catalog import IcebergCatalogAPI, CreateNamespaceRequest
from apache_polaris.sdk.management import PolarisDefaultApi
from apache_polaris.sdk.catalog.api.policy_api import PolicyAPI
@dataclass
class NamespacesCommand(Command):
"""
A Command implementation to represent `polaris namespaces`. The instance attributes correspond to parameters
that can be provided to various subcommands, except `namespaces_subcommand` which represents the subcommand
itself.
Example commands:
* polaris namespaces create --catalog my_schema my_namespace
* polaris namespaces list --catalog my_catalog
* polaris namespaces delete --catalog my_catalog my_namespace.inner
"""
namespaces_subcommand: str
catalog: Optional[str] = None
namespace: Optional[List[StrictStr]] = None
parent: Optional[List[StrictStr]] = None
location: Optional[str] = None
properties: Optional[Dict[str, StrictStr]] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.properties is None:
self.properties = {}
def validate(self) -> None:
if not self.catalog:
raise CliError(
f"Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}"
)
if self.namespaces_subcommand in {
Subcommands.CREATE,
Subcommands.DELETE,
Subcommands.GET,
Subcommands.SUMMARIZE,
}:
if not self.namespace:
raise CliError(
f"Missing required argument: {Argument.to_flag_name(Arguments.NAMESPACE)}"
)
def execute(self, api: PolarisDefaultApi) -> None:
catalog_api_client = get_catalog_api_client(api)
catalog_api = IcebergCatalogAPI(catalog_api_client)
catalog_name = cast(str, self.catalog)
namespace = cast(List[str], self.namespace)
if self.namespaces_subcommand == Subcommands.CREATE:
req_properties = self.properties or {}
if self.location:
req_properties = {**req_properties, Arguments.LOCATION: self.location}
request = CreateNamespaceRequest(
namespace=namespace,
properties=req_properties,
)
catalog_api.create_namespace(
prefix=catalog_name, create_namespace_request=request
)
elif self.namespaces_subcommand == Subcommands.LIST:
if self.parent:
result = catalog_api.list_namespaces(
prefix=catalog_name, parent=UNIT_SEPARATOR.join(self.parent)
)
else:
result = catalog_api.list_namespaces(prefix=catalog_name)
for namespace_item in result.namespaces:
print(json.dumps({"namespace": ".".join(namespace_item)}))
elif self.namespaces_subcommand == Subcommands.DELETE:
catalog_api.drop_namespace(
prefix=catalog_name,
namespace=UNIT_SEPARATOR.join(namespace),
)
elif self.namespaces_subcommand == Subcommands.GET:
print(
catalog_api.load_namespace_metadata(
prefix=catalog_name,
namespace=UNIT_SEPARATOR.join(namespace),
).to_json()
)
elif self.namespaces_subcommand == Subcommands.SUMMARIZE:
self._generate_summary(catalog_api)
else:
raise CliError(f"{self.namespaces_subcommand} is not supported in the CLI")
def _generate_summary(self, catalog_api: IcebergCatalogAPI) -> None:
catalog_name = cast(str, self.catalog)
namespace = cast(List[str], self.namespace)
ns_str = UNIT_SEPARATOR.join(namespace)
print(f"Namespace: {'.'.join(namespace)}")
print("-" * 80)
# Metadata
print("Metadata")
print(f" {'Level:':<30} {len(namespace)}")
if len(namespace) > 1:
print(f" {'Parent:':<30} {'.'.join(namespace[:-1])}")
sub_ns = (
catalog_api.list_namespaces(prefix=catalog_name, parent=ns_str).namespaces
or []
)
print(f" {'Sub-namespaces:':<30} {len(sub_ns)}")
tables = (
catalog_api.list_tables(prefix=catalog_name, namespace=ns_str).identifiers
or []
)
print(f" {'Tables:':<30} {len(tables)}")
views = (
catalog_api.list_views(prefix=catalog_name, namespace=ns_str).identifiers
or []
)
print(f" {'Views:':<30} {len(views)}")
# Effective policies
policy_api = PolicyAPI(catalog_api.api_client)
policies_resp = policy_api.get_applicable_policies(
prefix=catalog_name, namespace=ns_str
)
print("\nEffective Policies")
applicable_policies = policies_resp.applicable_policies or []
if applicable_policies:
for policy in sorted(applicable_policies, key=lambda x: x.name):
source = "Direct"
if policy.inherited:
target = (
".".join(policy.namespace) if policy.namespace else "Catalog"
)
source = f"Inherited from {target}"
print(f" - {policy.name} ({source})")
else:
print(" No policies apply to this namespace")
print("-" * 80)