1# Copyright 2023 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Shared helper functions for verifying versions of installed modules."""
16
17import sys
18from typing import Any
19
20import packaging.version
21
22from google.cloud.bigquery import exceptions
23
24
25_MIN_PYARROW_VERSION = packaging.version.Version("3.0.0")
26_MIN_BQ_STORAGE_VERSION = packaging.version.Version("2.0.0")
27_BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION = packaging.version.Version("2.6.0")
28_MIN_PANDAS_VERSION = packaging.version.Version("1.1.0")
29
30_MIN_PANDAS_VERSION_RANGE = packaging.version.Version("1.5.0")
31_MIN_PYARROW_VERSION_RANGE = packaging.version.Version("10.0.1")
32
33
34class PyarrowVersions:
35 """Version comparisons for pyarrow package."""
36
37 def __init__(self):
38 self._installed_version = None
39
40 @property
41 def installed_version(self) -> packaging.version.Version:
42 """Return the parsed version of pyarrow."""
43 if self._installed_version is None:
44 import pyarrow # type: ignore
45
46 self._installed_version = packaging.version.parse(
47 # Use 0.0.0, since it is earlier than any released version.
48 # Legacy versions also have the same property, but
49 # creating a LegacyVersion has been deprecated.
50 # https://github.com/pypa/packaging/issues/321
51 getattr(pyarrow, "__version__", "0.0.0")
52 )
53
54 return self._installed_version
55
56 @property
57 def use_compliant_nested_type(self) -> bool:
58 return self.installed_version.major >= 4
59
60 def try_import(self, raise_if_error: bool = False) -> Any:
61 """Verifies that a recent enough version of pyarrow extra is installed.
62
63 The function assumes that pyarrow extra is installed, and should thus
64 be used in places where this assumption holds.
65
66 Because `pip` can install an outdated version of this extra despite
67 the constraints in `setup.py`, the calling code can use this helper
68 to verify the version compatibility at runtime.
69
70 Returns:
71 The ``pyarrow`` module or ``None``.
72
73 Raises:
74 exceptions.LegacyPyarrowError:
75 If the pyarrow package is outdated and ``raise_if_error`` is
76 ``True``.
77 """
78 try:
79 import pyarrow
80 except ImportError as exc:
81 if raise_if_error:
82 raise exceptions.LegacyPyarrowError(
83 "pyarrow package not found. Install pyarrow version >="
84 f" {_MIN_PYARROW_VERSION}."
85 ) from exc
86 return None
87
88 if self.installed_version < _MIN_PYARROW_VERSION:
89 if raise_if_error:
90 msg = (
91 "Dependency pyarrow is outdated, please upgrade"
92 f" it to version >= {_MIN_PYARROW_VERSION}"
93 f" (version found: {self.installed_version})."
94 )
95 raise exceptions.LegacyPyarrowError(msg)
96 return None
97
98 return pyarrow
99
100
101PYARROW_VERSIONS = PyarrowVersions()
102
103
104class BQStorageVersions:
105 """Version comparisons for google-cloud-bigqueyr-storage package."""
106
107 def __init__(self):
108 self._installed_version = None
109
110 @property
111 def installed_version(self) -> packaging.version.Version:
112 """Return the parsed version of google-cloud-bigquery-storage."""
113 if self._installed_version is None:
114 from google.cloud import bigquery_storage
115
116 self._installed_version = packaging.version.parse(
117 # Use 0.0.0, since it is earlier than any released version.
118 # Legacy versions also have the same property, but
119 # creating a LegacyVersion has been deprecated.
120 # https://github.com/pypa/packaging/issues/321
121 getattr(bigquery_storage, "__version__", "0.0.0")
122 )
123
124 return self._installed_version # type: ignore
125
126 @property
127 def is_read_session_optional(self) -> bool:
128 """True if read_session is optional to rows().
129
130 See: https://github.com/googleapis/python-bigquery-storage/pull/228
131 """
132 return self.installed_version >= _BQ_STORAGE_OPTIONAL_READ_SESSION_VERSION
133
134 def try_import(self, raise_if_error: bool = False) -> Any:
135 """Tries to import the bigquery_storage module, and returns results
136 accordingly. It also verifies the module version is recent enough.
137
138 If the import succeeds, returns the ``bigquery_storage`` module.
139
140 If the import fails,
141 returns ``None`` when ``raise_if_error == False``,
142 raises Error when ``raise_if_error == True``.
143
144 Returns:
145 The ``bigquery_storage`` module or ``None``.
146
147 Raises:
148 exceptions.BigQueryStorageNotFoundError:
149 If google-cloud-bigquery-storage is not installed
150 exceptions.LegacyBigQueryStorageError:
151 If google-cloud-bigquery-storage package is outdated
152 """
153 try:
154 from google.cloud import bigquery_storage # type: ignore
155 except ImportError:
156 if raise_if_error:
157 msg = (
158 "Package google-cloud-bigquery-storage not found. "
159 "Install google-cloud-bigquery-storage version >= "
160 f"{_MIN_BQ_STORAGE_VERSION}."
161 )
162 raise exceptions.BigQueryStorageNotFoundError(msg)
163 return None
164
165 if self.installed_version < _MIN_BQ_STORAGE_VERSION:
166 if raise_if_error:
167 msg = (
168 "Dependency google-cloud-bigquery-storage is outdated, "
169 f"please upgrade it to version >= {_MIN_BQ_STORAGE_VERSION} "
170 f"(version found: {self.installed_version})."
171 )
172 raise exceptions.LegacyBigQueryStorageError(msg)
173 return None
174
175 return bigquery_storage
176
177
178BQ_STORAGE_VERSIONS = BQStorageVersions()
179
180
181class PandasVersions:
182 """Version comparisons for pandas package."""
183
184 def __init__(self):
185 self._installed_version = None
186
187 @property
188 def installed_version(self) -> packaging.version.Version:
189 """Return the parsed version of pandas"""
190 if self._installed_version is None:
191 import pandas # type: ignore
192
193 self._installed_version = packaging.version.parse(
194 # Use 0.0.0, since it is earlier than any released version.
195 # Legacy versions also have the same property, but
196 # creating a LegacyVersion has been deprecated.
197 # https://github.com/pypa/packaging/issues/321
198 getattr(pandas, "__version__", "0.0.0")
199 )
200
201 return self._installed_version
202
203 def try_import(self, raise_if_error: bool = False) -> Any:
204 """Verify that a recent enough version of pandas extra is installed.
205 The function assumes that pandas extra is installed, and should thus
206 be used in places where this assumption holds.
207 Because `pip` can install an outdated version of this extra despite
208 the constraints in `setup.py`, the calling code can use this helper
209 to verify the version compatibility at runtime.
210 Returns:
211 The ``pandas`` module or ``None``.
212 Raises:
213 exceptions.LegacyPandasError:
214 If the pandas package is outdated and ``raise_if_error`` is
215 ``True``.
216 """
217 try:
218 import pandas
219 except ImportError as exc:
220 if raise_if_error:
221 raise exceptions.LegacyPandasError(
222 "pandas package not found. Install pandas version >="
223 f" {_MIN_PANDAS_VERSION}"
224 ) from exc
225 return None
226
227 if self.installed_version < _MIN_PANDAS_VERSION:
228 if raise_if_error:
229 msg = (
230 "Dependency pandas is outdated, please upgrade"
231 f" it to version >= {_MIN_PANDAS_VERSION}"
232 f" (version found: {self.installed_version})."
233 )
234 raise exceptions.LegacyPandasError(msg)
235 return None
236
237 return pandas
238
239
240PANDAS_VERSIONS = PandasVersions()
241
242# Since RANGE support in pandas requires specific versions
243# of both pyarrow and pandas, we make this a separate
244# constant instead of as a property of PANDAS_VERSIONS
245# or PYARROW_VERSIONS.
246SUPPORTS_RANGE_PYARROW = (
247 PANDAS_VERSIONS.try_import() is not None
248 and PANDAS_VERSIONS.installed_version >= _MIN_PANDAS_VERSION_RANGE
249 and PYARROW_VERSIONS.try_import() is not None
250 and PYARROW_VERSIONS.installed_version >= _MIN_PYARROW_VERSION_RANGE
251)
252
253
254def extract_runtime_version():
255 # Retrieve the version information
256 version_info = sys.version_info
257
258 # Extract the major, minor, and micro components
259 major = version_info.major
260 minor = version_info.minor
261 micro = version_info.micro
262
263 # Display the version number in a clear format
264 return major, minor, micro