diff --git a/python/whylogs/api/whylabs/session/config.py b/python/whylogs/api/whylabs/session/config.py index 2e2b20d57..96e9964a8 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: Optional[bool] = None 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(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) def _init_parser(self) -> None: try: @@ -354,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() @@ -406,6 +411,22 @@ 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, 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: + 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() 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 + _CONFIG_WHYLABS_SECTION = "whylabs" diff --git a/python/whylogs/api/whylabs/session/prompts.py b/python/whylogs/api/whylabs/session/prompts.py index d084d5ea0..7fa185175 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 bf27b4cbc..5ff8bdb8c 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 fda51a3e1..1549e6afb 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, @@ -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, @@ -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 diff --git a/python/whylogs/api/writer/whylabs.py b/python/whylogs/api/writer/whylabs.py index 30d8f6c73..f82072529 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_base.py b/python/whylogs/api/writer/whylabs_base.py index d12831759..abb9dd886 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_batch_writer.py b/python/whylogs/api/writer/whylabs_batch_writer.py index 47f0aa8cf..273b96016 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 6799ea475..d6b9c67eb 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.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, @@ -192,6 +193,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 +363,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 +649,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/whylabs_estimation_result_writer.py b/python/whylogs/api/writer/whylabs_estimation_result_writer.py index d47807ae5..971a8419f 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 9a3a4928d..807899e99 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 7b8b271ce..8fdca2c38 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 diff --git a/python/whylogs/api/writer/writer.py b/python/whylogs/api/writer/writer.py index 68f1855f1..e2d08e2c7 100644 --- a/python/whylogs/api/writer/writer.py +++ b/python/whylogs/api/writer/writer.py @@ -105,6 +105,9 @@ def write( def option(self, **kwargs: Any) -> "Writer": return self + def _option(self, **kwargs: Any) -> "Writer": + return self + class WriterWrapper: """Elide the Writable argument""" @@ -127,6 +130,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