Skip to content

Commit

Permalink
[MAINTENANCE] Add check to CloudDataContext to ensure using latest …
Browse files Browse the repository at this point in the history
…PyPI version (#7753)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
cdkini and github-actions[bot] committed Apr 28, 2023
1 parent 8d8c5f9 commit 639b656
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
91 changes: 91 additions & 0 deletions great_expectations/data_context/_version_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import annotations

import json
import logging
import sys

import requests
from packaging import version
from typing_extensions import TypedDict

logger = logging.getLogger(__name__)


class _PyPIPackageInfo(TypedDict):
version: str


class _PyPIPackageData(TypedDict):
info: _PyPIPackageInfo


# Should only run in prod and in specific tests
# This flag let's us conditionally turn on the feature
_ENABLE_VERSION_CHECK_IN_TESTS = False


class _VersionChecker:

_BASE_PYPI_URL = "https://pypi.org/pypi"
_PYPI_GX_ENDPOINT = f"{_BASE_PYPI_URL}/great_expectations/json"

def __init__(self, user_version: str) -> None:
self._user_version = version.Version(user_version)

def check_if_using_latest_gx(self) -> bool:
if self._running_non_version_check_tests():
return True

pypi_version = self._get_latest_version_from_pypi()
if not pypi_version:
logger.debug("Could not compare with latest PyPI version; skipping check.")
return True

if self._is_using_outdated_release(pypi_version):
self._warn_user(pypi_version)
return False
return True

def _running_non_version_check_tests(self) -> bool:
return "pytest" in sys.modules and _ENABLE_VERSION_CHECK_IN_TESTS

def _get_latest_version_from_pypi(self) -> version.Version | None:
response_json: _PyPIPackageData | None = None
try:
response = requests.get(self._PYPI_GX_ENDPOINT)
response.raise_for_status()
response_json = response.json()
except json.JSONDecodeError as jsonError:
logger.debug(f"Failed to parse PyPI API response into JSON: {jsonError}")
except requests.HTTPError as http_err:
logger.debug(
f"An HTTP error occurred when trying to hit PyPI API: {http_err}"
)
except requests.Timeout as timeout_exc:
logger.debug(
f"Failed to hit the PyPI API due a timeout error: {timeout_exc}"
)

if not response_json:
return None

# Structure should be guaranteed but let's be defensive in case PyPI changes.
info = response_json.get("info", {})
pkg_version = info.get("version")
if not pkg_version:
logger.debug(
"Successfully hit PyPI API but payload structure is not as expected."
)
return None

return version.Version(pkg_version)

def _is_using_outdated_release(self, pypi_version: version.Version) -> bool:
return pypi_version > self._user_version

def _warn_user(self, pypi_version: version.Version) -> None:
logger.warning(
f"You are using great_expectations version {self._user_version}; "
f"however, version {pypi_version} is available.\nYou should consider "
"upgrading via `pip install great_expectations --upgrade`\n."
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
_ConfigurationProvider,
)
from great_expectations.core.serializer import JsonConfigSerializer
from great_expectations.data_context._version_checker import _VersionChecker
from great_expectations.data_context.cloud_constants import (
CLOUD_DEFAULT_BASE_URL,
GXCloudEnvironmentVariable,
Expand Down Expand Up @@ -104,6 +105,7 @@ def __init__(
ge_cloud_organization_id=ge_cloud_organization_id,
)

self._check_if_latest_version()
self._cloud_config = self.get_cloud_config(
cloud_base_url=cloud_base_url,
cloud_access_token=cloud_access_token,
Expand All @@ -119,6 +121,10 @@ def __init__(
runtime_environment=runtime_environment,
)

def _check_if_latest_version(self) -> None:
checker = _VersionChecker(__version__)
checker.check_if_using_latest_gx()

def _init_project_config(
self, project_config: Optional[Union[DataContextConfig, Mapping]]
) -> DataContextConfig:
Expand Down
52 changes: 52 additions & 0 deletions tests/data_context/cloud_data_context/test_version_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest
import responses

import great_expectations.data_context._version_checker as vc
from great_expectations.data_context._version_checker import _VersionChecker

# Set to some arbitrary value so tests will continue to work regardless of GX's actual version
_MOCK_PYPI_VERSION = "0.16.8"


@pytest.fixture
def enable_pypi_version_check():
vc._ENABLE_VERSION_CHECK_IN_TESTS = False
yield
vc._ENABLE_VERSION_CHECK_IN_TESTS = True


@pytest.mark.parametrize(
"version,expected,status",
[
pytest.param("0.15.0", False, 200, id="outdated"),
pytest.param(_MOCK_PYPI_VERSION, True, 200, id="up-to-date"),
# packaging.version should take care of dirty hashes but worth checking against
pytest.param(
f"{_MOCK_PYPI_VERSION}+59.g1ff4de04d.dirty",
True,
200,
id="up-to-date local dev",
),
# If we error, we shouldn't raise a warning to the user
pytest.param(_MOCK_PYPI_VERSION, True, 400, id="bad request"),
],
)
@pytest.mark.unit
@responses.activate
def test_check_if_using_latest_gx(
enable_pypi_version_check, version: str, expected: bool, status: int, caplog
):
pypi_payload = {"info": {"version": _MOCK_PYPI_VERSION}}
responses.add(
responses.GET,
_VersionChecker._PYPI_GX_ENDPOINT,
json=pypi_payload,
status=status,
)

checker = _VersionChecker(version)
actual = checker.check_if_using_latest_gx()

assert actual is expected
if not actual:
assert "upgrade" in caplog.text

0 comments on commit 639b656

Please sign in to comment.