This is an automated email from the ASF dual-hosted git repository. honahx pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push: new 3b06a8034 Update Makefile for python client with auto setup (#1995) 3b06a8034 is described below commit 3b06a803458aa61ad691d2edd132157dafbfb2d3 Author: Yong Zheng <yongzheng0...@gmail.com> AuthorDate: Thu Jul 10 15:44:45 2025 -0500 Update Makefile for python client with auto setup (#1995) Automate python client setup and use a virtual env instead to avoid change an end-users' OS python --- .github/workflows/python-client.yml | 18 +-- client/python/.pre-commit-config.yaml | 7 +- client/python/Makefile | 82 +++++++++++++- client/python/README.md | 14 +-- client/python/cli/constants.py | 205 +++++++++++++++++++--------------- client/python/cli/polaris_cli.py | 8 +- client/python/pyproject.toml | 2 +- 7 files changed, 212 insertions(+), 124 deletions(-) diff --git a/.github/workflows/python-client.yml b/.github/workflows/python-client.yml index 37ead61ed..7586e3b18 100644 --- a/.github/workflows/python-client.yml +++ b/.github/workflows/python-client.yml @@ -58,15 +58,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install Poetry - run: | - pip install --user --upgrade -r regtests/requirements.txt - - # TODO: add cache for poetry dependencies once we have poetry.lock in the repo - - name: Install dependencies - working-directory: client/python - run: poetry install --all-extras - - name: Lint working-directory: client/python run: | @@ -75,15 +66,14 @@ jobs: - name: Generated Client Tests working-directory: client/python run: | - export SCRIPT_DIR="non-existing-mock-directory" - poetry run pytest test/ + make test-client - name: Image build run: | ./gradlew \ - :polaris-server:assemble \ - :polaris-server:quarkusAppPartsBuild --rerun \ - -Dquarkus.container-image.build=true + :polaris-server:assemble \ + :polaris-server:quarkusAppPartsBuild --rerun \ + -Dquarkus.container-image.build=true - name: Integration Tests working-directory: client/python diff --git a/client/python/.pre-commit-config.yaml b/client/python/.pre-commit-config.yaml index 84b1ab95e..5ffb2bfc6 100644 --- a/client/python/.pre-commit-config.yaml +++ b/client/python/.pre-commit-config.yaml @@ -23,10 +23,12 @@ repos: - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.1 hooks: - - id: ruff + # Run the linter. + - id: ruff-check args: [ --fix, --exit-non-zero-on-fix ] + # Run the formatter. - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.16.0 @@ -34,3 +36,4 @@ repos: - id: mypy args: [--disallow-untyped-defs, --ignore-missing-imports, --install-types, --non-interactive] + files: 'integration_tests/.*\.py' diff --git a/client/python/Makefile b/client/python/Makefile index 7d65922ab..9d222fa39 100644 --- a/client/python/Makefile +++ b/client/python/Makefile @@ -15,10 +15,65 @@ # specific language governing permissions and limitations # under the License. -regenerate-client: +# .SILENT: + +# Configures the shell for recipes to use bash, enabling bash commands and ensuring +# that recipes exit on any command failure (including within pipes). +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +# Version information +VERSION ?= $(shell cat pyproject.toml | grep version | sed 's/version *= *"\(.*\)"/\1/') +BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%S%:z") +GIT_COMMIT := $(shell git rev-parse HEAD) +POETRY_VERSION := $(shell cat pyproject.toml | grep requires-poetry | sed 's/requires-poetry *= *"\(.*\)"/\1/') + +# Variables +VENV_DIR := .venv + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-30s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.PHONY: version +version: ## Print version information. + @echo "Apache Polaris version: ${VERSION}" + @echo "Build date: ${BUILD_DATE}" + @echo "Git commit: ${GIT_COMMIT}" + @echo "Poetry version: ${POETRY_VERSION}" + +# Target to create the virtual environment directory +$(VENV_DIR): + @echo "Setting up Python virtual environment at $(VENV_DIR)..." + python3 -m venv $(VENV_DIR) + @echo "Virtual environment created." + +.PHONY: setup-env +setup-env: $(VENV_DIR) install-poetry-deps + +.PHONY: install-poetry-deps +install-poetry-deps: + @echo "Installing Poetry and project dependencies into $(VENV_DIR)..." + # Ensure pip is up-to-date within the venv + $(VENV_DIR)/bin/pip install --upgrade pip + # Install poetry if not already present + @if [ ! -f "$(VENV_DIR)/bin/poetry" ]; then \ + $(VENV_DIR)/bin/pip install --upgrade "poetry${POETRY_VERSION}"; \ + fi + # Install needed dependencies using poetry + $(VENV_DIR)/bin/poetry install --all-extras + @echo "Poetry and dependencies installed." + +.PHONY: regenerate-client +regenerate-client: ## Regenerate the client code ../templates/regenerate.sh -test-integration: +.PHONY: test-client +test-client: setup-env ## Run client tests + SCRIPT_DIR="non-existing-mock-directory" $(VENV_DIR)/bin/poetry run pytest test/ + +.PHONY: test-integration +test-integration: setup-env ## Run integration tests docker compose -f docker-compose.yml kill docker compose -f docker-compose.yml rm -f docker compose -f docker-compose.yml up -d @@ -28,8 +83,25 @@ test-integration: echo "Still waiting for HTTP 200 from /q/health..."; \ done @echo "Polaris is healthy. Starting integration tests..." - poetry run pytest integration_tests/ ${PYTEST_ARGS} + $(VENV_DIR)/bin/poetry run pytest integration_tests/ ${PYTEST_ARGS} + +.PHONY: lint +lint: setup-env ## Run linting checks + $(VENV_DIR)/bin/poetry run pre-commit run --files integration_tests/* cli/* +.PHONY: clean-venv +clean-venv: + @echo "Attempting to remove virtual environment directory: $(VENV_DIR)..." + # SAFETY CHECK: Ensure VENV_DIR is not empty and exists before attempting to remove + @if [ -n "$(VENV_DIR)" ] && [ -d "$(VENV_DIR)" ]; then \ + rm -rf "$(VENV_DIR)"; \ + echo "Virtual environment removed."; \ + else \ + echo "Virtual environment directory '$(VENV_DIR)' not found or VENV_DIR is empty. No action taken."; \ + fi -lint: - poetry run pre-commit run --files integration_tests/* +.PHONY: clean +clean: clean-venv ## Cleanup + @echo "Cleaning up Python cache files..." + find . -type f -name "*.pyc" -delete + find . -type d -name "__pycache__" -delete diff --git a/client/python/README.md b/client/python/README.md index cec952681..4489452a1 100644 --- a/client/python/README.md +++ b/client/python/README.md @@ -6,9 +6,9 @@ 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 @@ -24,17 +24,13 @@ The Apache Polaris Python package provides a client for interacting with the Apa ### Prerequisites - Python 3.9 or later -- poetry >= 2.0 +- poetry >= 2.1 ### Installation -First we need to generate the OpenAPI client code from the OpenAPI specification. +First we need to generate the OpenAPI client code from the OpenAPI specification. ``` make regenerate-client ``` -Install the project with test dependencies: -``` -poetry install --all-extras -``` ### Auto-formatting and Linting ``` @@ -44,4 +40,4 @@ make lint ### Running Integration Tests ``` make test-integration -``` \ No newline at end of file +``` diff --git a/client/python/cli/constants.py b/client/python/cli/constants.py index f82ff2caa..93b36d998 100644 --- a/client/python/cli/constants.py +++ b/client/python/cli/constants.py @@ -53,8 +53,8 @@ class CatalogConnectionType(Enum): Represents a ConnectionType for an EXTERNAL catalog -- see ConnectionConfigInfo in the spec """ - HADOOP = 'hadoop' - ICEBERG = 'iceberg-rest' + HADOOP = "hadoop" + ICEBERG = "iceberg-rest" class AuthenticationType(Enum): @@ -62,9 +62,9 @@ class AuthenticationType(Enum): Represents a AuthenticationType for an EXTERNAL catalog -- see AuthenticationParameters in the spec """ - OAUTH = 'oauth' - BEARER = 'bearer' - SIGV4 = 'sigv4' + OAUTH = "oauth" + BEARER = "bearer" + SIGV4 = "sigv4" class ServiceIdentityType(Enum): @@ -72,7 +72,7 @@ class ServiceIdentityType(Enum): Represents a Service Identity Type for an EXTERNAL catalog -- see ServiceIdentityInfo in the spec """ - AWS_IAM = 'aws_iam' + AWS_IAM = "aws_iam" class Commands: @@ -129,57 +129,57 @@ 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' - HADOOP_WAREHOUSE = 'hadoop_warehouse' - ICEBERG_REMOTE_CATALOG_NAME = 'iceberg_remote_catalog_name' - CATALOG_CONNECTION_TYPE = 'catalog_connection_type' - CATALOG_AUTHENTICATION_TYPE = 'catalog_authentication_type' - CATALOG_SERVICE_IDENTITY_TYPE = 'catalog_service_identity_type' - CATALOG_SERVICE_IDENTITY_IAM_ARN = 'catalog_service_identity_iam_arn' - CATALOG_URI = 'catalog_uri' - CATALOG_TOKEN_URI = 'catalog_token_uri' - CATALOG_CLIENT_ID = 'catalog_client_id' - CATALOG_CLIENT_SECRET = 'catalog_client_secret' - CATALOG_CLIENT_SCOPE = 'catalog_client_scope' - CATALOG_BEARER_TOKEN = 'catalog_bearer_token' - CATALOG_ROLE_ARN = 'catalog_role_arn' - CATALOG_ROLE_SESSION_NAME = 'catalog_role_session_name' - CATALOG_EXTERNAL_ID = 'catalog_external_id' - CATALOG_SIGNING_REGION = 'catalog_signing_region' - CATALOG_SIGNING_NAME = 'catalog_signing_name' + 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" + HADOOP_WAREHOUSE = "hadoop_warehouse" + ICEBERG_REMOTE_CATALOG_NAME = "iceberg_remote_catalog_name" + CATALOG_CONNECTION_TYPE = "catalog_connection_type" + CATALOG_AUTHENTICATION_TYPE = "catalog_authentication_type" + CATALOG_SERVICE_IDENTITY_TYPE = "catalog_service_identity_type" + CATALOG_SERVICE_IDENTITY_IAM_ARN = "catalog_service_identity_iam_arn" + CATALOG_URI = "catalog_uri" + CATALOG_TOKEN_URI = "catalog_token_uri" + CATALOG_CLIENT_ID = "catalog_client_id" + CATALOG_CLIENT_SECRET = "catalog_client_secret" + CATALOG_CLIENT_SCOPE = "catalog_client_scope" + CATALOG_BEARER_TOKEN = "catalog_bearer_token" + CATALOG_ROLE_ARN = "catalog_role_arn" + CATALOG_ROLE_SESSION_NAME = "catalog_role_session_name" + CATALOG_EXTERNAL_ID = "catalog_external_id" + CATALOG_SIGNING_REGION = "catalog_signing_region" + CATALOG_SIGNING_NAME = "catalog_signing_name" class Hints: @@ -237,39 +237,64 @@ class Hints: DEFAULT_BASE_LOCATION = "A new default base location for the catalog" class External: - CATALOG_CONNECTION_TYPE = 'The type of external catalog in [ICEBERG, HADOOP].' - CATALOG_AUTHENTICATION_TYPE = 'The type of authentication in [OAUTH, BEARER, SIGV4]' - CATALOG_SERVICE_IDENTITY_TYPE = 'The type of service identity in [AWS_IAM]' - - CATALOG_SERVICE_IDENTITY_IAM_ARN = ('When using the AWS_IAM service identity type, this is the ARN ' - 'of the IAM user or IAM role Polaris uses to assume roles and ' - 'then access external resources.') - - CATALOG_URI = 'The URI of the external catalog' - HADOOP_WAREHOUSE = 'The warehouse to use when federating to a HADOOP catalog' - ICEBERG_REMOTE_CATALOG_NAME = 'The remote catalog name when federating to an Iceberg REST catalog' - - - CATALOG_TOKEN_URI = '(For authentication type OAUTH) Token server URI' - CATALOG_CLIENT_ID = '(For authentication type OAUTH) oauth client id' - CATALOG_CLIENT_SECRET = '(For authentication type OAUTH) oauth client secret (input-only)' - CATALOG_CLIENT_SCOPE = ('(For authentication type OAUTH) oauth scopes to specify when exchanging ' - 'for a short-lived access token. Multiple can be provided by specifying' - ' this option more than once') - - CATALOG_BEARER_TOKEN = '(For authentication type BEARER) Bearer token (input-only)' - - CATALOG_ROLE_ARN = ('(For authentication type SIGV4) The aws IAM role arn assumed by polaris ' - 'userArn when signing requests') - CATALOG_ROLE_SESSION_NAME = ('(For authentication type SIGV4) The role session name to be used ' - 'by the SigV4 protocol for signing requests') - CATALOG_EXTERNAL_ID = ('(For authentication type SIGV4) An optional external id used to establish ' - 'a trust relationship with AWS in the trust policy') - CATALOG_SIGNING_REGION = ('(For authentication type SIGV4) Region to be used by the SigV4 protocol ' - 'for signing requests') - CATALOG_SIGNING_NAME = ('(For authentication type SIGV4) The service name to be used by the SigV4 ' - 'protocol for signing requests, the default signing name is "execute-api" ' - 'is if not provided') + CATALOG_CONNECTION_TYPE = ( + "The type of external catalog in [ICEBERG, HADOOP]." + ) + CATALOG_AUTHENTICATION_TYPE = ( + "The type of authentication in [OAUTH, BEARER, SIGV4]" + ) + CATALOG_SERVICE_IDENTITY_TYPE = "The type of service identity in [AWS_IAM]" + + CATALOG_SERVICE_IDENTITY_IAM_ARN = ( + "When using the AWS_IAM service identity type, this is the ARN " + "of the IAM user or IAM role Polaris uses to assume roles and " + "then access external resources." + ) + + CATALOG_URI = "The URI of the external catalog" + HADOOP_WAREHOUSE = ( + "The warehouse to use when federating to a HADOOP catalog" + ) + ICEBERG_REMOTE_CATALOG_NAME = ( + "The remote catalog name when federating to an Iceberg REST catalog" + ) + + CATALOG_TOKEN_URI = "(For authentication type OAUTH) Token server URI" + CATALOG_CLIENT_ID = "(For authentication type OAUTH) oauth client id" + CATALOG_CLIENT_SECRET = ( + "(For authentication type OAUTH) oauth client secret (input-only)" + ) + CATALOG_CLIENT_SCOPE = ( + "(For authentication type OAUTH) oauth scopes to specify when exchanging " + "for a short-lived access token. Multiple can be provided by specifying" + " this option more than once" + ) + + CATALOG_BEARER_TOKEN = ( + "(For authentication type BEARER) Bearer token (input-only)" + ) + + CATALOG_ROLE_ARN = ( + "(For authentication type SIGV4) The aws IAM role arn assumed by polaris " + "userArn when signing requests" + ) + CATALOG_ROLE_SESSION_NAME = ( + "(For authentication type SIGV4) The role session name to be used " + "by the SigV4 protocol for signing requests" + ) + CATALOG_EXTERNAL_ID = ( + "(For authentication type SIGV4) An optional external id used to establish " + "a trust relationship with AWS in the trust policy" + ) + CATALOG_SIGNING_REGION = ( + "(For authentication type SIGV4) Region to be used by the SigV4 protocol " + "for signing requests" + ) + CATALOG_SIGNING_NAME = ( + "(For authentication type SIGV4) The service name to be used by the SigV4 " + 'protocol for signing requests, the default signing name is "execute-api" ' + "is if not provided" + ) class Principals: class Create: diff --git a/client/python/cli/polaris_cli.py b/client/python/cli/polaris_cli.py index 47e803865..83341ada4 100644 --- a/client/python/cli/polaris_cli.py +++ b/client/python/cli/polaris_cli.py @@ -158,9 +158,11 @@ class PolarisCli: # 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') + 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" diff --git a/client/python/pyproject.toml b/client/python/pyproject.toml index a5e20d97f..9740437bf 100644 --- a/client/python/pyproject.toml +++ b/client/python/pyproject.toml @@ -42,7 +42,7 @@ homepage = "https://polaris.apache.org/" repository = "https://github.com/apache/polaris/" [tool.poetry] -requires-poetry = ">=2.1" +requires-poetry = "==2.1.3" packages = [{ include = "polaris" }] [tool.poetry.group.test.dependencies]