Repository: great-expectations/great_expectations (python)

Issue:
Ephemeral Data Context Failing with permission denied on file
We are migrating from GX0.18 to GX 1.x and even though we are using Ephemeral Data Context and we are not using analytics, we are still getting below error:

Permission denied: '/root/.great_expectations/great_expectations.conf
The error is coming as part of the self._get_oss_id() inside _init_analytics even though we have analytics usage set as False in the data context and even though we are using ephemeral context.
This is happening with Databricks DBR15.4 on a streaming load, inside a forEachBatch.
Do you have any idea?
Why is GX checking for the file if we are using ephemeral and also turning analytics off?

https://discourse.greatexpectations.io/t/ephemeral-data-context-failing-with-permission-denied-on-file/2235

Pull request:
[BUGFIX] Disable init analytics as it was not really getting entirely disabled when specified on data context config
It seems init_analytics is being executed even when people is disabling analytics. 
Inside this execution, there seems to be a method self._get_oss_id()  searching for a root location `cls._ROOT_CONF_FILE.exists()` which is producing a permission denied error when used inside forEachBatch streaming on Databricks DBR15.4.

This PR proposes a fix to avoid invoking the init_analytics method if people disable it via config. 

If there is other suggestion for a workaround, while this PR is not release, please let me know as this is a bit critical for us.

You can find more context below:

- Closes #11275 GitHub Issue: https://github.com/great-expectations/great_expectations/issues/11275
- Discourse topic: https://discourse.greatexpectations.io/t/ephemeral-data-context-failing-with-permission-denied-on-file/2235


Patch:
diff --git a/great_expectations/data_context/data_context/abstract_data_context.py b/great_expectations/data_context/data_context/abstract_data_context.py
index cbde2280a..78855da04 100644
--- a/great_expectations/data_context/data_context/abstract_data_context.py
+++ b/great_expectations/data_context/data_context/abstract_data_context.py
@@ -249,6 +249,7 @@ class AbstractDataContext(ConfigPeer, ABC):
         self._init_data_source_manager()
 
         self._attach_fluent_config_datasources_and_build_data_connectors(self.fluent_config)
+
         self._init_analytics()
         submit_event(event=DataContextInitializedEvent())
 
@@ -272,15 +273,17 @@ class AbstractDataContext(ConfigPeer, ABC):
         )
 
     def _init_analytics(self) -> None:
-        init_analytics(
-            enable=self._determine_analytics_enabled(),
-            user_id=None,
-            data_context_id=self._data_context_id,
-            organization_id=None,
-            oss_id=self._get_oss_id(),
-            mode=self.mode,
-            user_agent_str=self._user_agent_str,
-        )
+        analytics_enabled = self._determine_analytics_enabled()
+        if analytics_enabled:
+            init_analytics(
+                enable=analytics_enabled,
+                user_id=None,
+                data_context_id=self._data_context_id,
+                organization_id=None,
+                oss_id=self._get_oss_id(),
+                mode=self.mode,
+                user_agent_str=self._user_agent_str,
+            )
 
     def _determine_analytics_enabled(self) -> bool:
         """
@@ -2015,13 +2018,13 @@ class AbstractDataContext(ConfigPeer, ABC):
         """  # noqa: E501 # FIXME CoP
         config = configparser.ConfigParser()
 
-        if not cls._ROOT_CONF_FILE.exists():
-            success = cls._scaffold_root_conf()
-            if not success:
-                return None
-            return cls._set_oss_id(config)
-
         try:
+            if not cls._ROOT_CONF_FILE.exists():
+                success = cls._scaffold_root_conf()
+                if not success:
+                    return None
+                return cls._set_oss_id(config)
+
             config.read(cls._ROOT_CONF_FILE)
         except OSError as e:
             logger.info(f"Something went wrong when trying to read from the user's conf file: {e}")
