We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
SQLAlchemy warning when not using SQL
**Describe the bug**
I'm using Azure Blob Storage as a backend and getting the following warning in the new version of ge 0.15.4. Don't think this warning showed up in versions prior to 0.15.

`WARNING:great_expectations.data_context.util:Something went wrong when trying to use SQLAlchemy to obfuscate URL: Could not parse rfc1738 URL from string 'BlobEndpoint=xxxxxx'`

**Expected behavior**
No warning of this is type should show when TupleAzureBlobStoreBackend backend is used.

**Environment (please complete the following information):**
 - System: Azure
 - Great Expectations Version: 0.15.4



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `great_expectations/data_context/util.py`
Content:
```
1 import copy
2 import inspect
3 import logging
4 import pathlib
5 import warnings
6 from typing import Any, Optional
7 from urllib.parse import urlparse
8 
9 import pyparsing as pp
10 
11 from great_expectations.alias_types import PathStr  # noqa: TCH001
12 from great_expectations.types import safe_deep_copy
13 from great_expectations.util import load_class, verify_dynamic_loading_support
14 
15 try:
16     import sqlalchemy as sa
17 except ImportError:
18     sa = None
19 
20 logger = logging.getLogger(__name__)
21 
22 
23 # TODO: Rename config to constructor_kwargs and config_defaults -> constructor_kwarg_default
24 # TODO: Improve error messages in this method. Since so much of our workflow is config-driven, this will be a *super* important part of DX.
25 def instantiate_class_from_config(config, runtime_environment, config_defaults=None):
26     """Build a GX class from configuration dictionaries."""
27 
28     if config_defaults is None:
29         config_defaults = {}
30 
31     config = copy.deepcopy(config)
32 
33     module_name = config.pop("module_name", None)
34     if module_name is None:
35         try:
36             module_name = config_defaults.pop("module_name")
37         except KeyError:
38             raise KeyError(
39                 "Neither config : {} nor config_defaults : {} contains a module_name key.".format(
40                     config,
41                     config_defaults,
42                 )
43             )
44     else:
45         # Pop the value without using it, to avoid sending an unwanted value to the config_class
46         config_defaults.pop("module_name", None)
47 
48     logger.debug(f"(instantiate_class_from_config) module_name -> {module_name}")
49     verify_dynamic_loading_support(module_name=module_name)
50 
51     class_name = config.pop("class_name", None)
52     if class_name is None:
53         logger.warning(
54             "Instantiating class from config without an explicit class_name is dangerous. Consider adding "
55             f"an explicit class_name for {config.get('name')}"
56         )
57         try:
58             class_name = config_defaults.pop("class_name")
59         except KeyError:
60             raise KeyError(
61                 "Neither config : {} nor config_defaults : {} contains a class_name key.".format(
62                     config,
63                     config_defaults,
64                 )
65             )
66     else:
67         # Pop the value without using it, to avoid sending an unwanted value to the config_class
68         config_defaults.pop("class_name", None)
69 
70     class_ = load_class(class_name=class_name, module_name=module_name)
71 
72     config_with_defaults = copy.deepcopy(config_defaults)
73     config_with_defaults.update(config)
74     if runtime_environment is not None:
75         # If there are additional kwargs available in the runtime_environment requested by a
76         # class to be instantiated, provide them
77         argspec = inspect.getfullargspec(class_.__init__)[0][1:]
78 
79         missing_args = set(argspec) - set(config_with_defaults.keys())
80         config_with_defaults.update(
81             {
82                 missing_arg: runtime_environment[missing_arg]
83                 for missing_arg in missing_args
84                 if missing_arg in runtime_environment
85             }
86         )
87         # Add the entire runtime_environment as well if it's requested
88         if "runtime_environment" in missing_args:
89             config_with_defaults.update({"runtime_environment": runtime_environment})
90 
91     try:
92         class_instance = class_(**config_with_defaults)
93     except TypeError as e:
94         raise TypeError(
95             "Couldn't instantiate class: {} with config: \n\t{}\n \n".format(
96                 class_name, format_dict_for_error_message(config_with_defaults)
97             )
98             + str(e)
99         )
100 
101     return class_instance
102 
103 
104 def format_dict_for_error_message(dict_):
105     # TODO : Tidy this up a bit. Indentation isn't fully consistent.
106 
107     return "\n\t".join("\t\t".join((str(key), str(dict_[key]))) for key in dict_)
108 
109 
110 def file_relative_path(
111     source_path: PathStr,
112     relative_path: PathStr,
113     strict: bool = True,
114 ) -> str:
115     """
116     This function is useful when one needs to load a file that is
117     relative to the position of the current file. (Such as when
118     you encode a configuration file path in source file and want
119     in runnable in any current working directory)
120 
121     It is meant to be used like the following:
122     file_relative_path(__file__, 'path/relative/to/file')
123 
124     This has been modified from Dagster's utils:
125     H/T https://github.com/dagster-io/dagster/blob/8a250e9619a49e8bff8e9aa7435df89c2d2ea039/python_modules/dagster/dagster/utils/__init__.py#L34
126     """
127     dir_path = pathlib.Path(source_path).parent
128     abs_path = dir_path.joinpath(relative_path).resolve(strict=strict)
129     return str(abs_path)
130 
131 
132 def parse_substitution_variable(substitution_variable: str) -> Optional[str]:
133     """
134     Parse and check whether the string contains a substitution variable of the case insensitive form ${SOME_VAR} or $SOME_VAR
135     Args:
136         substitution_variable: string to be parsed
137 
138     Returns:
139         string of variable name e.g. SOME_VAR or None if not parsable. If there are multiple substitution variables this currently returns the first e.g. $SOME_$TRING -> $SOME_
140     """
141     substitution_variable_name = pp.Word(pp.alphanums + "_").setResultsName(
142         "substitution_variable_name"
143     )
144     curly_brace_parser = "${" + substitution_variable_name + "}"
145     non_curly_brace_parser = "$" + substitution_variable_name
146     both_parser = curly_brace_parser | non_curly_brace_parser
147     try:
148         parsed_substitution_variable = both_parser.parseString(substitution_variable)
149         return parsed_substitution_variable.substitution_variable_name
150     except pp.ParseException:
151         return None
152 
153 
154 class PasswordMasker:
155     """
156     Used to mask passwords in Datasources. Does not mask sqlite urls.
157 
158     Example usage
159     masked_db_url = PasswordMasker.mask_db_url(url)
160     where url = "postgresql+psycopg2://username:password@host:65432/database"
161     and masked_url = "postgresql+psycopg2://username:***@host:65432/database"
162 
163     """
164 
165     MASKED_PASSWORD_STRING = "***"
166 
167     # values with the following keys will be processed with cls.mask_db_url:
168     URL_KEYS = {"connection_string", "url"}
169 
170     # values with these keys will be directly replaced with cls.MASKED_PASSWORD_STRING:
171     PASSWORD_KEYS = {"access_token", "password"}
172 
173     @classmethod
174     def mask_db_url(cls, url: str, use_urlparse: bool = False, **kwargs) -> str:
175         """
176         Mask password in database url.
177         Uses sqlalchemy engine parsing if sqlalchemy is installed, otherwise defaults to using urlparse from the stdlib which does not handle kwargs.
178         Args:
179             url: Database url e.g. "postgresql+psycopg2://username:password@host:65432/database"
180             use_urlparse: Skip trying to parse url with sqlalchemy and use urlparse
181             **kwargs: passed to create_engine()
182 
183         Returns:
184             url with password masked e.g. "postgresql+psycopg2://username:***@host:65432/database"
185         """
186         if sa is not None and use_urlparse is False:
187             try:
188                 engine = sa.create_engine(url, **kwargs)
189                 return engine.url.__repr__()
190             # Account for the edge case where we have SQLAlchemy in our env but haven't installed the appropriate dialect to match the input URL
191             except Exception as e:
192                 logger.warning(
193                     f"Something went wrong when trying to use SQLAlchemy to obfuscate URL: {e}"
194                 )
195         else:
196             warnings.warn(
197                 "SQLAlchemy is not installed, using urlparse to mask database url password which ignores **kwargs."
198             )
199         return cls._mask_db_url_no_sa(url=url)
200 
201     @classmethod
202     def _mask_db_url_no_sa(cls, url: str) -> str:
203         # oracle+cx_oracle does not parse well using urlparse, parse as oracle then swap back
204         replace_prefix = None
205         if url.startswith("oracle+cx_oracle"):
206             replace_prefix = {"original": "oracle+cx_oracle", "temporary": "oracle"}
207             url = url.replace(replace_prefix["original"], replace_prefix["temporary"])
208 
209         parsed_url = urlparse(url)
210 
211         # Do not parse sqlite
212         if parsed_url.scheme == "sqlite":
213             return url
214 
215         colon = ":" if parsed_url.port is not None else ""
216         masked_url = (
217             f"{parsed_url.scheme}://{parsed_url.username}:{cls.MASKED_PASSWORD_STRING}"
218             f"@{parsed_url.hostname}{colon}{parsed_url.port or ''}{parsed_url.path or ''}"
219         )
220 
221         if replace_prefix is not None:
222             masked_url = masked_url.replace(
223                 replace_prefix["temporary"], replace_prefix["original"]
224             )
225 
226         return masked_url
227 
228     @classmethod
229     def sanitize_config(cls, config: dict) -> dict:
230         """
231         Mask sensitive fields in a Dict.
232         """
233 
234         # be defensive, since it would be logical to expect this method works with DataContextConfig
235         if not isinstance(config, dict):
236             raise TypeError(
237                 "PasswordMasker.sanitize_config expects param `config` "
238                 + f"to be of type Dict, not of type {type(config)}"
239             )
240 
241         config_copy = safe_deep_copy(config)  # be immutable
242 
243         def recursive_cleaner_method(config: Any) -> None:
244             if isinstance(config, dict):
245                 for key, val in config.items():
246                     if not isinstance(val, str):
247                         recursive_cleaner_method(val)
248                     elif key in cls.URL_KEYS:
249                         config[key] = cls.mask_db_url(val)
250                     elif key in cls.PASSWORD_KEYS:
251                         config[key] = cls.MASKED_PASSWORD_STRING
252                     else:
253                         pass  # this string is not sensitive
254             elif isinstance(config, list):
255                 for val in config:
256                     recursive_cleaner_method(val)
257 
258         recursive_cleaner_method(config_copy)  # Perform anonymization in place
259 
260         return config_copy
261
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/great_expectations/data_context/util.py b/great_expectations/data_context/util.py
--- a/great_expectations/data_context/util.py
+++ b/great_expectations/data_context/util.py
@@ -1,7 +1,10 @@
+from __future__ import annotations
+
 import copy
 import inspect
 import logging
 import pathlib
+import re
 import warnings
 from typing import Any, Optional
 from urllib.parse import urlparse
@@ -9,6 +12,7 @@
 import pyparsing as pp
 
 from great_expectations.alias_types import PathStr  # noqa: TCH001
+from great_expectations.exceptions import StoreConfigurationError
 from great_expectations.types import safe_deep_copy
 from great_expectations.util import load_class, verify_dynamic_loading_support
 
@@ -183,7 +187,9 @@
         Returns:
             url with password masked e.g. "postgresql+psycopg2://username:***@host:65432/database"
         """
-        if sa is not None and use_urlparse is False:
+        if url.startswith("DefaultEndpointsProtocol"):
+            return cls._obfuscate_azure_blobstore_connection_string(url)
+        elif sa is not None and use_urlparse is False:
             try:
                 engine = sa.create_engine(url, **kwargs)
                 return engine.url.__repr__()
@@ -198,6 +204,25 @@
             )
         return cls._mask_db_url_no_sa(url=url)
 
+    @classmethod
+    def _obfuscate_azure_blobstore_connection_string(cls, url: str) -> str:
+        # Parse Azure Connection Strings
+        azure_conn_str_re = re.compile(
+            "(DefaultEndpointsProtocol=(http|https));(AccountName=([a-zA-Z0-9]+));(AccountKey=)(.+);(EndpointSuffix=([a-zA-Z\\.]+))"
+        )
+        try:
+            matched: re.Match[str] | None = azure_conn_str_re.match(url)
+            if not matched:
+                raise StoreConfigurationError(
+                    f"The URL for the Azure connection-string, was not configured properly. Please check and try again: {url} "
+                )
+            res = f"DefaultEndpointsProtocol={matched.group(2)};AccountName={matched.group(4)};AccountKey=***;EndpointSuffix={matched.group(8)}"
+            return res
+        except Exception as e:
+            raise StoreConfigurationError(
+                f"Something went wrong when trying to obfuscate URL for Azure connection-string. Please check your configuration: {e}"
+            )
+
     @classmethod
     def _mask_db_url_no_sa(cls, url: str) -> str:
         # oracle+cx_oracle does not parse well using urlparse, parse as oracle then swap back

{"golden_diff": "diff --git a/great_expectations/data_context/util.py b/great_expectations/data_context/util.py\n--- a/great_expectations/data_context/util.py\n+++ b/great_expectations/data_context/util.py\n@@ -1,7 +1,10 @@\n+from __future__ import annotations\n+\n import copy\n import inspect\n import logging\n import pathlib\n+import re\n import warnings\n from typing import Any, Optional\n from urllib.parse import urlparse\n@@ -9,6 +12,7 @@\n import pyparsing as pp\n \n from great_expectations.alias_types import PathStr  # noqa: TCH001\n+from great_expectations.exceptions import StoreConfigurationError\n from great_expectations.types import safe_deep_copy\n from great_expectations.util import load_class, verify_dynamic_loading_support\n \n@@ -183,7 +187,9 @@\n         Returns:\n             url with password masked e.g. \"postgresql+psycopg2://username:***@host:65432/database\"\n         \"\"\"\n-        if sa is not None and use_urlparse is False:\n+        if url.startswith(\"DefaultEndpointsProtocol\"):\n+            return cls._obfuscate_azure_blobstore_connection_string(url)\n+        elif sa is not None and use_urlparse is False:\n             try:\n                 engine = sa.create_engine(url, **kwargs)\n                 return engine.url.__repr__()\n@@ -198,6 +204,25 @@\n             )\n         return cls._mask_db_url_no_sa(url=url)\n \n+    @classmethod\n+    def _obfuscate_azure_blobstore_connection_string(cls, url: str) -> str:\n+        # Parse Azure Connection Strings\n+        azure_conn_str_re = re.compile(\n+            \"(DefaultEndpointsProtocol=(http|https));(AccountName=([a-zA-Z0-9]+));(AccountKey=)(.+);(EndpointSuffix=([a-zA-Z\\\\.]+))\"\n+        )\n+        try:\n+            matched: re.Match[str] | None = azure_conn_str_re.match(url)\n+            if not matched:\n+                raise StoreConfigurationError(\n+                    f\"The URL for the Azure connection-string, was not configured properly. Please check and try again: {url} \"\n+                )\n+            res = f\"DefaultEndpointsProtocol={matched.group(2)};AccountName={matched.group(4)};AccountKey=***;EndpointSuffix={matched.group(8)}\"\n+            return res\n+        except Exception as e:\n+            raise StoreConfigurationError(\n+                f\"Something went wrong when trying to obfuscate URL for Azure connection-string. Please check your configuration: {e}\"\n+            )\n+\n     @classmethod\n     def _mask_db_url_no_sa(cls, url: str) -> str:\n         # oracle+cx_oracle does not parse well using urlparse, parse as oracle then swap back\n", "issue": "SQLAlchemy warning when not using SQL\n**Describe the bug**\r\nI'm using Azure Blob Storage as a backend and getting the following warning in the new version of ge 0.15.4. Don't think this warning showed up in versions prior to 0.15.\r\n\r\n`WARNING:great_expectations.data_context.util:Something went wrong when trying to use SQLAlchemy to obfuscate URL: Could not parse rfc1738 URL from string 'BlobEndpoint=xxxxxx'`\r\n\r\n**Expected behavior**\r\nNo warning of this is type should show when TupleAzureBlobStoreBackend backend is used.\r\n\r\n**Environment (please complete the following information):**\r\n - System: Azure\r\n - Great Expectations Version: 0.15.4\r\n\r\n\n", "before_files": [{"content": "import copy\nimport inspect\nimport logging\nimport pathlib\nimport warnings\nfrom typing import Any, Optional\nfrom urllib.parse import urlparse\n\nimport pyparsing as pp\n\nfrom great_expectations.alias_types import PathStr  # noqa: TCH001\nfrom great_expectations.types import safe_deep_copy\nfrom great_expectations.util import load_class, verify_dynamic_loading_support\n\ntry:\n    import sqlalchemy as sa\nexcept ImportError:\n    sa = None\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO: Rename config to constructor_kwargs and config_defaults -> constructor_kwarg_default\n# TODO: Improve error messages in this method. Since so much of our workflow is config-driven, this will be a *super* important part of DX.\ndef instantiate_class_from_config(config, runtime_environment, config_defaults=None):\n    \"\"\"Build a GX class from configuration dictionaries.\"\"\"\n\n    if config_defaults is None:\n        config_defaults = {}\n\n    config = copy.deepcopy(config)\n\n    module_name = config.pop(\"module_name\", None)\n    if module_name is None:\n        try:\n            module_name = config_defaults.pop(\"module_name\")\n        except KeyError:\n            raise KeyError(\n                \"Neither config : {} nor config_defaults : {} contains a module_name key.\".format(\n                    config,\n                    config_defaults,\n                )\n            )\n    else:\n        # Pop the value without using it, to avoid sending an unwanted value to the config_class\n        config_defaults.pop(\"module_name\", None)\n\n    logger.debug(f\"(instantiate_class_from_config) module_name -> {module_name}\")\n    verify_dynamic_loading_support(module_name=module_name)\n\n    class_name = config.pop(\"class_name\", None)\n    if class_name is None:\n        logger.warning(\n            \"Instantiating class from config without an explicit class_name is dangerous. Consider adding \"\n            f\"an explicit class_name for {config.get('name')}\"\n        )\n        try:\n            class_name = config_defaults.pop(\"class_name\")\n        except KeyError:\n            raise KeyError(\n                \"Neither config : {} nor config_defaults : {} contains a class_name key.\".format(\n                    config,\n                    config_defaults,\n                )\n            )\n    else:\n        # Pop the value without using it, to avoid sending an unwanted value to the config_class\n        config_defaults.pop(\"class_name\", None)\n\n    class_ = load_class(class_name=class_name, module_name=module_name)\n\n    config_with_defaults = copy.deepcopy(config_defaults)\n    config_with_defaults.update(config)\n    if runtime_environment is not None:\n        # If there are additional kwargs available in the runtime_environment requested by a\n        # class to be instantiated, provide them\n        argspec = inspect.getfullargspec(class_.__init__)[0][1:]\n\n        missing_args = set(argspec) - set(config_with_defaults.keys())\n        config_with_defaults.update(\n            {\n                missing_arg: runtime_environment[missing_arg]\n                for missing_arg in missing_args\n                if missing_arg in runtime_environment\n            }\n        )\n        # Add the entire runtime_environment as well if it's requested\n        if \"runtime_environment\" in missing_args:\n            config_with_defaults.update({\"runtime_environment\": runtime_environment})\n\n    try:\n        class_instance = class_(**config_with_defaults)\n    except TypeError as e:\n        raise TypeError(\n            \"Couldn't instantiate class: {} with config: \\n\\t{}\\n \\n\".format(\n                class_name, format_dict_for_error_message(config_with_defaults)\n            )\n            + str(e)\n        )\n\n    return class_instance\n\n\ndef format_dict_for_error_message(dict_):\n    # TODO : Tidy this up a bit. Indentation isn't fully consistent.\n\n    return \"\\n\\t\".join(\"\\t\\t\".join((str(key), str(dict_[key]))) for key in dict_)\n\n\ndef file_relative_path(\n    source_path: PathStr,\n    relative_path: PathStr,\n    strict: bool = True,\n) -> str:\n    \"\"\"\n    This function is useful when one needs to load a file that is\n    relative to the position of the current file. (Such as when\n    you encode a configuration file path in source file and want\n    in runnable in any current working directory)\n\n    It is meant to be used like the following:\n    file_relative_path(__file__, 'path/relative/to/file')\n\n    This has been modified from Dagster's utils:\n    H/T https://github.com/dagster-io/dagster/blob/8a250e9619a49e8bff8e9aa7435df89c2d2ea039/python_modules/dagster/dagster/utils/__init__.py#L34\n    \"\"\"\n    dir_path = pathlib.Path(source_path).parent\n    abs_path = dir_path.joinpath(relative_path).resolve(strict=strict)\n    return str(abs_path)\n\n\ndef parse_substitution_variable(substitution_variable: str) -> Optional[str]:\n    \"\"\"\n    Parse and check whether the string contains a substitution variable of the case insensitive form ${SOME_VAR} or $SOME_VAR\n    Args:\n        substitution_variable: string to be parsed\n\n    Returns:\n        string of variable name e.g. SOME_VAR or None if not parsable. If there are multiple substitution variables this currently returns the first e.g. $SOME_$TRING -> $SOME_\n    \"\"\"\n    substitution_variable_name = pp.Word(pp.alphanums + \"_\").setResultsName(\n        \"substitution_variable_name\"\n    )\n    curly_brace_parser = \"${\" + substitution_variable_name + \"}\"\n    non_curly_brace_parser = \"$\" + substitution_variable_name\n    both_parser = curly_brace_parser | non_curly_brace_parser\n    try:\n        parsed_substitution_variable = both_parser.parseString(substitution_variable)\n        return parsed_substitution_variable.substitution_variable_name\n    except pp.ParseException:\n        return None\n\n\nclass PasswordMasker:\n    \"\"\"\n    Used to mask passwords in Datasources. Does not mask sqlite urls.\n\n    Example usage\n    masked_db_url = PasswordMasker.mask_db_url(url)\n    where url = \"postgresql+psycopg2://username:password@host:65432/database\"\n    and masked_url = \"postgresql+psycopg2://username:***@host:65432/database\"\n\n    \"\"\"\n\n    MASKED_PASSWORD_STRING = \"***\"\n\n    # values with the following keys will be processed with cls.mask_db_url:\n    URL_KEYS = {\"connection_string\", \"url\"}\n\n    # values with these keys will be directly replaced with cls.MASKED_PASSWORD_STRING:\n    PASSWORD_KEYS = {\"access_token\", \"password\"}\n\n    @classmethod\n    def mask_db_url(cls, url: str, use_urlparse: bool = False, **kwargs) -> str:\n        \"\"\"\n        Mask password in database url.\n        Uses sqlalchemy engine parsing if sqlalchemy is installed, otherwise defaults to using urlparse from the stdlib which does not handle kwargs.\n        Args:\n            url: Database url e.g. \"postgresql+psycopg2://username:password@host:65432/database\"\n            use_urlparse: Skip trying to parse url with sqlalchemy and use urlparse\n            **kwargs: passed to create_engine()\n\n        Returns:\n            url with password masked e.g. \"postgresql+psycopg2://username:***@host:65432/database\"\n        \"\"\"\n        if sa is not None and use_urlparse is False:\n            try:\n                engine = sa.create_engine(url, **kwargs)\n                return engine.url.__repr__()\n            # Account for the edge case where we have SQLAlchemy in our env but haven't installed the appropriate dialect to match the input URL\n            except Exception as e:\n                logger.warning(\n                    f\"Something went wrong when trying to use SQLAlchemy to obfuscate URL: {e}\"\n                )\n        else:\n            warnings.warn(\n                \"SQLAlchemy is not installed, using urlparse to mask database url password which ignores **kwargs.\"\n            )\n        return cls._mask_db_url_no_sa(url=url)\n\n    @classmethod\n    def _mask_db_url_no_sa(cls, url: str) -> str:\n        # oracle+cx_oracle does not parse well using urlparse, parse as oracle then swap back\n        replace_prefix = None\n        if url.startswith(\"oracle+cx_oracle\"):\n            replace_prefix = {\"original\": \"oracle+cx_oracle\", \"temporary\": \"oracle\"}\n            url = url.replace(replace_prefix[\"original\"], replace_prefix[\"temporary\"])\n\n        parsed_url = urlparse(url)\n\n        # Do not parse sqlite\n        if parsed_url.scheme == \"sqlite\":\n            return url\n\n        colon = \":\" if parsed_url.port is not None else \"\"\n        masked_url = (\n            f\"{parsed_url.scheme}://{parsed_url.username}:{cls.MASKED_PASSWORD_STRING}\"\n            f\"@{parsed_url.hostname}{colon}{parsed_url.port or ''}{parsed_url.path or ''}\"\n        )\n\n        if replace_prefix is not None:\n            masked_url = masked_url.replace(\n                replace_prefix[\"temporary\"], replace_prefix[\"original\"]\n            )\n\n        return masked_url\n\n    @classmethod\n    def sanitize_config(cls, config: dict) -> dict:\n        \"\"\"\n        Mask sensitive fields in a Dict.\n        \"\"\"\n\n        # be defensive, since it would be logical to expect this method works with DataContextConfig\n        if not isinstance(config, dict):\n            raise TypeError(\n                \"PasswordMasker.sanitize_config expects param `config` \"\n                + f\"to be of type Dict, not of type {type(config)}\"\n            )\n\n        config_copy = safe_deep_copy(config)  # be immutable\n\n        def recursive_cleaner_method(config: Any) -> None:\n            if isinstance(config, dict):\n                for key, val in config.items():\n                    if not isinstance(val, str):\n                        recursive_cleaner_method(val)\n                    elif key in cls.URL_KEYS:\n                        config[key] = cls.mask_db_url(val)\n                    elif key in cls.PASSWORD_KEYS:\n                        config[key] = cls.MASKED_PASSWORD_STRING\n                    else:\n                        pass  # this string is not sensitive\n            elif isinstance(config, list):\n                for val in config:\n                    recursive_cleaner_method(val)\n\n        recursive_cleaner_method(config_copy)  # Perform anonymization in place\n\n        return config_copy\n", "path": "great_expectations/data_context/util.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport copy\nimport inspect\nimport logging\nimport pathlib\nimport re\nimport warnings\nfrom typing import Any, Optional\nfrom urllib.parse import urlparse\n\nimport pyparsing as pp\n\nfrom great_expectations.alias_types import PathStr  # noqa: TCH001\nfrom great_expectations.exceptions import StoreConfigurationError\nfrom great_expectations.types import safe_deep_copy\nfrom great_expectations.util import load_class, verify_dynamic_loading_support\n\ntry:\n    import sqlalchemy as sa\nexcept ImportError:\n    sa = None\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO: Rename config to constructor_kwargs and config_defaults -> constructor_kwarg_default\n# TODO: Improve error messages in this method. Since so much of our workflow is config-driven, this will be a *super* important part of DX.\ndef instantiate_class_from_config(config, runtime_environment, config_defaults=None):\n    \"\"\"Build a GX class from configuration dictionaries.\"\"\"\n\n    if config_defaults is None:\n        config_defaults = {}\n\n    config = copy.deepcopy(config)\n\n    module_name = config.pop(\"module_name\", None)\n    if module_name is None:\n        try:\n            module_name = config_defaults.pop(\"module_name\")\n        except KeyError:\n            raise KeyError(\n                \"Neither config : {} nor config_defaults : {} contains a module_name key.\".format(\n                    config,\n                    config_defaults,\n                )\n            )\n    else:\n        # Pop the value without using it, to avoid sending an unwanted value to the config_class\n        config_defaults.pop(\"module_name\", None)\n\n    logger.debug(f\"(instantiate_class_from_config) module_name -> {module_name}\")\n    verify_dynamic_loading_support(module_name=module_name)\n\n    class_name = config.pop(\"class_name\", None)\n    if class_name is None:\n        logger.warning(\n            \"Instantiating class from config without an explicit class_name is dangerous. Consider adding \"\n            f\"an explicit class_name for {config.get('name')}\"\n        )\n        try:\n            class_name = config_defaults.pop(\"class_name\")\n        except KeyError:\n            raise KeyError(\n                \"Neither config : {} nor config_defaults : {} contains a class_name key.\".format(\n                    config,\n                    config_defaults,\n                )\n            )\n    else:\n        # Pop the value without using it, to avoid sending an unwanted value to the config_class\n        config_defaults.pop(\"class_name\", None)\n\n    class_ = load_class(class_name=class_name, module_name=module_name)\n\n    config_with_defaults = copy.deepcopy(config_defaults)\n    config_with_defaults.update(config)\n    if runtime_environment is not None:\n        # If there are additional kwargs available in the runtime_environment requested by a\n        # class to be instantiated, provide them\n        argspec = inspect.getfullargspec(class_.__init__)[0][1:]\n\n        missing_args = set(argspec) - set(config_with_defaults.keys())\n        config_with_defaults.update(\n            {\n                missing_arg: runtime_environment[missing_arg]\n                for missing_arg in missing_args\n                if missing_arg in runtime_environment\n            }\n        )\n        # Add the entire runtime_environment as well if it's requested\n        if \"runtime_environment\" in missing_args:\n            config_with_defaults.update({\"runtime_environment\": runtime_environment})\n\n    try:\n        class_instance = class_(**config_with_defaults)\n    except TypeError as e:\n        raise TypeError(\n            \"Couldn't instantiate class: {} with config: \\n\\t{}\\n \\n\".format(\n                class_name, format_dict_for_error_message(config_with_defaults)\n            )\n            + str(e)\n        )\n\n    return class_instance\n\n\ndef format_dict_for_error_message(dict_):\n    # TODO : Tidy this up a bit. Indentation isn't fully consistent.\n\n    return \"\\n\\t\".join(\"\\t\\t\".join((str(key), str(dict_[key]))) for key in dict_)\n\n\ndef file_relative_path(\n    source_path: PathStr,\n    relative_path: PathStr,\n    strict: bool = True,\n) -> str:\n    \"\"\"\n    This function is useful when one needs to load a file that is\n    relative to the position of the current file. (Such as when\n    you encode a configuration file path in source file and want\n    in runnable in any current working directory)\n\n    It is meant to be used like the following:\n    file_relative_path(__file__, 'path/relative/to/file')\n\n    This has been modified from Dagster's utils:\n    H/T https://github.com/dagster-io/dagster/blob/8a250e9619a49e8bff8e9aa7435df89c2d2ea039/python_modules/dagster/dagster/utils/__init__.py#L34\n    \"\"\"\n    dir_path = pathlib.Path(source_path).parent\n    abs_path = dir_path.joinpath(relative_path).resolve(strict=strict)\n    return str(abs_path)\n\n\ndef parse_substitution_variable(substitution_variable: str) -> Optional[str]:\n    \"\"\"\n    Parse and check whether the string contains a substitution variable of the case insensitive form ${SOME_VAR} or $SOME_VAR\n    Args:\n        substitution_variable: string to be parsed\n\n    Returns:\n        string of variable name e.g. SOME_VAR or None if not parsable. If there are multiple substitution variables this currently returns the first e.g. $SOME_$TRING -> $SOME_\n    \"\"\"\n    substitution_variable_name = pp.Word(pp.alphanums + \"_\").setResultsName(\n        \"substitution_variable_name\"\n    )\n    curly_brace_parser = \"${\" + substitution_variable_name + \"}\"\n    non_curly_brace_parser = \"$\" + substitution_variable_name\n    both_parser = curly_brace_parser | non_curly_brace_parser\n    try:\n        parsed_substitution_variable = both_parser.parseString(substitution_variable)\n        return parsed_substitution_variable.substitution_variable_name\n    except pp.ParseException:\n        return None\n\n\nclass PasswordMasker:\n    \"\"\"\n    Used to mask passwords in Datasources. Does not mask sqlite urls.\n\n    Example usage\n    masked_db_url = PasswordMasker.mask_db_url(url)\n    where url = \"postgresql+psycopg2://username:password@host:65432/database\"\n    and masked_url = \"postgresql+psycopg2://username:***@host:65432/database\"\n\n    \"\"\"\n\n    MASKED_PASSWORD_STRING = \"***\"\n\n    # values with the following keys will be processed with cls.mask_db_url:\n    URL_KEYS = {\"connection_string\", \"url\"}\n\n    # values with these keys will be directly replaced with cls.MASKED_PASSWORD_STRING:\n    PASSWORD_KEYS = {\"access_token\", \"password\"}\n\n    @classmethod\n    def mask_db_url(cls, url: str, use_urlparse: bool = False, **kwargs) -> str:\n        \"\"\"\n        Mask password in database url.\n        Uses sqlalchemy engine parsing if sqlalchemy is installed, otherwise defaults to using urlparse from the stdlib which does not handle kwargs.\n        Args:\n            url: Database url e.g. \"postgresql+psycopg2://username:password@host:65432/database\"\n            use_urlparse: Skip trying to parse url with sqlalchemy and use urlparse\n            **kwargs: passed to create_engine()\n\n        Returns:\n            url with password masked e.g. \"postgresql+psycopg2://username:***@host:65432/database\"\n        \"\"\"\n        if url.startswith(\"DefaultEndpointsProtocol\"):\n            return cls._obfuscate_azure_blobstore_connection_string(url)\n        elif sa is not None and use_urlparse is False:\n            try:\n                engine = sa.create_engine(url, **kwargs)\n                return engine.url.__repr__()\n            # Account for the edge case where we have SQLAlchemy in our env but haven't installed the appropriate dialect to match the input URL\n            except Exception as e:\n                logger.warning(\n                    f\"Something went wrong when trying to use SQLAlchemy to obfuscate URL: {e}\"\n                )\n        else:\n            warnings.warn(\n                \"SQLAlchemy is not installed, using urlparse to mask database url password which ignores **kwargs.\"\n            )\n        return cls._mask_db_url_no_sa(url=url)\n\n    @classmethod\n    def _obfuscate_azure_blobstore_connection_string(cls, url: str) -> str:\n        # Parse Azure Connection Strings\n        azure_conn_str_re = re.compile(\n            \"(DefaultEndpointsProtocol=(http|https));(AccountName=([a-zA-Z0-9]+));(AccountKey=)(.+);(EndpointSuffix=([a-zA-Z\\\\.]+))\"\n        )\n        try:\n            matched: re.Match[str] | None = azure_conn_str_re.match(url)\n            if not matched:\n                raise StoreConfigurationError(\n                    f\"The URL for the Azure connection-string, was not configured properly. Please check and try again: {url} \"\n                )\n            res = f\"DefaultEndpointsProtocol={matched.group(2)};AccountName={matched.group(4)};AccountKey=***;EndpointSuffix={matched.group(8)}\"\n            return res\n        except Exception as e:\n            raise StoreConfigurationError(\n                f\"Something went wrong when trying to obfuscate URL for Azure connection-string. Please check your configuration: {e}\"\n            )\n\n    @classmethod\n    def _mask_db_url_no_sa(cls, url: str) -> str:\n        # oracle+cx_oracle does not parse well using urlparse, parse as oracle then swap back\n        replace_prefix = None\n        if url.startswith(\"oracle+cx_oracle\"):\n            replace_prefix = {\"original\": \"oracle+cx_oracle\", \"temporary\": \"oracle\"}\n            url = url.replace(replace_prefix[\"original\"], replace_prefix[\"temporary\"])\n\n        parsed_url = urlparse(url)\n\n        # Do not parse sqlite\n        if parsed_url.scheme == \"sqlite\":\n            return url\n\n        colon = \":\" if parsed_url.port is not None else \"\"\n        masked_url = (\n            f\"{parsed_url.scheme}://{parsed_url.username}:{cls.MASKED_PASSWORD_STRING}\"\n            f\"@{parsed_url.hostname}{colon}{parsed_url.port or ''}{parsed_url.path or ''}\"\n        )\n\n        if replace_prefix is not None:\n            masked_url = masked_url.replace(\n                replace_prefix[\"temporary\"], replace_prefix[\"original\"]\n            )\n\n        return masked_url\n\n    @classmethod\n    def sanitize_config(cls, config: dict) -> dict:\n        \"\"\"\n        Mask sensitive fields in a Dict.\n        \"\"\"\n\n        # be defensive, since it would be logical to expect this method works with DataContextConfig\n        if not isinstance(config, dict):\n            raise TypeError(\n                \"PasswordMasker.sanitize_config expects param `config` \"\n                + f\"to be of type Dict, not of type {type(config)}\"\n            )\n\n        config_copy = safe_deep_copy(config)  # be immutable\n\n        def recursive_cleaner_method(config: Any) -> None:\n            if isinstance(config, dict):\n                for key, val in config.items():\n                    if not isinstance(val, str):\n                        recursive_cleaner_method(val)\n                    elif key in cls.URL_KEYS:\n                        config[key] = cls.mask_db_url(val)\n                    elif key in cls.PASSWORD_KEYS:\n                        config[key] = cls.MASKED_PASSWORD_STRING\n                    else:\n                        pass  # this string is not sensitive\n            elif isinstance(config, list):\n                for val in config:\n                    recursive_cleaner_method(val)\n\n        recursive_cleaner_method(config_copy)  # Perform anonymization in place\n\n        return config_copy\n", "path": "great_expectations/data_context/util.py"}]}