diff --git a/.gitignore b/.gitignore index ba659e586f..94b99a9b18 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ htmlcov pyiceberg/avro/decoder_fast.c pyiceberg/avro/*.html pyiceberg/avro/*.so + +# Generated version file +pyiceberg/_version.py diff --git a/build-module.py b/build-module.py index d91375e8e6..a7f89fb19f 100644 --- a/build-module.py +++ b/build-module.py @@ -22,6 +22,19 @@ allowed_to_fail = os.environ.get("CIBUILDWHEEL", "0") != "1" +def generate_version_file() -> None: + """Generate the _version.py file using setuptools_scm.""" + try: + from setuptools_scm import get_version # type: ignore[import-not-found] + + version = get_version(root=".", relative_to=__file__) + print(f"Generated version: {version}") + except Exception as e: + if not allowed_to_fail: + raise + print(f"Warning: Could not generate version file: {e}") + + def build_cython_extensions() -> None: import Cython.Compiler.Options from Cython.Build import build_ext, cythonize @@ -65,6 +78,7 @@ def build_cython_extensions() -> None: try: + generate_version_file() build_cython_extensions() except Exception: if not allowed_to_fail: diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index 913dc85bea..35bdd70316 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -74,6 +74,7 @@ ) from pyiceberg.typedef import EMPTY_DICT, UTF8, IcebergBaseModel, Identifier, Properties from pyiceberg.types import transform_dict_value_to_str +from pyiceberg.utils.build_info import git_commit_short_id from pyiceberg.utils.deprecated import deprecation_message from pyiceberg.utils.properties import get_first_property_value, get_header_properties, property_as_bool @@ -484,6 +485,8 @@ def _config_headers(self, session: Session) -> None: session.headers.update(header_properties) session.headers["Content-type"] = "application/json" session.headers["User-Agent"] = f"PyIceberg/{__version__}" + session.headers["X-Client-Version"] = __version__ + session.headers["X-Client-Git-Commit-Short"] = git_commit_short_id() session.headers.setdefault("X-Iceberg-Access-Delegation", ACCESS_DELEGATION_DEFAULT) def _create_table( diff --git a/pyiceberg/utils/build_info.py b/pyiceberg/utils/build_info.py new file mode 100644 index 0000000000..c9e2cba72e --- /dev/null +++ b/pyiceberg/utils/build_info.py @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Utilities for build information.""" + +import re +from functools import lru_cache + + +@lru_cache(maxsize=1) +def git_commit_short_id() -> str: + """Extract the short git commit ID from the version string.""" + try: + from pyiceberg._version import __version__ + + match = re.search(r"\+g([0-9a-f]{7})", __version__) + if match: + return match.group(1) + + return "unknown" + except (ImportError, AttributeError): + return "unknown" diff --git a/pyproject.toml b/pyproject.toml index c0dc2514f2..8233792101 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -295,13 +295,38 @@ ignore_missing_imports = true pyiceberg = "pyiceberg.cli.console:run" [build-system] -requires = ["poetry-core>=1.0.0", "wheel", "Cython>=3.0.0", "setuptools"] +requires = ["poetry-core>=1.0.0", "wheel", "Cython>=3.0.0", "setuptools", "setuptools-scm>=8.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.build] generate-setup-file = false script = "build-module.py" +[tool.setuptools_scm] +write_to = "pyiceberg/_version.py" +write_to_template = """# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# Generated by setuptools_scm + +__version__ = "{version}" +__version_tuple__ = {version_tuple} +""" +git_describe_command = "git describe --dirty --tags --long --match '*[0-9]*'" + [tool.poetry.extras] pyarrow = ["pyarrow", "pyiceberg-core"] pandas = ["pandas", "pyarrow"] @@ -535,5 +560,9 @@ ignore_missing_imports = true module = "pyroaring.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "pyiceberg._version" +ignore_missing_imports = true + [tool.coverage.run] source = ['pyiceberg/'] diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 5aee65d8b5..cc30fb8b2c 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -368,6 +368,27 @@ def test_config_sets_headers(requests_mock: Mocker) -> None: ) +def test_client_version_headers(requests_mock: Mocker) -> None: + import re + + from pyiceberg import __version__ + + requests_mock.get( + f"{TEST_URI}v1/config", + json={"defaults": {}, "overrides": {}}, + status_code=200, + ) + + catalog = RestCatalog("rest", uri=TEST_URI, warehouse="s3://some-bucket") + + assert catalog._session.headers.get("X-Client-Version") == __version__ + assert "X-Client-Git-Commit-Short" in catalog._session.headers + git_commit = catalog._session.headers.get("X-Client-Git-Commit-Short") + assert git_commit is not None + assert isinstance(git_commit, str) + assert re.match(r"^[0-9a-f]{7}$", git_commit), f"Expected 7-char hex git hash, got: {git_commit}" + + @pytest.mark.filterwarnings( "ignore:Deprecated in 0.8.0, will be removed in 1.0.0. Iceberg REST client is missing the OAuth2 server URI:DeprecationWarning" )