diff --git a/great_expectations/data_context/data_context/cloud_data_context.py b/great_expectations/data_context/data_context/cloud_data_context.py
index ed5268cc1..1d20ad4bc 100644
--- a/great_expectations/data_context/data_context/cloud_data_context.py
+++ b/great_expectations/data_context/data_context/cloud_data_context.py
@@ -144,16 +144,18 @@ class CloudDataContext(SerializableDataContext):
     @override
     def _init_analytics(self) -> None:
         organization_id = self.ge_cloud_config.organization_id
-        init_analytics(
-            enable=self._determine_analytics_enabled(),
-            user_id=self._get_cloud_user_id(),
-            data_context_id=self._data_context_id,
-            organization_id=uuid.UUID(organization_id) if organization_id else None,
-            oss_id=self._get_oss_id(),
-            cloud_mode=True,
-            mode=self.mode,
-            user_agent_str=self._user_agent_str,
-        )
+        analytics_enabled = self._determine_analytics_enabled()
+        if analytics_enabled:
+            init_analytics(
+                enable=analytics_enabled,
+                user_id=self._get_cloud_user_id(),
+                data_context_id=self._data_context_id,
+                organization_id=uuid.UUID(organization_id) if organization_id else None,
+                oss_id=self._get_oss_id(),
+                cloud_mode=True,
+                mode=self.mode,
+                user_agent_str=self._user_agent_str,
+            )
 
     def _get_cloud_user_id(self) -> uuid.UUID | None:
         if not ENV_CONFIG.gx_analytics_enabled:


Test patch:
diff --git a/tests/analytics/test_analytics.py b/tests/analytics/test_analytics.py
index 76bb908e9..4ed264420 100644
--- a/tests/analytics/test_analytics.py
+++ b/tests/analytics/test_analytics.py
@@ -209,7 +209,7 @@ def test_cloud_context_init(
     ],
 )
 @pytest.mark.unit
-def test_analytics_enabled_on_load(
+def test_analytics_enabled_on_load_unless_disabled(
     environment_variable: bool,
     constructor_variable: Optional[bool],
     expected_value: bool,
@@ -231,15 +231,18 @@ def test_analytics_enabled_on_load(
             user_agent_str=user_agent_str,
         )
 
-    mock_init.assert_called_with(
-        enable=expected_value,
-        data_context_id=mock.ANY,
-        organization_id=mock.ANY,
-        oss_id=mock.ANY,
-        user_id=mock.ANY,
-        user_agent_str=user_agent_str,
-        mode="ephemeral",
-    )
+    if expected_value is not False:
+        mock_init.assert_called_with(
+            enable=expected_value,
+            data_context_id=mock.ANY,
+            organization_id=mock.ANY,
+            oss_id=mock.ANY,
+            user_id=mock.ANY,
+            user_agent_str=user_agent_str,
+            mode="ephemeral",
+        )
+    else:
+        mock_init.assert_not_called()
 
 
 @pytest.mark.unit
@@ -298,15 +301,16 @@ def test_analytics_enabled_after_setting_explicitly(
         context.enable_analytics(enable_analytics)
 
     assert context.config.analytics_enabled == enable_analytics
-    mock_init.assert_called_with(
-        enable=enable_analytics,
-        data_context_id=mock.ANY,
-        organization_id=mock.ANY,
-        oss_id=mock.ANY,
-        user_id=mock.ANY,
-        user_agent_str=mock.ANY,
-        mode="ephemeral",
-    )
+    if enable_analytics is True:
+        mock_init.assert_called_with(
+            enable=enable_analytics,
+            data_context_id=mock.ANY,
+            organization_id=mock.ANY,
+            oss_id=mock.ANY,
+            user_id=mock.ANY,
+            user_agent_str=mock.ANY,
+            mode="ephemeral",
+        )
 
 
 @pytest.mark.parametrize("initial_user_agent_str", [None, "old user agent string"])