From 0a2e4be6c481716a94f14df0c95574c9f8b764e7 Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Thu, 26 Sep 2024 22:09:58 +0100 Subject: [PATCH 1/6] Opt-in to uploading profiles from why.log() --- python/whylogs/api/whylabs/session/config.py | 19 +++++++++++++++- python/whylogs/api/whylabs/session/prompts.py | 9 ++++++++ python/whylogs/api/whylabs/session/session.py | 6 +++-- .../api/whylabs/session/session_manager.py | 2 +- python/whylogs/api/writer/whylabs_base.py | 4 ++++ python/whylogs/api/writer/whylabs_client.py | 22 +++++++++++++++++++ python/whylogs/api/writer/writer.py | 8 +++++++ 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/python/whylogs/api/whylabs/session/config.py b/python/whylogs/api/whylabs/session/config.py index 2e2b20d57d..6ad32cbfd3 100644 --- a/python/whylogs/api/whylabs/session/config.py +++ b/python/whylogs/api/whylabs/session/config.py @@ -16,6 +16,7 @@ prompt_default_dataset_id, prompt_org_id, prompt_session_type, + prompt_upload_on_log, ) from whylogs.api.whylabs.session.session_types import ApiKeyV1, ApiKeyV2 from whylogs.api.whylabs.session.session_types import InteractiveLogger as il @@ -66,10 +67,11 @@ class ConfigVariableName(Enum): class InitConfig: whylabs_api_key: Optional[str] = None allow_anonymous: bool = True - allow_local: bool = False + allow_local: bool = True default_dataset_id: Optional[str] = None config_path: Optional[str] = None force_local: Optional[bool] = None + upload_on_log: bool = False class SessionConfig: @@ -90,8 +92,10 @@ def __init__(self, init_config: Optional[InitConfig] = None) -> None: if force_interactive: self.reset_config() self.session_type = self._determine_session_type_prompt(self._init_config) + self.upload_on_log = self._determine_upload_on_log_prompt() else: self.session_type = self._determine_session_type(self._init_config) + self.upload_on_log = self._determine_upload_on_log(self._init_config) def _init_parser(self) -> None: try: @@ -406,6 +410,19 @@ def _determine_session_type(self, init_config: InitConfig) -> SessionType: f"interactive environment. See {INIT_DOCS} for instructions on using why.init()." ) + def _determine_upload_on_log_prompt(self) -> bool: + return self.session_type != SessionType.LOCAL and prompt_upload_on_log() + + def _determine_upload_on_log(self, init_config: InitConfig) -> bool: + if init_config.force_local or self.session_type == SessionType.LOCAL: + return False + + # If we're in an interactive environment then prompt the user to pick upload on log + if is_interractive(): + return self._determine_upload_on_log_prompt() + + return init_config.upload_on_log or False + _CONFIG_WHYLABS_SECTION = "whylabs" diff --git a/python/whylogs/api/whylabs/session/prompts.py b/python/whylogs/api/whylabs/session/prompts.py index d084d5ea0b..7fa1851758 100644 --- a/python/whylogs/api/whylabs/session/prompts.py +++ b/python/whylogs/api/whylabs/session/prompts.py @@ -43,6 +43,15 @@ def prompt_session_type(allow_anonymous: bool = True, allow_local: bool = False) return [SessionType.WHYLABS, SessionType.WHYLABS_ANONYMOUS, SessionType.LOCAL][choice - 1] +def prompt_upload_on_log() -> bool: + options = [ + "No. Use an explicit WhyLabsWriter to manage uploads to WhyLabs", + "Yes. Calling why.log() will automatically upload the results to WhyLabs", + ] + choice = _get_user_choice("Do you want to automatically upload profiles in why.log()?", options) + return [False, True][choice - 1] + + def prompt_default_dataset_id() -> Optional[str]: try: sys.stdout.flush() diff --git a/python/whylogs/api/whylabs/session/session.py b/python/whylogs/api/whylabs/session/session.py index bf27b4cbc6..5ff8bdb8ca 100644 --- a/python/whylogs/api/whylabs/session/session.py +++ b/python/whylogs/api/whylabs/session/session.py @@ -279,7 +279,9 @@ def __create_log_api(self, config: SessionConfig) -> LogApi: def upload_reference_profiles(self, profile_aliases: Dict[str, ResultSet]) -> Union[UploadResult, NotSupported]: results: List[str] = [] for alias, profile in profile_aliases.items(): - success, ids = profile.writer("whylabs").option(reference_profile_name=alias).write() + success, ids = ( + profile.writer("whylabs")._option(call_from_log=True).option(reference_profile_name=alias).write() + ) if success: ids = ids if isinstance(ids, list) else [(True, ids)] results.append(*[id for _, id in ids]) @@ -301,7 +303,7 @@ def upload_reference_profiles(self, profile_aliases: Dict[str, ResultSet]) -> Un ) def upload_batch_profile(self, profile: ResultSet) -> Union[UploadResult, NotSupported]: - result = profile.writer("whylabs").write() + result = profile.writer("whylabs")._option(call_from_log=True).write() utc_now = int(datetime.now(timezone.utc).timestamp() * 1000) timestamps: Set[int] = set() # For generating the viewing url after upload diff --git a/python/whylogs/api/whylabs/session/session_manager.py b/python/whylogs/api/whylabs/session/session_manager.py index fda51a3e1a..8c6aa9fb4e 100644 --- a/python/whylogs/api/whylabs/session/session_manager.py +++ b/python/whylogs/api/whylabs/session/session_manager.py @@ -57,7 +57,7 @@ def is_active() -> bool: def init( reinit: bool = False, allow_anonymous: bool = True, - allow_local: bool = False, + allow_local: bool = True, whylabs_api_key: Optional[str] = None, default_dataset_id: Optional[str] = None, config_path: Optional[str] = None, diff --git a/python/whylogs/api/writer/whylabs_base.py b/python/whylogs/api/writer/whylabs_base.py index d12831759d..abb9dd8869 100644 --- a/python/whylogs/api/writer/whylabs_base.py +++ b/python/whylogs/api/writer/whylabs_base.py @@ -140,6 +140,10 @@ def option(self, **kwargs) -> Writer: # type: ignore self._whylabs_client = self._whylabs_client.option(**kwargs) return self + def _option(self, **kwargs) -> Writer: # type: ignore + self._whylabs_client = self._whylabs_client._option(**kwargs) + return self + def _get_dataset_epoch( self, view: Union[DatasetProfileView, SegmentedDatasetProfileView], utc_now: Optional[datetime.datetime] = None ) -> int: diff --git a/python/whylogs/api/writer/whylabs_client.py b/python/whylogs/api/writer/whylabs_client.py index 6799ea4753..62cd82d6f8 100644 --- a/python/whylogs/api/writer/whylabs_client.py +++ b/python/whylogs/api/writer/whylabs_client.py @@ -192,6 +192,7 @@ def __init__( self._api_config: Optional[Configuration] = None self._prefer_sync = read_bool_env_var(WHYLOGS_PREFER_SYNC_KEY, False) self._transaction_id: Optional[str] = None + self._called_from_log = False _http_proxy = os.environ.get("HTTP_PROXY") _https_proxy = os.environ.get("HTTPS_PROXY") @@ -361,6 +362,19 @@ def option(self, **kwargs) -> "WhyLabsClient": # type: ignore ) return self + def _option(self, **kwargs) -> "WhyLabsClient": # type: ignore + """ + + Parameters + ---------- + called_from_log: bool Set this to True in the context of a Session logger + """ + + called_from_log = kwargs.get("called_from_log") + if called_from_log is not None: + self._called_from_log = called_from_log + return self + def _tag_columns(self, columns: List[str], value: str) -> Tuple[bool, str]: """Sets the column as an input or output for the specified dataset. @@ -634,6 +648,14 @@ def do_upload( profile_file: Optional[IO[bytes]] = None, ) -> Tuple[bool, str]: assert profile_path or profile_file, "Either a file or file path must be specified when uploading profiles" + session = default_init() + config = session.config + if config.upload_on_log and not self._called_from_log: + logger.warning( + "The current session is configured to upload profiles in the why.log() call. " + + "Uploading profiles explicitely with a WhyLabsWriter may result in redundant uploads." + ) + try: if profile_file: status, reason = self._put_file(profile_file, upload_url, dataset_timestamp) # type: ignore diff --git a/python/whylogs/api/writer/writer.py b/python/whylogs/api/writer/writer.py index 68f1855f12..df6384959d 100644 --- a/python/whylogs/api/writer/writer.py +++ b/python/whylogs/api/writer/writer.py @@ -105,6 +105,10 @@ def write( def option(self, **kwargs: Any) -> "Writer": return self + @abstractmethod + def _option(self, **kwargs: Any) -> "Writer": + return self + class WriterWrapper: """Elide the Writable argument""" @@ -127,6 +131,10 @@ def option(self, **kwargs: Any) -> "WriterWrapper": self._writer = self._writer.option(**kwargs) return self + def _option(self, **kwargs: Any) -> "WriterWrapper": + self._writer = self._writer._option(**kwargs) + return self + class Writers: @staticmethod From 63501bafa97776e169e94e888db499a104901169 Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Thu, 26 Sep 2024 21:58:12 +0000 Subject: [PATCH 2/6] allow default _option() method --- python/whylogs/api/writer/writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/whylogs/api/writer/writer.py b/python/whylogs/api/writer/writer.py index df6384959d..e2d08e2c70 100644 --- a/python/whylogs/api/writer/writer.py +++ b/python/whylogs/api/writer/writer.py @@ -105,7 +105,6 @@ def write( def option(self, **kwargs: Any) -> "Writer": return self - @abstractmethod def _option(self, **kwargs: Any) -> "Writer": return self From 57908ddf888ad54ca0c70cee49a5fa5b8cd56ad5 Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Fri, 27 Sep 2024 19:02:10 +0000 Subject: [PATCH 3/6] smarter prompting logic --- python/whylogs/api/whylabs/session/config.py | 16 ++++++++++------ .../api/whylabs/session/session_manager.py | 6 +----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/whylogs/api/whylabs/session/config.py b/python/whylogs/api/whylabs/session/config.py index 6ad32cbfd3..96e9964a8b 100644 --- a/python/whylogs/api/whylabs/session/config.py +++ b/python/whylogs/api/whylabs/session/config.py @@ -71,7 +71,7 @@ class InitConfig: default_dataset_id: Optional[str] = None config_path: Optional[str] = None force_local: Optional[bool] = None - upload_on_log: bool = False + upload_on_log: Optional[bool] = None class SessionConfig: @@ -92,7 +92,7 @@ def __init__(self, init_config: Optional[InitConfig] = None) -> None: if force_interactive: self.reset_config() self.session_type = self._determine_session_type_prompt(self._init_config) - self.upload_on_log = self._determine_upload_on_log_prompt() + self.upload_on_log = self._determine_upload_on_log_prompt(self._init_config) else: self.session_type = self._determine_session_type(self._init_config) self.upload_on_log = self._determine_upload_on_log(self._init_config) @@ -358,7 +358,8 @@ def _notify_type_local(self) -> None: ) def _determine_session_type_prompt(self, init_config: InitConfig) -> SessionType: - session_type = prompt_session_type(init_config.allow_anonymous, init_config.allow_local) + allow_local = init_config.allow_local and init_config.upload_on_log in {None, False} + session_type = prompt_session_type(init_config.allow_anonymous, allow_local) if session_type == SessionType.WHYLABS: api_key = prompt_api_key() @@ -410,7 +411,10 @@ def _determine_session_type(self, init_config: InitConfig) -> SessionType: f"interactive environment. See {INIT_DOCS} for instructions on using why.init()." ) - def _determine_upload_on_log_prompt(self) -> bool: + def _determine_upload_on_log_prompt(self, init_config: InitConfig) -> bool: + if init_config.upload_on_log is not None: + return init_config.upload_on_log and self.session_type != SessionType.LOCAL + return self.session_type != SessionType.LOCAL and prompt_upload_on_log() def _determine_upload_on_log(self, init_config: InitConfig) -> bool: @@ -418,8 +422,8 @@ def _determine_upload_on_log(self, init_config: InitConfig) -> bool: return False # If we're in an interactive environment then prompt the user to pick upload on log - if is_interractive(): - return self._determine_upload_on_log_prompt() + if is_interractive() and init_config.upload_on_log is None: + return self._determine_upload_on_log_prompt(init_config) return init_config.upload_on_log or False diff --git a/python/whylogs/api/whylabs/session/session_manager.py b/python/whylogs/api/whylabs/session/session_manager.py index 8c6aa9fb4e..39e189cd6b 100644 --- a/python/whylogs/api/whylabs/session/session_manager.py +++ b/python/whylogs/api/whylabs/session/session_manager.py @@ -113,6 +113,7 @@ def init( default_dataset_id=default_dataset_id, config_path=config_path, force_local=kwargs.get("force_local", False), + upload_on_log=kwargs.get("upload_on_log", False), ) ) @@ -135,11 +136,6 @@ def get_current_session() -> Optional[Session]: if manager is not None: return manager.session - il.warning_once( - f"No session found. Call whylogs.init() to initialize a session and authenticate. See {INIT_DOCS} for more information.", - logger.warning, - ) - return None From 33a5c9dd621368e83b0e9492bf765e95fba11c5d Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Fri, 27 Sep 2024 22:19:43 +0000 Subject: [PATCH 4/6] pre-commit --- python/whylogs/api/whylabs/session/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/whylogs/api/whylabs/session/session_manager.py b/python/whylogs/api/whylabs/session/session_manager.py index 39e189cd6b..1549e6afb0 100644 --- a/python/whylogs/api/whylabs/session/session_manager.py +++ b/python/whylogs/api/whylabs/session/session_manager.py @@ -1,7 +1,7 @@ import logging from typing import Optional -from whylogs.api.whylabs.session.config import INIT_DOCS, InitConfig, SessionConfig +from whylogs.api.whylabs.session.config import InitConfig, SessionConfig from whylogs.api.whylabs.session.session import ( ApiKeySession, GuestSession, From 6ec4e8091b1dec9260721461de921d662765db10 Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Fri, 27 Sep 2024 23:30:52 +0100 Subject: [PATCH 5/6] wrong import --- python/whylogs/api/writer/whylabs.py | 2 +- python/whylogs/api/writer/whylabs_batch_writer.py | 2 +- python/whylogs/api/writer/whylabs_client.py | 3 ++- python/whylogs/api/writer/whylabs_estimation_result_writer.py | 2 +- python/whylogs/api/writer/whylabs_reference_writer.py | 2 +- python/whylogs/api/writer/whylabs_transaction_writer.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/python/whylogs/api/writer/whylabs.py b/python/whylogs/api/writer/whylabs.py index 30d8f6c73d..f82072529a 100644 --- a/python/whylogs/api/writer/whylabs.py +++ b/python/whylogs/api/writer/whylabs.py @@ -3,7 +3,7 @@ from whylabs_client import ApiClient -from whylogs.api.whylabs.session.session_manager import INIT_DOCS +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.writer.whylabs_base import WhyLabsWriterBase from whylogs.api.writer.whylabs_batch_writer import WhyLabsBatchWriter from whylogs.api.writer.whylabs_client import WhyLabsClient diff --git a/python/whylogs/api/writer/whylabs_batch_writer.py b/python/whylogs/api/writer/whylabs_batch_writer.py index 47f0aa8cf3..273b96016d 100644 --- a/python/whylogs/api/writer/whylabs_batch_writer.py +++ b/python/whylogs/api/writer/whylabs_batch_writer.py @@ -7,7 +7,7 @@ from whylabs_client import ApiClient from whylogs.api.logger.result_set import SegmentedResultSet -from whylogs.api.whylabs.session.session_manager import INIT_DOCS +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.writer.whylabs_base import WhyLabsWriterBase from whylogs.api.writer.whylabs_client import WhyLabsClient from whylogs.api.writer.writer import _Writable diff --git a/python/whylogs/api/writer/whylabs_client.py b/python/whylogs/api/writer/whylabs_client.py index 62cd82d6f8..43adad7d04 100644 --- a/python/whylogs/api/writer/whylabs_client.py +++ b/python/whylogs/api/writer/whylabs_client.py @@ -38,7 +38,8 @@ ) from whylogs.api.logger.result_set import ResultSet, SegmentedResultSet -from whylogs.api.whylabs.session.session_manager import INIT_DOCS, default_init +from whylogs.api.whylabs.session.session_manager import default_init +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.whylabs.session.whylabs_client_cache import ( ClientCacheConfig, EnvironmentKeyRefresher, diff --git a/python/whylogs/api/writer/whylabs_estimation_result_writer.py b/python/whylogs/api/writer/whylabs_estimation_result_writer.py index d47807ae50..971a8419ff 100644 --- a/python/whylogs/api/writer/whylabs_estimation_result_writer.py +++ b/python/whylogs/api/writer/whylabs_estimation_result_writer.py @@ -5,7 +5,7 @@ from whylabs_client import ApiClient from whylogs.api.logger import log -from whylogs.api.whylabs.session.session_manager import INIT_DOCS +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.writer.whylabs_base import WhyLabsWriterBase from whylogs.api.writer.whylabs_client import WhyLabsClient from whylogs.api.writer.writer import _Writable diff --git a/python/whylogs/api/writer/whylabs_reference_writer.py b/python/whylogs/api/writer/whylabs_reference_writer.py index 9a3a4928d1..807899e997 100644 --- a/python/whylogs/api/writer/whylabs_reference_writer.py +++ b/python/whylogs/api/writer/whylabs_reference_writer.py @@ -6,7 +6,7 @@ from whylabs_client import ApiClient from whylogs.api.logger.result_set import SegmentedResultSet -from whylogs.api.whylabs.session.session_manager import INIT_DOCS +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.writer.whylabs_base import WhyLabsWriterBase from whylogs.api.writer.whylabs_client import WhyLabsClient from whylogs.api.writer.writer import _Writable diff --git a/python/whylogs/api/writer/whylabs_transaction_writer.py b/python/whylogs/api/writer/whylabs_transaction_writer.py index 7b8b271ce0..8fdca2c38b 100644 --- a/python/whylogs/api/writer/whylabs_transaction_writer.py +++ b/python/whylogs/api/writer/whylabs_transaction_writer.py @@ -5,7 +5,7 @@ from whylabs_client import ApiClient from whylogs.api.logger.result_set import SegmentedResultSet -from whylogs.api.whylabs.session.session_manager import INIT_DOCS +from whylogs.api.whylabs.session.config import INIT_DOCS from whylogs.api.writer.whylabs_base import WhyLabsWriterBase from whylogs.api.writer.whylabs_client import WhyLabsClient from whylogs.api.writer.writer import _Writable From f381a31cd9f7df5b71871df99614aa3f23ee2913 Mon Sep 17 00:00:00 2001 From: Richard Rogers Date: Fri, 27 Sep 2024 23:38:29 +0100 Subject: [PATCH 6/6] pre-commit --- python/whylogs/api/writer/whylabs_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/whylogs/api/writer/whylabs_client.py b/python/whylogs/api/writer/whylabs_client.py index 43adad7d04..d6b9c67eba 100644 --- a/python/whylogs/api/writer/whylabs_client.py +++ b/python/whylogs/api/writer/whylabs_client.py @@ -38,8 +38,8 @@ ) from whylogs.api.logger.result_set import ResultSet, SegmentedResultSet -from whylogs.api.whylabs.session.session_manager import default_init from whylogs.api.whylabs.session.config import INIT_DOCS +from whylogs.api.whylabs.session.session_manager import default_init from whylogs.api.whylabs.session.whylabs_client_cache import ( ClientCacheConfig, EnvironmentKeyRefresher,