diff --git a/client/python/cli/command/__init__.py b/client/python/cli/command/__init__.py index 414ce43f50..897cbaaa52 100644 --- a/client/python/cli/command/__init__.py +++ b/client/python/cli/command/__init__.py @@ -32,8 +32,7 @@ class Command(ABC): """ @staticmethod - def from_options(options: argparse.Namespace) -> 'Command': - + def from_options(options: argparse.Namespace) -> "Command": def options_get(key, f=lambda x: x): return f(getattr(options, key)) if hasattr(options, key) else None @@ -44,8 +43,9 @@ def options_get(key, f=lambda x: x): command = None if options.command == Commands.CATALOGS: from cli.command.catalogs import CatalogsCommand + command = CatalogsCommand( - options_get(f'{Commands.CATALOGS}_subcommand'), + options_get(f"{Commands.CATALOGS}_subcommand"), catalog_type=options_get(Arguments.TYPE), default_base_location=options_get(Arguments.DEFAULT_BASE_LOCATION), storage_type=options_get(Arguments.STORAGE_TYPE), @@ -61,81 +61,100 @@ def options_get(key, f=lambda x: x): catalog_name=options_get(Arguments.CATALOG), properties={} if properties is None else properties, set_properties={} if set_properties is None else set_properties, - remove_properties=[] if remove_properties is None else remove_properties + remove_properties=[] + if remove_properties is None + else remove_properties, ) elif options.command == Commands.PRINCIPALS: from cli.command.principals import PrincipalsCommand + command = PrincipalsCommand( - options_get(f'{Commands.PRINCIPALS}_subcommand'), + options_get(f"{Commands.PRINCIPALS}_subcommand"), type=options_get(Arguments.TYPE), principal_name=options_get(Arguments.PRINCIPAL), client_id=options_get(Arguments.CLIENT_ID), principal_role=options_get(Arguments.PRINCIPAL_ROLE), properties={} if properties is None else properties, set_properties={} if set_properties is None else set_properties, - remove_properties=[] if remove_properties is None else remove_properties + remove_properties=[] + if remove_properties is None + else remove_properties, ) elif options.command == Commands.PRINCIPAL_ROLES: from cli.command.principal_roles import PrincipalRolesCommand + command = PrincipalRolesCommand( - options_get(f'{Commands.PRINCIPAL_ROLES}_subcommand'), + options_get(f"{Commands.PRINCIPAL_ROLES}_subcommand"), principal_role_name=options_get(Arguments.PRINCIPAL_ROLE), principal_name=options_get(Arguments.PRINCIPAL), catalog_name=options_get(Arguments.CATALOG), catalog_role_name=options_get(Arguments.CATALOG_ROLE), properties={} if properties is None else properties, set_properties={} if set_properties is None else set_properties, - remove_properties=[] if remove_properties is None else remove_properties + remove_properties=[] + if remove_properties is None + else remove_properties, ) elif options.command == Commands.CATALOG_ROLES: from cli.command.catalog_roles import CatalogRolesCommand + command = CatalogRolesCommand( - options_get(f'{Commands.CATALOG_ROLES}_subcommand'), + options_get(f"{Commands.CATALOG_ROLES}_subcommand"), catalog_name=options_get(Arguments.CATALOG), catalog_role_name=options_get(Arguments.CATALOG_ROLE), principal_role_name=options_get(Arguments.PRINCIPAL_ROLE), properties={} if properties is None else properties, set_properties={} if set_properties is None else set_properties, - remove_properties=[] if remove_properties is None else remove_properties + remove_properties=[] + if remove_properties is None + else remove_properties, ) elif options.command == Commands.PRIVILEGES: from cli.command.privileges import PrivilegesCommand - subcommand = options_get(f'{Commands.PRIVILEGES}_subcommand') + + subcommand = options_get(f"{Commands.PRIVILEGES}_subcommand") command = PrivilegesCommand( subcommand, - action=options_get(f'{subcommand}_subcommand'), + action=options_get(f"{subcommand}_subcommand"), catalog_name=options_get(Arguments.CATALOG), catalog_role_name=options_get(Arguments.CATALOG_ROLE), - namespace=options_get(Arguments.NAMESPACE, lambda s: s.split('.') if s else None), + namespace=options_get( + Arguments.NAMESPACE, lambda s: s.split(".") if s else None + ), view=options_get(Arguments.VIEW), table=options_get(Arguments.TABLE), privilege=options_get(Arguments.PRIVILEGE), - cascade=options_get(Arguments.CASCADE) + cascade=options_get(Arguments.CASCADE), ) elif options.command == Commands.NAMESPACES: from cli.command.namespaces import NamespacesCommand - subcommand = options_get(f'{Commands.NAMESPACES}_subcommand') + + subcommand = options_get(f"{Commands.NAMESPACES}_subcommand") command = NamespacesCommand( subcommand, catalog=options_get(Arguments.CATALOG), - namespace=options_get(Arguments.NAMESPACE, lambda s: s.split('.')), - parent=options_get(Arguments.PARENT, lambda s: s.split('.') if s else None), + namespace=options_get(Arguments.NAMESPACE, lambda s: s.split(".")), + parent=options_get( + Arguments.PARENT, lambda s: s.split(".") if s else None + ), location=options_get(Arguments.LOCATION), - properties=properties + properties=properties, ) elif options.command == Commands.PROFILES: from cli.command.profiles import ProfilesCommand - subcommand = options_get(f'{Commands.PROFILES}_subcommand') + + subcommand = options_get(f"{Commands.PROFILES}_subcommand") command = ProfilesCommand( - subcommand, - profile_name=options_get(Arguments.PROFILE) + subcommand, profile_name=options_get(Arguments.PROFILE) ) if command is not None: command.validate() return command else: - raise Exception("Please specify a command or run ./polaris --help to view the available commands") + raise Exception( + "Please specify a command or run ./polaris --help to view the available commands" + ) def execute(self, api: PolarisDefaultApi) -> None: """ diff --git a/client/python/cli/command/catalog_roles.py b/client/python/cli/command/catalog_roles.py index 1c7d4df9a7..3fa7cebb5c 100644 --- a/client/python/cli/command/catalog_roles.py +++ b/client/python/cli/command/catalog_roles.py @@ -24,8 +24,13 @@ from cli.command import Command from cli.constants import Subcommands, Arguments from cli.options.option_tree import Argument -from polaris.management import PolarisDefaultApi, CreateCatalogRoleRequest, CatalogRole, UpdateCatalogRoleRequest, \ - GrantCatalogRoleRequest +from polaris.management import ( + PolarisDefaultApi, + CreateCatalogRoleRequest, + CatalogRole, + UpdateCatalogRoleRequest, + GrantCatalogRoleRequest, +) @dataclass @@ -51,34 +56,44 @@ class CatalogRolesCommand(Command): def validate(self): if not self.catalog_name: - raise Exception(f'Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}') + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}" + ) if self.catalog_roles_subcommand in {Subcommands.GRANT, Subcommands.REVOKE}: if not self.principal_role_name: - raise Exception(f'Missing required argument: {Argument.to_flag_name(Arguments.PRINCIPAL_ROLE)}') + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.PRINCIPAL_ROLE)}" + ) def execute(self, api: PolarisDefaultApi) -> None: if self.catalog_roles_subcommand == Subcommands.CREATE: request = CreateCatalogRoleRequest( catalog_role=CatalogRole( - name=self.catalog_role_name, - properties=self.properties + name=self.catalog_role_name, properties=self.properties ) ) api.create_catalog_role(self.catalog_name, request) elif self.catalog_roles_subcommand == Subcommands.DELETE: api.delete_catalog_role(self.catalog_name, self.catalog_role_name) elif self.catalog_roles_subcommand == Subcommands.GET: - print(api.get_catalog_role(self.catalog_name, self.catalog_role_name).to_json()) + print( + api.get_catalog_role( + self.catalog_name, self.catalog_role_name + ).to_json() + ) elif self.catalog_roles_subcommand == Subcommands.LIST: if self.principal_role_name: for catalog_role in api.list_catalog_roles_for_principal_role( - self.principal_role_name, self.catalog_name).roles: + self.principal_role_name, self.catalog_name + ).roles: print(catalog_role.to_json()) else: for catalog_role in api.list_catalog_roles(self.catalog_name).roles: print(catalog_role.to_json()) elif self.catalog_roles_subcommand == Subcommands.UPDATE: - catalog_role = api.get_catalog_role(self.catalog_name, self.catalog_role_name) + catalog_role = api.get_catalog_role( + self.catalog_name, self.catalog_role_name + ) new_properties = catalog_role.properties or {} # Add or update all entries specified in set_properties @@ -92,19 +107,22 @@ def execute(self, api: PolarisDefaultApi) -> None: request = UpdateCatalogRoleRequest( current_entity_version=catalog_role.entity_version, - properties=new_properties + properties=new_properties, ) api.update_catalog_role(self.catalog_name, self.catalog_role_name, request) elif self.catalog_roles_subcommand == Subcommands.GRANT: request = GrantCatalogRoleRequest( - catalog_role=CatalogRole( - name=self.catalog_role_name - ), - properties=self.properties + catalog_role=CatalogRole(name=self.catalog_role_name), + properties=self.properties, + ) + api.assign_catalog_role_to_principal_role( + self.principal_role_name, self.catalog_name, request ) - api.assign_catalog_role_to_principal_role(self.principal_role_name, self.catalog_name, request) elif self.catalog_roles_subcommand == Subcommands.REVOKE: api.revoke_catalog_role_from_principal_role( - self.principal_role_name, self.catalog_name, self.catalog_role_name) + self.principal_role_name, self.catalog_name, self.catalog_role_name + ) else: - raise Exception(f'{self.catalog_roles_subcommand} is not supported in the CLI') + raise Exception( + f"{self.catalog_roles_subcommand} is not supported in the CLI" + ) diff --git a/client/python/cli/command/catalogs.py b/client/python/cli/command/catalogs.py index 718e6d11aa..0baf2cffad 100644 --- a/client/python/cli/command/catalogs.py +++ b/client/python/cli/command/catalogs.py @@ -24,9 +24,18 @@ from cli.command import Command from cli.constants import StorageType, CatalogType, Subcommands, Arguments from cli.options.option_tree import Argument -from polaris.management import PolarisDefaultApi, CreateCatalogRequest, UpdateCatalogRequest, \ - StorageConfigInfo, ExternalCatalog, AwsStorageConfigInfo, AzureStorageConfigInfo, GcpStorageConfigInfo, \ - PolarisCatalog, CatalogProperties +from polaris.management import ( + PolarisDefaultApi, + CreateCatalogRequest, + UpdateCatalogRequest, + StorageConfigInfo, + ExternalCatalog, + AwsStorageConfigInfo, + AzureStorageConfigInfo, + GcpStorageConfigInfo, + PolarisCatalog, + CatalogProperties, +) @dataclass @@ -63,38 +72,58 @@ class CatalogsCommand(Command): def validate(self): if self.catalogs_subcommand == Subcommands.CREATE: if not self.storage_type: - raise Exception(f'Missing required argument:' - f' {Argument.to_flag_name(Arguments.STORAGE_TYPE)}') + raise Exception( + f"Missing required argument:" + f" {Argument.to_flag_name(Arguments.STORAGE_TYPE)}" + ) if not self.default_base_location: - raise Exception(f'Missing required argument:' - f' {Argument.to_flag_name(Arguments.DEFAULT_BASE_LOCATION)}') + raise Exception( + f"Missing required argument:" + f" {Argument.to_flag_name(Arguments.DEFAULT_BASE_LOCATION)}" + ) if self.storage_type == StorageType.S3.value: if not self.role_arn: - raise Exception(f"Missing required argument for storage type 's3':" - f" {Argument.to_flag_name(Arguments.ROLE_ARN)}") + raise Exception( + f"Missing required argument for storage type 's3':" + f" {Argument.to_flag_name(Arguments.ROLE_ARN)}" + ) if self._has_azure_storage_info() or self._has_gcs_storage_info(): - raise Exception(f"Storage type 's3' supports the storage credentials" - f" {Argument.to_flag_name(Arguments.ROLE_ARN)}," - f" {Argument.to_flag_name(Arguments.REGION)}," - f" {Argument.to_flag_name(Arguments.EXTERNAL_ID)}, and" - f" {Argument.to_flag_name(Arguments.USER_ARN)}") + raise Exception( + f"Storage type 's3' supports the storage credentials" + f" {Argument.to_flag_name(Arguments.ROLE_ARN)}," + f" {Argument.to_flag_name(Arguments.REGION)}," + f" {Argument.to_flag_name(Arguments.EXTERNAL_ID)}, and" + f" {Argument.to_flag_name(Arguments.USER_ARN)}" + ) elif self.storage_type == StorageType.AZURE.value: if not self.tenant_id: - raise Exception("Missing required argument for storage type 'azure': " - f" {Argument.to_flag_name(Arguments.TENANT_ID)}") + raise Exception( + "Missing required argument for storage type 'azure': " + f" {Argument.to_flag_name(Arguments.TENANT_ID)}" + ) if self._has_aws_storage_info() or self._has_gcs_storage_info(): - raise Exception("Storage type 'azure' supports the storage credentials" - f" {Argument.to_flag_name(Arguments.TENANT_ID)}," - f" {Argument.to_flag_name(Arguments.MULTI_TENANT_APP_NAME)}, and" - f" {Argument.to_flag_name(Arguments.CONSENT_URL)}") + raise Exception( + "Storage type 'azure' supports the storage credentials" + f" {Argument.to_flag_name(Arguments.TENANT_ID)}," + f" {Argument.to_flag_name(Arguments.MULTI_TENANT_APP_NAME)}, and" + f" {Argument.to_flag_name(Arguments.CONSENT_URL)}" + ) elif self.storage_type == StorageType.GCS.value: if self._has_aws_storage_info() or self._has_azure_storage_info(): - raise Exception("Storage type 'gcs' supports the storage credential" - f" {Argument.to_flag_name(Arguments.SERVICE_ACCOUNT)}") + raise Exception( + "Storage type 'gcs' supports the storage credential" + f" {Argument.to_flag_name(Arguments.SERVICE_ACCOUNT)}" + ) elif self.storage_type == StorageType.FILE.value: - if self._has_aws_storage_info() or self._has_azure_storage_info() or self._has_gcs_storage_info(): - raise Exception("Storage type 'file' does not support any storage credentials") + if ( + self._has_aws_storage_info() + or self._has_azure_storage_info() + or self._has_gcs_storage_info() + ): + raise Exception( + "Storage type 'file' does not support any storage credentials" + ) def _has_aws_storage_info(self): return self.role_arn or self.external_id or self.user_arn or self.region @@ -114,7 +143,7 @@ def _build_storage_config_info(self): role_arn=self.role_arn, external_id=self.external_id, user_arn=self.user_arn, - region=self.region + region=self.region, ) elif self.storage_type == StorageType.AZURE.value: config = AzureStorageConfigInfo( @@ -128,12 +157,12 @@ def _build_storage_config_info(self): config = GcpStorageConfigInfo( storage_type=self.storage_type.upper(), allowed_locations=self.allowed_locations, - gcs_service_account=self.service_account + gcs_service_account=self.service_account, ) elif self.storage_type == StorageType.FILE.value: config = StorageConfigInfo( storage_type=self.storage_type.upper(), - allowed_locations=self.allowed_locations + allowed_locations=self.allowed_locations, ) return config @@ -148,8 +177,8 @@ def execute(self, api: PolarisDefaultApi) -> None: storage_config_info=config, properties=CatalogProperties( default_base_location=self.default_base_location, - additional_properties=self.properties - ) + additional_properties=self.properties, + ), ) ) else: @@ -160,8 +189,8 @@ def execute(self, api: PolarisDefaultApi) -> None: storage_config_info=config, properties=CatalogProperties( default_base_location=self.default_base_location, - additional_properties=self.properties - ) + additional_properties=self.properties, + ), ) ) api.create_catalog(request) @@ -175,13 +204,25 @@ def execute(self, api: PolarisDefaultApi) -> None: elif self.catalogs_subcommand == Subcommands.UPDATE: catalog = api.get_catalog(self.catalog_name) - if self.default_base_location or self.set_properties or self.remove_properties: - new_default_base_location = self.default_base_location or catalog.properties.default_base_location - new_additional_properties = catalog.properties.additional_properties or {} + if ( + self.default_base_location + or self.set_properties + or self.remove_properties + ): + new_default_base_location = ( + self.default_base_location + or catalog.properties.default_base_location + ) + new_additional_properties = ( + catalog.properties.additional_properties or {} + ) # Add or update all entries specified in set_properties if self.set_properties: - new_additional_properties = {**new_additional_properties, **self.set_properties} + new_additional_properties = { + **new_additional_properties, + **self.set_properties, + } # Remove all keys specified in remove_properties if self.remove_properties: @@ -190,11 +231,15 @@ def execute(self, api: PolarisDefaultApi) -> None: catalog.properties = CatalogProperties( default_base_location=new_default_base_location, - additional_properties=new_additional_properties + additional_properties=new_additional_properties, ) - if (self._has_aws_storage_info() or self._has_azure_storage_info() or - self._has_gcs_storage_info() or self.allowed_locations): + if ( + self._has_aws_storage_info() + or self._has_azure_storage_info() + or self._has_gcs_storage_info() + or self.allowed_locations + ): # We must first reconstitute local storage-config related settings from the existing # catalog to properly construct the complete updated storage-config updated_storage_info = catalog.storage_config_info @@ -204,7 +249,9 @@ def execute(self, api: PolarisDefaultApi) -> None: # in option_tree.py should be applied individually against the existing # storage_config_info here. if self.allowed_locations: - updated_storage_info.allowed_locations.extend(self.allowed_locations) + updated_storage_info.allowed_locations.extend( + self.allowed_locations + ) if self.region: # Note: We have to lowercase the returned value because the server enum @@ -212,20 +259,21 @@ def execute(self, api: PolarisDefaultApi) -> None: storage_type = updated_storage_info.storage_type if storage_type.lower() != StorageType.S3.value: raise Exception( - f'--region requires S3 storage_type, got: {storage_type}') + f"--region requires S3 storage_type, got: {storage_type}" + ) updated_storage_info.region = self.region request = UpdateCatalogRequest( current_entity_version=catalog.entity_version, properties=catalog.properties.to_dict(), - storage_config_info=updated_storage_info + storage_config_info=updated_storage_info, ) else: request = UpdateCatalogRequest( current_entity_version=catalog.entity_version, - properties=catalog.properties.to_dict() + properties=catalog.properties.to_dict(), ) api.update_catalog(self.catalog_name, request) else: - raise Exception(f'{self.catalogs_subcommand} is not supported in the CLI') + raise Exception(f"{self.catalogs_subcommand} is not supported in the CLI") diff --git a/client/python/cli/command/namespaces.py b/client/python/cli/command/namespaces.py index 65687226a0..7631384731 100644 --- a/client/python/cli/command/namespaces.py +++ b/client/python/cli/command/namespaces.py @@ -26,7 +26,12 @@ from cli.command import Command from cli.constants import Subcommands, Arguments, UNIT_SEPARATOR from cli.options.option_tree import Argument -from polaris.catalog import IcebergCatalogAPI, CreateNamespaceRequest, ApiClient, Configuration +from polaris.catalog import ( + IcebergCatalogAPI, + CreateNamespaceRequest, + ApiClient, + Configuration, +) from polaris.management import PolarisDefaultApi @@ -51,16 +56,19 @@ class NamespacesCommand(Command): def validate(self): if not self.catalog: - raise Exception(f'Missing required argument:' - f' {Argument.to_flag_name(Arguments.CATALOG)}') + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}" + ) def _get_catalog_api(self, api: PolarisDefaultApi): """ Convert a management API to a catalog API """ - catalog_host = re.match(r'(https?://.+)/api/management', api.api_client.configuration.host).group(1) + catalog_host = re.match( + r"(https?://.+)/api/management", api.api_client.configuration.host + ).group(1) configuration = Configuration( - host=f'{catalog_host}/api/catalog', + host=f"{catalog_host}/api/catalog", username=api.api_client.configuration.username, password=api.api_client.configuration.password, access_token=api.api_client.configuration.access_token, @@ -74,26 +82,29 @@ def execute(self, api: PolarisDefaultApi) -> None: if self.location: req_properties = {**req_properties, Arguments.LOCATION: self.location} request = CreateNamespaceRequest( - namespace=self.namespace, - properties=req_properties + namespace=self.namespace, properties=req_properties ) catalog_api.create_namespace( - prefix=self.catalog, - create_namespace_request=request) + prefix=self.catalog, create_namespace_request=request + ) elif self.namespaces_subcommand == Subcommands.LIST: if self.parent is not None: result = catalog_api.list_namespaces( - prefix=self.catalog, - parent=UNIT_SEPARATOR.join(self.parent)) + prefix=self.catalog, parent=UNIT_SEPARATOR.join(self.parent) + ) else: result = catalog_api.list_namespaces(prefix=self.catalog) for namespace in result.namespaces: - print(json.dumps({"namespace": '.'.join(namespace)})) + print(json.dumps({"namespace": ".".join(namespace)})) elif self.namespaces_subcommand == Subcommands.DELETE: - catalog_api.drop_namespace(prefix=self.catalog, namespace=UNIT_SEPARATOR.join(self.namespace)) + catalog_api.drop_namespace( + prefix=self.catalog, namespace=UNIT_SEPARATOR.join(self.namespace) + ) elif self.namespaces_subcommand == Subcommands.GET: - print(catalog_api.load_namespace_metadata( - prefix=self.catalog, - namespace=UNIT_SEPARATOR.join(self.namespace)).to_json()) + print( + catalog_api.load_namespace_metadata( + prefix=self.catalog, namespace=UNIT_SEPARATOR.join(self.namespace) + ).to_json() + ) else: raise Exception(f"{self.namespaces_subcommand} is not supported in the CLI") diff --git a/client/python/cli/command/principal_roles.py b/client/python/cli/command/principal_roles.py index 911f741f5c..8dd48bb744 100644 --- a/client/python/cli/command/principal_roles.py +++ b/client/python/cli/command/principal_roles.py @@ -24,8 +24,13 @@ from cli.command import Command from cli.constants import Subcommands, Arguments from cli.options.option_tree import Argument -from polaris.management import PolarisDefaultApi, CreatePrincipalRoleRequest, PrincipalRole, UpdatePrincipalRoleRequest, \ - GrantPrincipalRoleRequest +from polaris.management import ( + PolarisDefaultApi, + CreatePrincipalRoleRequest, + PrincipalRole, + UpdatePrincipalRoleRequest, + GrantPrincipalRoleRequest, +) @dataclass @@ -52,19 +57,22 @@ class PrincipalRolesCommand(Command): def validate(self): if self.principal_roles_subcommand == Subcommands.LIST: if self.principal_name and self.catalog_role_name: - raise Exception(f'You may provide either {Argument.to_flag_name(Arguments.PRINCIPAL)} or' - f' {Argument.to_flag_name(Arguments.CATALOG_ROLE)}, but not both') + raise Exception( + f"You may provide either {Argument.to_flag_name(Arguments.PRINCIPAL)} or" + f" {Argument.to_flag_name(Arguments.CATALOG_ROLE)}, but not both" + ) if self.principal_roles_subcommand in {Subcommands.GRANT, Subcommands.REVOKE}: if not self.principal_name: - raise Exception(f'Missing required argument for {self.principal_roles_subcommand}:' - f' {Argument.to_flag_name(Arguments.PRINCIPAL)}') + raise Exception( + f"Missing required argument for {self.principal_roles_subcommand}:" + f" {Argument.to_flag_name(Arguments.PRINCIPAL)}" + ) def execute(self, api: PolarisDefaultApi) -> None: if self.principal_roles_subcommand == Subcommands.CREATE: request = CreatePrincipalRoleRequest( principal_role=PrincipalRole( - name=self.principal_role_name, - properties=self.properties + name=self.principal_role_name, properties=self.properties ) ) api.create_principal_role(request) @@ -74,10 +82,14 @@ def execute(self, api: PolarisDefaultApi) -> None: print(api.get_principal_role(self.principal_role_name).to_json()) elif self.principal_roles_subcommand == Subcommands.LIST: if self.catalog_role_name: - for principal_role in api.list_principal_roles(self.catalog_role_name).roles: + for principal_role in api.list_principal_roles( + self.catalog_role_name + ).roles: print(principal_role.to_json()) elif self.principal_name: - for principal_role in api.list_principal_roles_assigned(self.principal_name).roles: + for principal_role in api.list_principal_roles_assigned( + self.principal_name + ).roles: print(principal_role.to_json()) else: for principal_role in api.list_principal_roles().roles: @@ -97,17 +109,17 @@ def execute(self, api: PolarisDefaultApi) -> None: request = UpdatePrincipalRoleRequest( current_entity_version=principal_role.entity_version, - properties=new_properties + properties=new_properties, ) api.update_principal_role(self.principal_role_name, request) elif self.principal_roles_subcommand == Subcommands.GRANT: request = GrantPrincipalRoleRequest( - principal_role=PrincipalRole( - name=self.principal_role_name - ), + principal_role=PrincipalRole(name=self.principal_role_name), ) api.assign_principal_role(self.principal_name, request) elif self.principal_roles_subcommand == Subcommands.REVOKE: api.revoke_principal_role(self.principal_name, self.principal_role_name) else: - raise Exception(f"{self.principal_roles_subcommand} is not supported in the CLI") + raise Exception( + f"{self.principal_roles_subcommand} is not supported in the CLI" + ) diff --git a/client/python/cli/command/principals.py b/client/python/cli/command/principals.py index a25c2a0c1c..58779f77d4 100644 --- a/client/python/cli/command/principals.py +++ b/client/python/cli/command/principals.py @@ -24,7 +24,13 @@ from cli.command import Command from cli.constants import Subcommands -from polaris.management import PolarisDefaultApi, CreatePrincipalRequest, Principal, PrincipalWithCredentials, UpdatePrincipalRequest +from polaris.management import ( + PolarisDefaultApi, + CreatePrincipalRequest, + Principal, + PrincipalWithCredentials, + UpdatePrincipalRequest, +) @dataclass @@ -52,23 +58,40 @@ class PrincipalsCommand(Command): def _get_catalogs(self, api: PolarisDefaultApi): for catalog in api.list_catalogs().catalogs: - yield catalog.to_dict()['name'] - + yield catalog.to_dict()["name"] + def _get_principal_roles(self, api: PolarisDefaultApi): - for principal_role in api.list_principal_roles_assigned(self.principal_name).roles: - yield principal_role.to_dict()['name'] - - def _get_catalog_roles(self, api: PolarisDefaultApi, principal_role_name: str, catalog_name: str): - for catalog_role in api.list_catalog_roles_for_principal_role(principal_role_name, catalog_name).roles: - yield catalog_role.to_dict()['name'] - - def _get_privileges(self, api: PolarisDefaultApi, catalog_name: str, catalog_role_name: str): - for grant in api.list_grants_for_catalog_role(catalog_name, catalog_role_name).grants: + for principal_role in api.list_principal_roles_assigned( + self.principal_name + ).roles: + yield principal_role.to_dict()["name"] + + def _get_catalog_roles( + self, api: PolarisDefaultApi, principal_role_name: str, catalog_name: str + ): + for catalog_role in api.list_catalog_roles_for_principal_role( + principal_role_name, catalog_name + ).roles: + yield catalog_role.to_dict()["name"] + + def _get_privileges( + self, api: PolarisDefaultApi, catalog_name: str, catalog_role_name: str + ): + for grant in api.list_grants_for_catalog_role( + catalog_name, catalog_role_name + ).grants: yield grant.to_dict() - def build_credential_json(self, principal_with_credentials: PrincipalWithCredentials): + def build_credential_json( + self, principal_with_credentials: PrincipalWithCredentials + ): credentials = principal_with_credentials.credentials - return json.dumps({"clientId": credentials.client_id, "clientSecret": credentials.client_secret.get_secret_value()}) + return json.dumps( + { + "clientId": credentials.client_id, + "clientSecret": credentials.client_secret.get_secret_value(), + } + ) def validate(self): pass @@ -80,7 +103,7 @@ def execute(self, api: PolarisDefaultApi) -> None: type=self.type.upper(), name=self.principal_name, client_id=self.client_id, - properties=self.properties + properties=self.properties, ) ) print(self.build_credential_json(api.create_principal(request))) @@ -90,13 +113,17 @@ def execute(self, api: PolarisDefaultApi) -> None: print(api.get_principal(self.principal_name).to_json()) elif self.principals_subcommand == Subcommands.LIST: if self.principal_role: - for principal in api.list_assignee_principals_for_principal_role(self.principal_role).principals: + for principal in api.list_assignee_principals_for_principal_role( + self.principal_role + ).principals: print(principal.to_json()) else: for principal in api.list_principals().principals: print(principal.to_json()) elif self.principals_subcommand == Subcommands.ROTATE_CREDENTIALS: - print(self.build_credential_json(api.rotate_credentials(self.principal_name))) + print( + self.build_credential_json(api.rotate_credentials(self.principal_name)) + ) elif self.principals_subcommand == Subcommands.UPDATE: principal = api.get_principal(self.principal_name) new_properties = principal.properties or {} @@ -112,37 +139,37 @@ def execute(self, api: PolarisDefaultApi) -> None: request = UpdatePrincipalRequest( current_entity_version=principal.entity_version, - properties=new_properties + properties=new_properties, ) api.update_principal(self.principal_name, request) elif self.principals_subcommand == Subcommands.ACCESS: - principal = api.get_principal(self.principal_name).to_dict()['name'] + principal = api.get_principal(self.principal_name).to_dict()["name"] principal_roles = self._get_principal_roles(api) # Initialize the result structure - result = { - 'principal': principal, - 'principal_roles': [] - } - + result = {"principal": principal, "principal_roles": []} + # Construct the result structure for each principal role for principal_role in principal_roles: - role_data = { - 'name': principal_role, - 'catalog_roles': [] - } + role_data = {"name": principal_role, "catalog_roles": []} # For each catalog role, get associated privileges for catalog in self._get_catalogs(api): - catalog_roles = self._get_catalog_roles(api, principal_role, catalog) + catalog_roles = self._get_catalog_roles( + api, principal_role, catalog + ) for catalog_role in catalog_roles: catalog_data = { - 'name': catalog_role, - 'catalog': catalog, - 'privileges': [] + "name": catalog_role, + "catalog": catalog, + "privileges": [], } - catalog_data['privileges'] = list(self._get_privileges(api, catalog_data['catalog'], catalog_role)) - role_data['catalog_roles'].append(catalog_data) - result['principal_roles'].append(role_data) + catalog_data["privileges"] = list( + self._get_privileges( + api, catalog_data["catalog"], catalog_role + ) + ) + role_data["catalog_roles"].append(catalog_data) + result["principal_roles"].append(role_data) print(json.dumps(result)) else: raise Exception(f"{self.principals_subcommand} is not supported in the CLI") diff --git a/client/python/cli/command/privileges.py b/client/python/cli/command/privileges.py index c7db215558..6cd19913ff 100644 --- a/client/python/cli/command/privileges.py +++ b/client/python/cli/command/privileges.py @@ -24,9 +24,19 @@ from cli.command import Command from cli.constants import Subcommands, Actions, Arguments from cli.options.option_tree import Argument -from polaris.management import PolarisDefaultApi, AddGrantRequest, NamespaceGrant, \ - RevokeGrantRequest, CatalogGrant, TableGrant, ViewGrant, CatalogPrivilege, NamespacePrivilege, TablePrivilege, \ - ViewPrivilege +from polaris.management import ( + PolarisDefaultApi, + AddGrantRequest, + NamespaceGrant, + RevokeGrantRequest, + CatalogGrant, + TableGrant, + ViewGrant, + CatalogPrivilege, + NamespacePrivilege, + TablePrivilege, + ViewPrivilege, +) @dataclass @@ -54,75 +64,87 @@ class PrivilegesCommand(Command): def validate(self): if not self.catalog_name: - raise Exception(f'Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}') + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.CATALOG)}" + ) if not self.catalog_role_name: - raise Exception(f'Missing required argument: {Argument.to_flag_name(Arguments.CATALOG_ROLE)}') + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.CATALOG_ROLE)}" + ) if not self.privileges_subcommand: - raise Exception('A subcommand must be provided') - if (self.privileges_subcommand in {Subcommands.NAMESPACE, Subcommands.TABLE, Subcommands.VIEW} - and not self.namespace): - raise Exception(f'Missing required argument: {Argument.to_flag_name(Arguments.NAMESPACE)}') + raise Exception("A subcommand must be provided") + if ( + self.privileges_subcommand + in {Subcommands.NAMESPACE, Subcommands.TABLE, Subcommands.VIEW} + and not self.namespace + ): + raise Exception( + f"Missing required argument: {Argument.to_flag_name(Arguments.NAMESPACE)}" + ) if self.action == Actions.GRANT and self.cascade: - raise Exception('Unrecognized argument for GRANT: --cascade') + raise Exception("Unrecognized argument for GRANT: --cascade") if self.privileges_subcommand == Subcommands.CATALOG: if self.privilege not in {i.value for i in CatalogPrivilege}: - raise Exception(f'Invalid catalog privilege: {self.privilege}') + raise Exception(f"Invalid catalog privilege: {self.privilege}") if self.privileges_subcommand == Subcommands.NAMESPACE: if self.privilege not in {i.value for i in NamespacePrivilege}: - raise Exception(f'Invalid namespace privilege: {self.privilege}') + raise Exception(f"Invalid namespace privilege: {self.privilege}") if self.privileges_subcommand == Subcommands.TABLE: if self.privilege not in {i.value for i in TablePrivilege}: - raise Exception(f'Invalid table privilege: {self.privilege}') + raise Exception(f"Invalid table privilege: {self.privilege}") if self.privileges_subcommand == Subcommands.VIEW: if self.privilege not in {i.value for i in ViewPrivilege}: - raise Exception(f'Invalid view privilege: {self.privilege}') + raise Exception(f"Invalid view privilege: {self.privilege}") def execute(self, api: PolarisDefaultApi) -> None: if self.privileges_subcommand == Subcommands.LIST: - for grant in api.list_grants_for_catalog_role(self.catalog_name, self.catalog_role_name).grants: + for grant in api.list_grants_for_catalog_role( + self.catalog_name, self.catalog_role_name + ).grants: print(grant.to_json()) else: grant = None if self.privileges_subcommand == Subcommands.CATALOG: grant = CatalogGrant( - type=Subcommands.CATALOG, - privilege=CatalogPrivilege(self.privilege) + type=Subcommands.CATALOG, privilege=CatalogPrivilege(self.privilege) ) elif self.privileges_subcommand == Subcommands.NAMESPACE: grant = NamespaceGrant( type=Subcommands.NAMESPACE, namespace=self.namespace, - privilege=NamespacePrivilege(self.privilege) + privilege=NamespacePrivilege(self.privilege), ) elif self.privileges_subcommand == Subcommands.TABLE: grant = TableGrant( type=Subcommands.TABLE, namespace=self.namespace, table_name=self.table, - privilege=TablePrivilege(self.privilege) + privilege=TablePrivilege(self.privilege), ) elif self.privileges_subcommand == Subcommands.VIEW: grant = ViewGrant( type=Subcommands.VIEW, namespace=self.namespace, view_name=self.view, - privilege=ViewPrivilege(self.privilege) + privilege=ViewPrivilege(self.privilege), ) if not grant: - raise Exception(f'{self.privileges_subcommand} is not supported in the CLI') + raise Exception( + f"{self.privileges_subcommand} is not supported in the CLI" + ) elif self.action == Actions.GRANT: - request = AddGrantRequest( - grant=grant + request = AddGrantRequest(grant=grant) + api.add_grant_to_catalog_role( + self.catalog_name, self.catalog_role_name, request ) - api.add_grant_to_catalog_role(self.catalog_name, self.catalog_role_name, request) elif self.action == Actions.REVOKE: - request = RevokeGrantRequest( - grant=grant + request = RevokeGrantRequest(grant=grant) + api.revoke_grant_from_catalog_role( + self.catalog_name, self.catalog_role_name, self.cascade, request ) - api.revoke_grant_from_catalog_role(self.catalog_name, self.catalog_role_name, self.cascade, request) else: - raise Exception(f'{self.action} is not supported in the CLI') + raise Exception(f"{self.action} is not supported in the CLI") diff --git a/client/python/cli/command/profiles.py b/client/python/cli/command/profiles.py index 8679907b0d..fc689ae2bb 100644 --- a/client/python/cli/command/profiles.py +++ b/client/python/cli/command/profiles.py @@ -24,7 +24,13 @@ from cli.command import Command -from cli.constants import Subcommands, DEFAULT_HOSTNAME, DEFAULT_PORT, CONFIG_DIR, CONFIG_FILE +from cli.constants import ( + Subcommands, + DEFAULT_HOSTNAME, + DEFAULT_PORT, + CONFIG_DIR, + CONFIG_FILE, +) from polaris.management import PolarisDefaultApi @@ -61,20 +67,20 @@ def _save_profiles(self, profiles: Dict[str, Dict[str, str]]) -> None: def _create_profile(self, name: str) -> None: profiles = self._load_profiles() if name not in profiles: - client_id = input("Polaris Client ID: ") - client_secret = input("Polaris Client Secret: ") - host = input(f"Polaris Host [{DEFAULT_HOSTNAME}]: ") or DEFAULT_HOSTNAME - port = input(f"Polaris Port [{DEFAULT_PORT}]: ") or DEFAULT_PORT - profiles[name] = { - "client_id": client_id, - "client_secret": client_secret, - "host": host, - "port": port - } - self._save_profiles(profiles) + client_id = input("Polaris Client ID: ") + client_secret = input("Polaris Client Secret: ") + host = input(f"Polaris Host [{DEFAULT_HOSTNAME}]: ") or DEFAULT_HOSTNAME + port = input(f"Polaris Port [{DEFAULT_PORT}]: ") or DEFAULT_PORT + profiles[name] = { + "client_id": client_id, + "client_secret": client_secret, + "host": host, + "port": port, + } + self._save_profiles(profiles) else: - print(f"Profile {name} already exists.") - sys.exit(1) + print(f"Profile {name} already exists.") + sys.exit(1) def _get_profile(self, name: str) -> Optional[Dict[str, str]]: profiles = self._load_profiles() @@ -98,15 +104,20 @@ def _update_profile(self, name: str) -> None: current_host = profiles[name].get("host") current_port = profiles[name].get("port") - client_id = input(f"Polaris Client ID [{current_client_id}]: ") or current_client_id - client_secret = input(f"Polaris Client Secret [{current_client_secret}]: ") or current_client_secret + client_id = ( + input(f"Polaris Client ID [{current_client_id}]: ") or current_client_id + ) + client_secret = ( + input(f"Polaris Client Secret [{current_client_secret}]: ") + or current_client_secret + ) host = input(f"Polaris Client ID [{current_host}]: ") or current_host port = input(f"Polaris Client Secret [{current_port}]: ") or current_port profiles[name] = { "client_id": client_id, "client_secret": client_secret, "host": host, - "port": port + "port": port, } self._save_profiles(profiles) else: @@ -125,7 +136,7 @@ def execute(self, api: Optional[PolarisDefaultApi] = None) -> None: print(f"Polaris profile {self.profile_name} deleted successfully.") elif self.profiles_subcommand == Subcommands.UPDATE: self._update_profile(self.profile_name) - print(f"Polaris profile {self.profile_name} updated successfully.") + print(f"Polaris profile {self.profile_name} updated successfully.") elif self.profiles_subcommand == Subcommands.GET: profile = self._get_profile(self.profile_name) if profile: diff --git a/client/python/cli/constants.py b/client/python/cli/constants.py index c9f28ba3c3..91fff0dc0f 100644 --- a/client/python/cli/constants.py +++ b/client/python/cli/constants.py @@ -25,10 +25,10 @@ class StorageType(Enum): Represents a Storage Type within the Polaris API -- `s3`, `azure`, `gcs`, or `file`. """ - S3 = 's3' - AZURE = 'azure' - GCS = 'gcs' - FILE = 'file' + S3 = "s3" + AZURE = "azure" + GCS = "gcs" + FILE = "file" class CatalogType(Enum): @@ -36,8 +36,8 @@ class CatalogType(Enum): Represents a Catalog Type within the Polaris API -- `internal` or `external` """ - INTERNAL = 'internal' - EXTERNAL = 'external' + INTERNAL = "internal" + EXTERNAL = "external" class PrincipalType(Enum): @@ -45,7 +45,7 @@ class PrincipalType(Enum): Represents a Principal Type within the Polaris API -- currently only `service` """ - SERVICE = 'service' + SERVICE = "service" class Commands: @@ -53,13 +53,13 @@ class Commands: Represents the various commands available in the CLI """ - CATALOGS = 'catalogs' - PRINCIPALS = 'principals' - PRINCIPAL_ROLES = 'principal-roles' - CATALOG_ROLES = 'catalog-roles' - PRIVILEGES = 'privileges' - NAMESPACES = 'namespaces' - PROFILES = 'profiles' + CATALOGS = "catalogs" + PRINCIPALS = "principals" + PRINCIPAL_ROLES = "principal-roles" + CATALOG_ROLES = "catalog-roles" + PRIVILEGES = "privileges" + NAMESPACES = "namespaces" + PROFILES = "profiles" class Subcommands: @@ -68,19 +68,19 @@ class Subcommands: all these subcommands. """ - CREATE = 'create' - DELETE = 'delete' - GET = 'get' - LIST = 'list' - UPDATE = 'update' - ROTATE_CREDENTIALS = 'rotate-credentials' - CATALOG = 'catalog' - NAMESPACE = 'namespace' - TABLE = 'table' - VIEW = 'view' - GRANT = 'grant' - REVOKE = 'revoke' - ACCESS = 'access' + CREATE = "create" + DELETE = "delete" + GET = "get" + LIST = "list" + UPDATE = "update" + ROTATE_CREDENTIALS = "rotate-credentials" + CATALOG = "catalog" + NAMESPACE = "namespace" + TABLE = "table" + VIEW = "view" + GRANT = "grant" + REVOKE = "revoke" + ACCESS = "access" class Actions: @@ -89,8 +89,8 @@ class Actions: `privileges` command support actions. """ - GRANT = 'grant' - REVOKE = 'revoke' + GRANT = "grant" + REVOKE = "revoke" class Arguments: @@ -102,40 +102,40 @@ class Arguments: These values should be snake_case, but they will get mapped to kebab-case in `Parser.parse` """ - TYPE = 'type' - DEFAULT_BASE_LOCATION = 'default_base_location' - STORAGE_TYPE = 'storage_type' - ALLOWED_LOCATION = 'allowed_location' - ROLE_ARN = 'role_arn' - EXTERNAL_ID = 'external_id' - USER_ARN = 'user_arn' - TENANT_ID = 'tenant_id' - MULTI_TENANT_APP_NAME = 'multi_tenant_app_name' - CONSENT_URL = 'consent_url' - SERVICE_ACCOUNT = 'service_account' - CATALOG_ROLE = 'catalog_role' - CATALOG = 'catalog' - PRINCIPAL = 'principal' - CLIENT_ID = 'client_id' - PRINCIPAL_ROLE = 'principal_role' - PROPERTY = 'property' - SET_PROPERTY = 'set_property' - REMOVE_PROPERTY = 'remove_property' - PRIVILEGE = 'privilege' - NAMESPACE = 'namespace' - TABLE = 'table' - VIEW = 'view' - CASCADE = 'cascade' - CLIENT_SECRET = 'client_secret' - ACCESS_TOKEN = 'access_token' - HOST = 'host' - PORT = 'port' - BASE_URL = 'base_url' - PARENT = 'parent' - LOCATION = 'location' - REGION = 'region' - PROFILE = 'profile' - PROXY = 'proxy' + TYPE = "type" + DEFAULT_BASE_LOCATION = "default_base_location" + STORAGE_TYPE = "storage_type" + ALLOWED_LOCATION = "allowed_location" + ROLE_ARN = "role_arn" + EXTERNAL_ID = "external_id" + USER_ARN = "user_arn" + TENANT_ID = "tenant_id" + MULTI_TENANT_APP_NAME = "multi_tenant_app_name" + CONSENT_URL = "consent_url" + SERVICE_ACCOUNT = "service_account" + CATALOG_ROLE = "catalog_role" + CATALOG = "catalog" + PRINCIPAL = "principal" + CLIENT_ID = "client_id" + PRINCIPAL_ROLE = "principal_role" + PROPERTY = "property" + SET_PROPERTY = "set_property" + REMOVE_PROPERTY = "remove_property" + PRIVILEGE = "privilege" + NAMESPACE = "namespace" + TABLE = "table" + VIEW = "view" + CASCADE = "cascade" + CLIENT_SECRET = "client_secret" + ACCESS_TOKEN = "access_token" + HOST = "host" + PORT = "port" + BASE_URL = "base_url" + PARENT = "parent" + LOCATION = "location" + REGION = "region" + PROFILE = "profile" + PROXY = "proxy" class Hints: @@ -145,97 +145,119 @@ class Hints: parameter used by `catalog-roles create` and `catalog-roles delete` may be the same. """ - PROPERTY = ('A key/value pair such as: tag=value. Multiple can be provided by specifying this option' - ' more than once') - SET_PROPERTY = ('A key/value pair such as: tag=value. Merges the specified key/value into an existing' - ' properties map by updating the value if the key already exists or creating a new' - ' entry if not. Multiple can be provided by specifying this option more than once') - REMOVE_PROPERTY = ('A key to remove from a properties map. If the key already does not exist then' - ' no action is takn for the specified key. If properties are also being set in' - ' the same update command then the list of removals is applied last. Multiple' - ' can be provided by specifying this option more than once') + PROPERTY = ( + "A key/value pair such as: tag=value. Multiple can be provided by specifying this option" + " more than once" + ) + SET_PROPERTY = ( + "A key/value pair such as: tag=value. Merges the specified key/value into an existing" + " properties map by updating the value if the key already exists or creating a new" + " entry if not. Multiple can be provided by specifying this option more than once" + ) + REMOVE_PROPERTY = ( + "A key to remove from a properties map. If the key already does not exist then" + " no action is takn for the specified key. If properties are also being set in" + " the same update command then the list of removals is applied last. Multiple" + " can be provided by specifying this option more than once" + ) class Catalogs: - GRANT = 'Grant a catalog role to a catalog' - REVOKE = 'Revoke a catalog role from a catalog' + GRANT = "Grant a catalog role to a catalog" + REVOKE = "Revoke a catalog role from a catalog" class Create: - TYPE = 'The type of catalog to create in [INTERNAL, EXTERNAL]. INTERNAL by default.' - DEFAULT_BASE_LOCATION = '(Required) Default base location of the catalog' - STORAGE_TYPE = '(Required) The type of storage to use for the catalog' - ALLOWED_LOCATION = ('An allowed location for files tracked by the catalog. ' - 'Multiple locations can be provided by specifying this option more than once.') - - ROLE_ARN = '(Required for S3) A role ARN to use when connecting to S3' - EXTERNAL_ID = '(Only for S3) The external ID to use when connecting to S3' - REGION = '(Only for S3) The region to use when connecting to S3' - USER_ARN = '(Only for S3) A user ARN to use when connecting to S3' - - TENANT_ID = '(Required for Azure) A tenant ID to use when connecting to Azure Storage' - MULTI_TENANT_APP_NAME = '(Only for Azure) The app name to use when connecting to Azure Storage' - CONSENT_URL = '(Only for Azure) A consent URL granting permissions for the Azure Storage location' - - SERVICE_ACCOUNT = '(Only for GCS) The service account to use when connecting to GCS' + TYPE = "The type of catalog to create in [INTERNAL, EXTERNAL]. INTERNAL by default." + DEFAULT_BASE_LOCATION = "(Required) Default base location of the catalog" + STORAGE_TYPE = "(Required) The type of storage to use for the catalog" + ALLOWED_LOCATION = ( + "An allowed location for files tracked by the catalog. " + "Multiple locations can be provided by specifying this option more than once." + ) + + ROLE_ARN = "(Required for S3) A role ARN to use when connecting to S3" + EXTERNAL_ID = "(Only for S3) The external ID to use when connecting to S3" + REGION = "(Only for S3) The region to use when connecting to S3" + USER_ARN = "(Only for S3) A user ARN to use when connecting to S3" + + TENANT_ID = "(Required for Azure) A tenant ID to use when connecting to Azure Storage" + MULTI_TENANT_APP_NAME = ( + "(Only for Azure) The app name to use when connecting to Azure Storage" + ) + CONSENT_URL = "(Only for Azure) A consent URL granting permissions for the Azure Storage location" + + SERVICE_ACCOUNT = ( + "(Only for GCS) The service account to use when connecting to GCS" + ) class Update: - DEFAULT_BASE_LOCATION = 'A new default base location for the catalog' + DEFAULT_BASE_LOCATION = "A new default base location for the catalog" class Principals: class Create: - TYPE = 'The type of principal to create in [SERVICE]' - NAME = 'The principal name' - CLIENT_ID = 'The output-only OAuth clientId associated with this principal if applicable' + TYPE = "The type of principal to create in [SERVICE]" + NAME = "The principal name" + CLIENT_ID = "The output-only OAuth clientId associated with this principal if applicable" class Revoke: - PRINCIPAL_ROLE = 'A principal role to revoke from this principal' + PRINCIPAL_ROLE = "A principal role to revoke from this principal" class PrincipalRoles: - PRINCIPAL_ROLE = 'The name of a principal role' - LIST = 'List principal roles, optionally limited to those held a given principal' + PRINCIPAL_ROLE = "The name of a principal role" + LIST = ( + "List principal roles, optionally limited to those held a given principal" + ) - GRANT = 'Grant a principal role to a principal' - REVOKE = 'Revoke a principal role from a principal' + GRANT = "Grant a principal role to a principal" + REVOKE = "Revoke a principal role from a principal" class Grant: - PRINCIPAL = 'A principal to grant this principal role to' + PRINCIPAL = "A principal to grant this principal role to" class Revoke: - PRINCIPAL = 'A principal to revoke this principal role from' + PRINCIPAL = "A principal to revoke this principal role from" class List: - CATALOG_ROLE = ('The name of a catalog role. If provided, show only principal roles assigned to this' - ' catalog role.') - PRINCIPAL_NAME = ('The name of a principal. If provided, show only principal roles assigned to this' - ' principal.') + CATALOG_ROLE = ( + "The name of a catalog role. If provided, show only principal roles assigned to this" + " catalog role." + ) + PRINCIPAL_NAME = ( + "The name of a principal. If provided, show only principal roles assigned to this" + " principal." + ) class CatalogRoles: - CATALOG_NAME = 'The name of an existing catalog' - CATALOG_ROLE = 'The name of a catalog role' - LIST = 'List catalog roles within a catalog. Optionally, specify a principal role.' - REVOKE_CATALOG_ROLE = 'Revoke a catalog role from a principal role' - GRANT_CATALOG_ROLE = 'Grant a catalog role to a principal role' + CATALOG_NAME = "The name of an existing catalog" + CATALOG_ROLE = "The name of a catalog role" + LIST = ( + "List catalog roles within a catalog. Optionally, specify a principal role." + ) + REVOKE_CATALOG_ROLE = "Revoke a catalog role from a principal role" + GRANT_CATALOG_ROLE = "Grant a catalog role to a principal role" class Grant: - CATALOG_NAME = 'The name of a catalog' - CATALOG_ROLE = 'The name of a catalog role' - PRIVILEGE = 'The privilege to grant or revoke' - NAMESPACE = 'A period-delimited namespace' - TABLE = 'The name of a table' - VIEW = 'The name of a view' - CASCADE = 'When revoking privileges, additionally revoke privileges that depend on the specified privilege' + CATALOG_NAME = "The name of a catalog" + CATALOG_ROLE = "The name of a catalog role" + PRIVILEGE = "The privilege to grant or revoke" + NAMESPACE = "A period-delimited namespace" + TABLE = "The name of a table" + VIEW = "The name of a view" + CASCADE = "When revoking privileges, additionally revoke privileges that depend on the specified privilege" class Namespaces: - LOCATION = 'If specified, the location at which to store the namespace and entities inside it' - PARENT = 'If specified, list namespaces inside this parent namespace' + LOCATION = "If specified, the location at which to store the namespace and entities inside it" + PARENT = "If specified, list namespaces inside this parent namespace" UNIT_SEPARATOR = chr(0x1F) -CLIENT_ID_ENV = 'CLIENT_ID' -CLIENT_SECRET_ENV = 'CLIENT_SECRET' -CLIENT_PROFILE_ENV = 'CLIENT_PROFILE' -DEFAULT_HOSTNAME = 'localhost' +CLIENT_ID_ENV = "CLIENT_ID" +CLIENT_SECRET_ENV = "CLIENT_SECRET" +CLIENT_PROFILE_ENV = "CLIENT_PROFILE" +DEFAULT_HOSTNAME = "localhost" DEFAULT_PORT = 8181 -CONFIG_DIR = os.environ.get('SCRIPT_DIR') +CONFIG_DIR = os.environ.get("SCRIPT_DIR") if CONFIG_DIR is None: - raise Exception("The SCRIPT_DIR environment variable is not set. Please set it to the Polaris's script directory.") -CONFIG_FILE = os.path.join(CONFIG_DIR, '.polaris.json') + raise Exception( + "The SCRIPT_DIR environment variable is not set. Please set it to the Polaris's script directory." + ) +CONFIG_FILE = os.path.join(CONFIG_DIR, ".polaris.json") diff --git a/client/python/cli/options/option_tree.py b/client/python/cli/options/option_tree.py index f926230e19..aaec9476d8 100644 --- a/client/python/cli/options/option_tree.py +++ b/client/python/cli/options/option_tree.py @@ -19,7 +19,16 @@ from dataclasses import dataclass, field from typing import List -from cli.constants import StorageType, CatalogType, PrincipalType, Hints, Commands, Arguments, Subcommands, Actions +from cli.constants import ( + StorageType, + CatalogType, + PrincipalType, + Hints, + Commands, + Arguments, + Subcommands, + Actions, +) @dataclass @@ -37,18 +46,19 @@ class Argument: default: object = None def __post_init__(self): - if self.name.startswith('--'): - raise Exception(f'Argument name {self.name} starts with `--`: should this be a flag_name?') + if self.name.startswith("--"): + raise Exception( + f"Argument name {self.name} starts with `--`: should this be a flag_name?" + ) @staticmethod def to_flag_name(argument_name): - return '--' + argument_name.replace('_', '-') + return "--" + argument_name.replace("_", "-") def get_flag_name(self): return Argument.to_flag_name(self.name) - @dataclass class Option: """ @@ -60,7 +70,7 @@ class Option: hint: str = None input_name: str = None args: List[Argument] = field(default_factory=list) - children: List['Option'] = field(default_factory=list) + children: List["Option"] = field(default_factory=list) class OptionTree: @@ -71,171 +81,537 @@ class OptionTree: _CATALOG_ROLE_AND_CATALOG = [ Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.CATALOG_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) + Argument(Arguments.CATALOG_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE), ] @staticmethod def get_tree() -> List[Option]: return [ - Option(Commands.CATALOGS, 'manage catalogs', children=[ - Option(Subcommands.CREATE, args=[ - Argument(Arguments.TYPE, str, Hints.Catalogs.Create.TYPE, lower=True, - choices=[ct.value for ct in CatalogType], default=CatalogType.INTERNAL.value), - Argument(Arguments.STORAGE_TYPE, str, Hints.Catalogs.Create.STORAGE_TYPE, lower=True, - choices=[st.value for st in StorageType]), - Argument(Arguments.DEFAULT_BASE_LOCATION, str, Hints.Catalogs.Create.DEFAULT_BASE_LOCATION), - Argument(Arguments.ALLOWED_LOCATION, str, Hints.Catalogs.Create.ALLOWED_LOCATION, - allow_repeats=True), - Argument(Arguments.ROLE_ARN, str, Hints.Catalogs.Create.ROLE_ARN), - Argument(Arguments.REGION, str, Hints.Catalogs.Create.REGION), - Argument(Arguments.EXTERNAL_ID, str, Hints.Catalogs.Create.EXTERNAL_ID), - Argument(Arguments.TENANT_ID, str, Hints.Catalogs.Create.TENANT_ID), - Argument(Arguments.MULTI_TENANT_APP_NAME, str, Hints.Catalogs.Create.MULTI_TENANT_APP_NAME), - Argument(Arguments.CONSENT_URL, str, Hints.Catalogs.Create.CONSENT_URL), - Argument(Arguments.SERVICE_ACCOUNT, str, Hints.Catalogs.Create.SERVICE_ACCOUNT), - Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True), - ], input_name=Arguments.CATALOG), - Option(Subcommands.DELETE, input_name=Arguments.CATALOG), - Option(Subcommands.GET, input_name=Arguments.CATALOG), - Option(Subcommands.LIST, args=[ - Argument(Arguments.PRINCIPAL_ROLE, str, Hints.PrincipalRoles.PRINCIPAL_ROLE) - ]), - Option(Subcommands.UPDATE, args=[ - Argument(Arguments.DEFAULT_BASE_LOCATION, str, Hints.Catalogs.Update.DEFAULT_BASE_LOCATION), - Argument(Arguments.ALLOWED_LOCATION, str, Hints.Catalogs.Create.ALLOWED_LOCATION, - allow_repeats=True), - Argument(Arguments.REGION, str, Hints.Catalogs.Create.REGION), - Argument(Arguments.SET_PROPERTY, str, Hints.SET_PROPERTY, allow_repeats=True), - Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), - ], input_name=Arguments.CATALOG) - ]), - Option(Commands.PRINCIPALS, 'manage principals', children=[ - Option(Subcommands.CREATE, args=[ - Argument(Arguments.TYPE, str, Hints.Principals.Create.TYPE, lower=True, - choices=[pt.value for pt in PrincipalType], default=PrincipalType.SERVICE.value), - Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) - ], input_name=Arguments.PRINCIPAL), - Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL), - Option(Subcommands.GET, input_name=Arguments.PRINCIPAL), - Option(Subcommands.LIST), - Option(Subcommands.ROTATE_CREDENTIALS, input_name=Arguments.PRINCIPAL), - Option(Subcommands.UPDATE, args=[ - Argument(Arguments.SET_PROPERTY, str, Hints.SET_PROPERTY, allow_repeats=True), - Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), - ], input_name=Arguments.PRINCIPAL), - Option(Subcommands.ACCESS, input_name=Arguments.PRINCIPAL), - ]), - Option(Commands.PRINCIPAL_ROLES, 'manage principal roles', children=[ - Option(Subcommands.CREATE, args=[ - Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) - ], input_name=Arguments.PRINCIPAL_ROLE), - Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL_ROLE), - Option(Subcommands.GET, input_name=Arguments.PRINCIPAL_ROLE), - Option(Subcommands.LIST, hint=Hints.PrincipalRoles.LIST, args=[ - Argument(Arguments.CATALOG_ROLE, str, Hints.PrincipalRoles.List.CATALOG_ROLE), - Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.List.PRINCIPAL_NAME) - ]), - Option(Subcommands.UPDATE, args=[ - Argument(Arguments.SET_PROPERTY, str, Hints.SET_PROPERTY, allow_repeats=True), - Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), - ], input_name=Arguments.PRINCIPAL_ROLE), - Option(Subcommands.GRANT, hint=Hints.PrincipalRoles.GRANT, args=[ - Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.Grant.PRINCIPAL) - ], input_name=Arguments.PRINCIPAL_ROLE), - Option(Subcommands.REVOKE, hint=Hints.PrincipalRoles.REVOKE, args=[ - Argument(Arguments.PRINCIPAL, str, Hints.PrincipalRoles.Revoke.PRINCIPAL) - ], input_name=Arguments.PRINCIPAL_ROLE) - ]), - Option(Commands.CATALOG_ROLES, 'manage catalog roles', children=[ - Option(Subcommands.CREATE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) - ], input_name=Arguments.CATALOG_ROLE), - Option(Subcommands.DELETE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - ], input_name=Arguments.CATALOG_ROLE), - Option(Subcommands.GET, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - ], input_name=Arguments.CATALOG_ROLE), - Option(Subcommands.LIST, hint=Hints.CatalogRoles.LIST, args=[ - Argument(Arguments.PRINCIPAL_ROLE, str, Hints.PrincipalRoles.PRINCIPAL_ROLE) - ], input_name=Arguments.CATALOG), - Option(Subcommands.UPDATE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.SET_PROPERTY, str, Hints.SET_PROPERTY, allow_repeats=True), - Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), - ], input_name=Arguments.CATALOG_ROLE), - Option(Subcommands.GRANT, hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.PRINCIPAL_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) - ], input_name=Arguments.CATALOG_ROLE), - Option(Subcommands.REVOKE, hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.PRINCIPAL_ROLE, str, Hints.CatalogRoles.CATALOG_ROLE) - ], input_name=Arguments.CATALOG_ROLE) - ]), - Option(Commands.PRIVILEGES, 'manage privileges for a catalog role', children=[ - Option(Subcommands.LIST, args=OptionTree._CATALOG_ROLE_AND_CATALOG), - Option(Subcommands.CATALOG, children=[ - Option(Actions.GRANT, args=OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - Option(Actions.REVOKE, args=[ - Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - ]), - Option(Subcommands.NAMESPACE, children=[ - Option(Actions.GRANT, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - Option(Actions.REVOKE, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), - Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - ]), - Option(Subcommands.TABLE, children=[ - Option(Actions.GRANT, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), - Argument(Arguments.TABLE, str, Hints.Grant.TABLE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - Option(Actions.REVOKE, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), - Argument(Arguments.TABLE, str, Hints.Grant.TABLE), - Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - ]), - Option(Subcommands.VIEW, children=[ - Option(Actions.GRANT, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), - Argument(Arguments.VIEW, str, Hints.Grant.VIEW) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - Option(Actions.REVOKE, args=[ - Argument(Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE), - Argument(Arguments.VIEW, str, Hints.Grant.VIEW), - Argument(Arguments.CASCADE, bool, Hints.Grant.CASCADE) - ] + OptionTree._CATALOG_ROLE_AND_CATALOG, input_name=Arguments.PRIVILEGE), - ]) - ]), - Option(Commands.NAMESPACES, 'manage namespaces', children=[ - Option(Subcommands.CREATE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.LOCATION, str, Hints.Namespaces.LOCATION), - Argument(Arguments.PROPERTY, str, Hints.PROPERTY, allow_repeats=True) - ], input_name=Arguments.NAMESPACE), - Option(Subcommands.LIST, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME), - Argument(Arguments.PARENT, str, Hints.Namespaces.PARENT) - ]), - Option(Subcommands.DELETE, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME) - ], input_name=Arguments.NAMESPACE), - Option(Subcommands.GET, args=[ - Argument(Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME) - ], input_name=Arguments.NAMESPACE), - ]), - Option(Commands.PROFILES, 'manage profiles', children=[ - Option(Subcommands.CREATE, input_name=Arguments.PROFILE), - Option(Subcommands.DELETE, input_name=Arguments.PROFILE), - Option(Subcommands.UPDATE, input_name=Arguments.PROFILE), - Option(Subcommands.GET, input_name=Arguments.PROFILE), - Option(Subcommands.LIST), - ]) + Option( + Commands.CATALOGS, + "manage catalogs", + children=[ + Option( + Subcommands.CREATE, + args=[ + Argument( + Arguments.TYPE, + str, + Hints.Catalogs.Create.TYPE, + lower=True, + choices=[ct.value for ct in CatalogType], + default=CatalogType.INTERNAL.value, + ), + Argument( + Arguments.STORAGE_TYPE, + str, + Hints.Catalogs.Create.STORAGE_TYPE, + lower=True, + choices=[st.value for st in StorageType], + ), + Argument( + Arguments.DEFAULT_BASE_LOCATION, + str, + Hints.Catalogs.Create.DEFAULT_BASE_LOCATION, + ), + Argument( + Arguments.ALLOWED_LOCATION, + str, + Hints.Catalogs.Create.ALLOWED_LOCATION, + allow_repeats=True, + ), + Argument( + Arguments.ROLE_ARN, str, Hints.Catalogs.Create.ROLE_ARN + ), + Argument( + Arguments.REGION, str, Hints.Catalogs.Create.REGION + ), + Argument( + Arguments.EXTERNAL_ID, + str, + Hints.Catalogs.Create.EXTERNAL_ID, + ), + Argument( + Arguments.TENANT_ID, + str, + Hints.Catalogs.Create.TENANT_ID, + ), + Argument( + Arguments.MULTI_TENANT_APP_NAME, + str, + Hints.Catalogs.Create.MULTI_TENANT_APP_NAME, + ), + Argument( + Arguments.CONSENT_URL, + str, + Hints.Catalogs.Create.CONSENT_URL, + ), + Argument( + Arguments.SERVICE_ACCOUNT, + str, + Hints.Catalogs.Create.SERVICE_ACCOUNT, + ), + Argument( + Arguments.PROPERTY, + str, + Hints.PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.CATALOG, + ), + Option(Subcommands.DELETE, input_name=Arguments.CATALOG), + Option(Subcommands.GET, input_name=Arguments.CATALOG), + Option( + Subcommands.LIST, + args=[ + Argument( + Arguments.PRINCIPAL_ROLE, + str, + Hints.PrincipalRoles.PRINCIPAL_ROLE, + ) + ], + ), + Option( + Subcommands.UPDATE, + args=[ + Argument( + Arguments.DEFAULT_BASE_LOCATION, + str, + Hints.Catalogs.Update.DEFAULT_BASE_LOCATION, + ), + Argument( + Arguments.ALLOWED_LOCATION, + str, + Hints.Catalogs.Create.ALLOWED_LOCATION, + allow_repeats=True, + ), + Argument( + Arguments.REGION, str, Hints.Catalogs.Create.REGION + ), + Argument( + Arguments.SET_PROPERTY, + str, + Hints.SET_PROPERTY, + allow_repeats=True, + ), + Argument( + Arguments.REMOVE_PROPERTY, + str, + Hints.REMOVE_PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.CATALOG, + ), + ], + ), + Option( + Commands.PRINCIPALS, + "manage principals", + children=[ + Option( + Subcommands.CREATE, + args=[ + Argument( + Arguments.TYPE, + str, + Hints.Principals.Create.TYPE, + lower=True, + choices=[pt.value for pt in PrincipalType], + default=PrincipalType.SERVICE.value, + ), + Argument( + Arguments.PROPERTY, + str, + Hints.PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.PRINCIPAL, + ), + Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL), + Option(Subcommands.GET, input_name=Arguments.PRINCIPAL), + Option(Subcommands.LIST), + Option( + Subcommands.ROTATE_CREDENTIALS, input_name=Arguments.PRINCIPAL + ), + Option( + Subcommands.UPDATE, + args=[ + Argument( + Arguments.SET_PROPERTY, + str, + Hints.SET_PROPERTY, + allow_repeats=True, + ), + Argument( + Arguments.REMOVE_PROPERTY, + str, + Hints.REMOVE_PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.PRINCIPAL, + ), + Option(Subcommands.ACCESS, input_name=Arguments.PRINCIPAL), + ], + ), + Option( + Commands.PRINCIPAL_ROLES, + "manage principal roles", + children=[ + Option( + Subcommands.CREATE, + args=[ + Argument( + Arguments.PROPERTY, + str, + Hints.PROPERTY, + allow_repeats=True, + ) + ], + input_name=Arguments.PRINCIPAL_ROLE, + ), + Option(Subcommands.DELETE, input_name=Arguments.PRINCIPAL_ROLE), + Option(Subcommands.GET, input_name=Arguments.PRINCIPAL_ROLE), + Option( + Subcommands.LIST, + hint=Hints.PrincipalRoles.LIST, + args=[ + Argument( + Arguments.CATALOG_ROLE, + str, + Hints.PrincipalRoles.List.CATALOG_ROLE, + ), + Argument( + Arguments.PRINCIPAL, + str, + Hints.PrincipalRoles.List.PRINCIPAL_NAME, + ), + ], + ), + Option( + Subcommands.UPDATE, + args=[ + Argument( + Arguments.SET_PROPERTY, + str, + Hints.SET_PROPERTY, + allow_repeats=True, + ), + Argument( + Arguments.REMOVE_PROPERTY, + str, + Hints.REMOVE_PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.PRINCIPAL_ROLE, + ), + Option( + Subcommands.GRANT, + hint=Hints.PrincipalRoles.GRANT, + args=[ + Argument( + Arguments.PRINCIPAL, + str, + Hints.PrincipalRoles.Grant.PRINCIPAL, + ) + ], + input_name=Arguments.PRINCIPAL_ROLE, + ), + Option( + Subcommands.REVOKE, + hint=Hints.PrincipalRoles.REVOKE, + args=[ + Argument( + Arguments.PRINCIPAL, + str, + Hints.PrincipalRoles.Revoke.PRINCIPAL, + ) + ], + input_name=Arguments.PRINCIPAL_ROLE, + ), + ], + ), + Option( + Commands.CATALOG_ROLES, + "manage catalog roles", + children=[ + Option( + Subcommands.CREATE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument( + Arguments.PROPERTY, + str, + Hints.PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + Option( + Subcommands.DELETE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + Option( + Subcommands.GET, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + Option( + Subcommands.LIST, + hint=Hints.CatalogRoles.LIST, + args=[ + Argument( + Arguments.PRINCIPAL_ROLE, + str, + Hints.PrincipalRoles.PRINCIPAL_ROLE, + ) + ], + input_name=Arguments.CATALOG, + ), + Option( + Subcommands.UPDATE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument( + Arguments.SET_PROPERTY, + str, + Hints.SET_PROPERTY, + allow_repeats=True, + ), + Argument( + Arguments.REMOVE_PROPERTY, + str, + Hints.REMOVE_PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + Option( + Subcommands.GRANT, + hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument( + Arguments.PRINCIPAL_ROLE, + str, + Hints.CatalogRoles.CATALOG_ROLE, + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + Option( + Subcommands.REVOKE, + hint=Hints.CatalogRoles.GRANT_CATALOG_ROLE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument( + Arguments.PRINCIPAL_ROLE, + str, + Hints.CatalogRoles.CATALOG_ROLE, + ), + ], + input_name=Arguments.CATALOG_ROLE, + ), + ], + ), + Option( + Commands.PRIVILEGES, + "manage privileges for a catalog role", + children=[ + Option(Subcommands.LIST, args=OptionTree._CATALOG_ROLE_AND_CATALOG), + Option( + Subcommands.CATALOG, + children=[ + Option( + Actions.GRANT, + args=OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + Option( + Actions.REVOKE, + args=[ + Argument( + Arguments.CASCADE, bool, Hints.Grant.CASCADE + ) + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + ], + ), + Option( + Subcommands.NAMESPACE, + children=[ + Option( + Actions.GRANT, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ) + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + Option( + Actions.REVOKE, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ), + Argument( + Arguments.CASCADE, bool, Hints.Grant.CASCADE + ), + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + ], + ), + Option( + Subcommands.TABLE, + children=[ + Option( + Actions.GRANT, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ), + Argument(Arguments.TABLE, str, Hints.Grant.TABLE), + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + Option( + Actions.REVOKE, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ), + Argument(Arguments.TABLE, str, Hints.Grant.TABLE), + Argument( + Arguments.CASCADE, bool, Hints.Grant.CASCADE + ), + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + ], + ), + Option( + Subcommands.VIEW, + children=[ + Option( + Actions.GRANT, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ), + Argument(Arguments.VIEW, str, Hints.Grant.VIEW), + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + Option( + Actions.REVOKE, + args=[ + Argument( + Arguments.NAMESPACE, str, Hints.Grant.NAMESPACE + ), + Argument(Arguments.VIEW, str, Hints.Grant.VIEW), + Argument( + Arguments.CASCADE, bool, Hints.Grant.CASCADE + ), + ] + + OptionTree._CATALOG_ROLE_AND_CATALOG, + input_name=Arguments.PRIVILEGE, + ), + ], + ), + ], + ), + Option( + Commands.NAMESPACES, + "manage namespaces", + children=[ + Option( + Subcommands.CREATE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument( + Arguments.LOCATION, str, Hints.Namespaces.LOCATION + ), + Argument( + Arguments.PROPERTY, + str, + Hints.PROPERTY, + allow_repeats=True, + ), + ], + input_name=Arguments.NAMESPACE, + ), + Option( + Subcommands.LIST, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ), + Argument(Arguments.PARENT, str, Hints.Namespaces.PARENT), + ], + ), + Option( + Subcommands.DELETE, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ) + ], + input_name=Arguments.NAMESPACE, + ), + Option( + Subcommands.GET, + args=[ + Argument( + Arguments.CATALOG, str, Hints.CatalogRoles.CATALOG_NAME + ) + ], + input_name=Arguments.NAMESPACE, + ), + ], + ), + Option( + Commands.PROFILES, + "manage profiles", + children=[ + Option(Subcommands.CREATE, input_name=Arguments.PROFILE), + Option(Subcommands.DELETE, input_name=Arguments.PROFILE), + Option(Subcommands.UPDATE, input_name=Arguments.PROFILE), + Option(Subcommands.GET, input_name=Arguments.PROFILE), + Option(Subcommands.LIST), + ], + ), ] diff --git a/client/python/cli/options/parser.py b/client/python/cli/options/parser.py index 8613474fe7..26f5c8dfcb 100644 --- a/client/python/cli/options/parser.py +++ b/client/python/cli/options/parser.py @@ -37,57 +37,84 @@ class Parser(object): """ _ROOT_ARGUMENTS = [ - Argument(Arguments.HOST, str, hint='hostname'), - Argument(Arguments.PORT, int, hint='port'), - Argument(Arguments.BASE_URL, str, hint='complete base URL instead of hostname:port'), - Argument(Arguments.CLIENT_ID, str, hint='client ID for token-based authentication'), - Argument(Arguments.CLIENT_SECRET, str, hint='client secret for token-based authentication'), - Argument(Arguments.ACCESS_TOKEN, str, hint='access token for token-based authentication'), - Argument(Arguments.PROFILE, str, hint='profile for token-based authentication'), - Argument(Arguments.PROXY, str, hint='proxy URL'), + Argument(Arguments.HOST, str, hint="hostname"), + Argument(Arguments.PORT, int, hint="port"), + Argument( + Arguments.BASE_URL, str, hint="complete base URL instead of hostname:port" + ), + Argument( + Arguments.CLIENT_ID, str, hint="client ID for token-based authentication" + ), + Argument( + Arguments.CLIENT_SECRET, + str, + hint="client secret for token-based authentication", + ), + Argument( + Arguments.ACCESS_TOKEN, + str, + hint="access token for token-based authentication", + ), + Argument(Arguments.PROFILE, str, hint="profile for token-based authentication"), + Argument(Arguments.PROXY, str, hint="proxy URL"), ] @staticmethod def _build_parser() -> argparse.ArgumentParser: - parser = TreeHelpParser(description='Polaris CLI') + parser = TreeHelpParser(description="Polaris CLI") for arg in Parser._ROOT_ARGUMENTS: if arg.default is not None: - parser.add_argument(arg.get_flag_name(), type=arg.type, help=arg.hint, default=arg.default) + parser.add_argument( + arg.get_flag_name(), + type=arg.type, + help=arg.hint, + default=arg.default, + ) else: parser.add_argument(arg.get_flag_name(), type=arg.type, help=arg.hint) # Add everything from the option tree to the parser: def add_arguments(parser, args: List[Argument]): for arg in args: - kwargs = {'help': arg.hint, 'type': arg.type} + kwargs = {"help": arg.hint, "type": arg.type} if arg.choices: - kwargs['choices'] = arg.choices + kwargs["choices"] = arg.choices if arg.lower: - kwargs['type'] = kwargs['type'].lower + kwargs["type"] = kwargs["type"].lower if arg.default: - kwargs['default'] = arg.default + kwargs["default"] = arg.default if arg.type is bool: - del kwargs['type'] - parser.add_argument(arg.get_flag_name(), **kwargs, action='store_true') + del kwargs["type"] + parser.add_argument( + arg.get_flag_name(), **kwargs, action="store_true" + ) elif arg.allow_repeats: - parser.add_argument(arg.get_flag_name(), **kwargs, action='append') + parser.add_argument(arg.get_flag_name(), **kwargs, action="append") else: parser.add_argument(arg.get_flag_name(), **kwargs) def recurse_options(subparser, options: List[Option]): for option in options: - option_parser = subparser.add_parser(option.name, help=option.hint or option.name) + option_parser = subparser.add_parser( + option.name, help=option.hint or option.name + ) add_arguments(option_parser, option.args) if option.input_name: - option_parser.add_argument(option.input_name, type=str, - help=option.input_name.replace('_', ' '), default=None) + option_parser.add_argument( + option.input_name, + type=str, + help=option.input_name.replace("_", " "), + default=None, + ) if option.children: - children_subparser = option_parser.add_subparsers(dest=f'{option.name}_subcommand', required=False) + children_subparser = option_parser.add_subparsers( + dest=f"{option.name}_subcommand", required=False + ) recurse_options(children_subparser, option.children) - subparser = parser.add_subparsers(dest='command', required=False) + subparser = parser.add_subparsers(dest="command", required=False) recurse_options(subparser, OptionTree.get_tree()) return parser @@ -102,13 +129,13 @@ def parse_properties(properties: List[str]) -> Optional[Dict[str, str]]: return None results = dict() for property in properties: - if '=' not in property: - raise Exception(f'Could not parse property `{property}`') - key, value = property.split('=', 1) + if "=" not in property: + raise Exception(f"Could not parse property `{property}`") + key, value = property.split("=", 1) if not value: - raise Exception(f'Could not parse property `{property}`') + raise Exception(f"Could not parse property `{property}`") if key in results: - raise Exception(f'Duplicate property key `{key}`') + raise Exception(f"Duplicate property key `{key}`") results[key] = value return results @@ -118,19 +145,21 @@ class TreeHelpParser(argparse.ArgumentParser): Replaces the default help behavior with a more readable message. """ - INDENT = ' ' * 2 + INDENT = " " * 2 def parse_args(self, args=None, namespace=None): if args is None: args = sys.argv[1:] - help_index = min([float('inf')] + [args.index(x) for x in ['-h', '--help'] if x in args]) - if help_index < float('inf'): + help_index = min( + [float("inf")] + [args.index(x) for x in ["-h", "--help"] if x in args] + ) + if help_index < float("inf"): tree_str = self._get_tree_str(args[:help_index]) if tree_str: - print(f'input: polaris {" ".join(args)}') - print('options:') + print(f"input: polaris {' '.join(args)}") + print("options:") print(tree_str) - print('\n') + print("\n") self.print_usage() super().exit() else: @@ -141,11 +170,15 @@ def parse_args(self, args=None, namespace=None): def _get_tree_str(self, args: List[str]) -> Optional[str]: command_path = self._get_command_path(args, OptionTree.get_tree()) if len(command_path) == 0: - result = TreeHelpParser.INDENT + 'polaris' + result = TreeHelpParser.INDENT + "polaris" for arg in Parser._ROOT_ARGUMENTS: - result += '\n' + (TreeHelpParser.INDENT * 2) + f"{arg.get_flag_name()} {arg.hint}" + result += ( + "\n" + + (TreeHelpParser.INDENT * 2) + + f"{arg.get_flag_name()} {arg.hint}" + ) for option in OptionTree.get_tree(): - result += '\n' + self._get_tree_for_option(option, indent=2) + result += "\n" + self._get_tree_for_option(option, indent=2) return result else: option_node = self._get_option_node(command_path, OptionTree.get_tree()) @@ -159,19 +192,25 @@ def _get_tree_for_option(self, option: Option, indent=1) -> str: result += (TreeHelpParser.INDENT * indent) + option.name if option.args: - result += '\n' + (TreeHelpParser.INDENT * (indent + 1)) + "Named arguments:" + result += "\n" + (TreeHelpParser.INDENT * (indent + 1)) + "Named arguments:" for arg in option.args: - result += '\n' + (TreeHelpParser.INDENT * (indent + 2)) + f"{arg.get_flag_name()} {arg.hint}" + result += ( + "\n" + + (TreeHelpParser.INDENT * (indent + 2)) + + f"{arg.get_flag_name()} {arg.hint}" + ) if option.input_name: - result += '\n' + (TreeHelpParser.INDENT * (indent + 1)) + "Positional arguments:" - result += '\n' + (TreeHelpParser.INDENT * (indent + 2)) + option.input_name + result += ( + "\n" + (TreeHelpParser.INDENT * (indent + 1)) + "Positional arguments:" + ) + result += "\n" + (TreeHelpParser.INDENT * (indent + 2)) + option.input_name if len(option.args) > 0 and len(option.children) > 0: - result += '\n' + result += "\n" for child in sorted(option.children, key=lambda o: o.name): - result += '\n' + self._get_tree_for_option(child, indent + 1) + result += "\n" + self._get_tree_for_option(child, indent + 1) return result @@ -194,7 +233,9 @@ def _get_command_path(self, args: List[str], options: List[Option]) -> List[str] break return command_path - def _get_option_node(self, command_path: List[str], nodes: List[Option]) -> Optional[Option]: + def _get_option_node( + self, command_path: List[str], nodes: List[Option] + ) -> Optional[Option]: if len(command_path) > 0: for node in nodes: if node.name == command_path[0]: @@ -203,4 +244,3 @@ def _get_option_node(self, command_path: List[str], nodes: List[Option]) -> Opti else: return self._get_option_node(command_path[1:], node.children) return None - diff --git a/client/python/cli/polaris_cli.py b/client/python/cli/polaris_cli.py index d607912326..83341ada41 100644 --- a/client/python/cli/polaris_cli.py +++ b/client/python/cli/polaris_cli.py @@ -24,12 +24,22 @@ from typing import Dict -from cli.constants import Arguments, Commands, CLIENT_ID_ENV, CLIENT_SECRET_ENV, CLIENT_PROFILE_ENV, DEFAULT_HOSTNAME, DEFAULT_PORT, CONFIG_FILE +from cli.constants import ( + Arguments, + Commands, + CLIENT_ID_ENV, + CLIENT_SECRET_ENV, + CLIENT_PROFILE_ENV, + DEFAULT_HOSTNAME, + DEFAULT_PORT, + CONFIG_FILE, +) from cli.options.option_tree import Argument from cli.options.parser import Parser from polaris.management import ApiClient, Configuration from polaris.management import PolarisDefaultApi + class PolarisCli: """ Implements a basic Command-Line Interface (CLI) for interacting with a Polaris service. The CLI can be used to @@ -51,6 +61,7 @@ def execute(args=None): options = Parser.parse(args) if options.command == Commands.PROFILES: from cli.command import Command + command = Command.from_options(options) command.execute() else: @@ -58,6 +69,7 @@ def execute(args=None): with client_builder() as api_client: try: from cli.command import Command + admin_api = PolarisDefaultApi(api_client) command = Command.from_options(options) command.execute(admin_api) @@ -68,15 +80,20 @@ def execute(args=None): @staticmethod def _try_print_exception(e): try: - error = json.loads(e.body)['error'] - sys.stderr.write(f'Exception when communicating with the Polaris server.' - f' {error["type"]}: {error["message"]}{os.linesep}') + error = json.loads(e.body)["error"] + sys.stderr.write( + f"Exception when communicating with the Polaris server." + f" {error['type']}: {error['message']}{os.linesep}" + ) except JSONDecodeError as _: - sys.stderr.write(f'Exception when communicating with the Polaris server.' - f' {e.status}: {e.reason}{os.linesep}') + sys.stderr.write( + f"Exception when communicating with the Polaris server." + f" {e.status}: {e.reason}{os.linesep}" + ) except Exception as _: - sys.stderr.write(f'Exception when communicating with the Polaris server.' - f' {e}{os.linesep}') + sys.stderr.write( + f"Exception when communicating with the Polaris server. {e}{os.linesep}" + ) @staticmethod def _load_profiles() -> Dict[str, Dict[str, str]]: @@ -88,19 +105,19 @@ def _load_profiles() -> Dict[str, Dict[str, str]]: @staticmethod def _get_token(api_client: ApiClient, catalog_url, client_id, client_secret) -> str: response = api_client.call_api( - 'POST', - f'{catalog_url}/oauth/tokens', - header_params={'Content-Type': 'application/x-www-form-urlencoded'}, + "POST", + f"{catalog_url}/oauth/tokens", + header_params={"Content-Type": "application/x-www-form-urlencoded"}, post_params={ - 'grant_type': 'client_credentials', - 'client_id': client_id, - 'client_secret': client_secret, - 'scope': 'PRINCIPAL_ROLE:ALL' - } + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": "PRINCIPAL_ROLE:ALL", + }, ).response.data - if 'access_token' not in json.loads(response): - raise Exception('Failed to get access token') - return json.loads(response)['access_token'] + if "access_token" not in json.loads(response): + raise Exception("Failed to get access token") + return json.loads(response)["access_token"] @staticmethod def _get_client_builder(options): @@ -110,38 +127,50 @@ def _get_client_builder(options): profiles = PolarisCli._load_profiles() profile = profiles.get(client_profile) if not profile: - raise Exception(f'Polaris profile {client_profile} not found') + raise Exception(f"Polaris profile {client_profile} not found") # Determine which credentials to use - client_id = options.client_id or os.getenv(CLIENT_ID_ENV) or profile.get('client_id') - client_secret = options.client_secret or os.getenv(CLIENT_SECRET_ENV) or profile.get('client_secret') - + client_id = ( + options.client_id or os.getenv(CLIENT_ID_ENV) or profile.get("client_id") + ) + client_secret = ( + options.client_secret + or os.getenv(CLIENT_SECRET_ENV) + or profile.get("client_secret") + ) + # Validates has_access_token = options.access_token is not None has_client_secret = client_id is not None and client_secret is not None if has_access_token and (options.client_id or options.client_secret): - raise Exception(f'Please provide credentials via either {Argument.to_flag_name(Arguments.CLIENT_ID)} &' - f' {Argument.to_flag_name(Arguments.CLIENT_SECRET)} or' - f' {Argument.to_flag_name(Arguments.ACCESS_TOKEN)}, but not both') + raise Exception( + f"Please provide credentials via either {Argument.to_flag_name(Arguments.CLIENT_ID)} &" + f" {Argument.to_flag_name(Arguments.CLIENT_SECRET)} or" + f" {Argument.to_flag_name(Arguments.ACCESS_TOKEN)}, but not both" + ) if not has_access_token and not has_client_secret: - raise Exception(f'Please provide credentials via either {Argument.to_flag_name(Arguments.CLIENT_ID)} &' - f' {Argument.to_flag_name(Arguments.CLIENT_SECRET)} or' - f' {Argument.to_flag_name(Arguments.ACCESS_TOKEN)}.' - f' Alternatively, you may set the environment variables {CLIENT_ID_ENV} &' - f' {CLIENT_SECRET_ENV}.') + raise Exception( + f"Please provide credentials via either {Argument.to_flag_name(Arguments.CLIENT_ID)} &" + f" {Argument.to_flag_name(Arguments.CLIENT_SECRET)} or" + f" {Argument.to_flag_name(Arguments.ACCESS_TOKEN)}." + f" Alternatively, you may set the environment variables {CLIENT_ID_ENV} &" + f" {CLIENT_SECRET_ENV}." + ) # Authenticate accordingly if options.base_url: if options.host is not None or options.port is not None: - raise Exception(f'Please provide either {Argument.to_flag_name(Arguments.BASE_URL)} or' - f' {Argument.to_flag_name(Arguments.HOST)} &' - f' {Argument.to_flag_name(Arguments.PORT)}, but not both') - - polaris_management_url = f'{options.base_url}/api/management/v1' - polaris_catalog_url = f'{options.base_url}/api/catalog/v1' + raise Exception( + f"Please provide either {Argument.to_flag_name(Arguments.BASE_URL)} or" + f" {Argument.to_flag_name(Arguments.HOST)} &" + f" {Argument.to_flag_name(Arguments.PORT)}, but not both" + ) + + polaris_management_url = f"{options.base_url}/api/management/v1" + polaris_catalog_url = f"{options.base_url}/api/catalog/v1" else: - host = options.host or profile.get('host') or DEFAULT_HOSTNAME - port = options.port or profile.get('port') or DEFAULT_PORT - polaris_management_url = f'http://{host}:{port}/api/management/v1' - polaris_catalog_url = f'http://{host}:{port}/api/catalog/v1' + host = options.host or profile.get("host") or DEFAULT_HOSTNAME + port = options.port or profile.get("port") or DEFAULT_PORT + polaris_management_url = f"http://{host}:{port}/api/management/v1" + polaris_catalog_url = f"http://{host}:{port}/api/catalog/v1" config = Configuration(host=polaris_management_url) config.proxy = options.proxy @@ -152,7 +181,9 @@ def _get_client_builder(options): config.password = client_secret if not has_access_token and not PolarisCli.DIRECT_AUTHENTICATION_ENABLED: - token = PolarisCli._get_token(ApiClient(config), polaris_catalog_url, client_id, client_secret) + token = PolarisCli._get_token( + ApiClient(config), polaris_catalog_url, client_id, client_secret + ) config.username = None config.password = None config.access_token = token @@ -160,5 +191,5 @@ def _get_client_builder(options): return lambda: ApiClient(config) -if __name__ == '__main__': +if __name__ == "__main__": PolarisCli.execute()