From 619581fcbd8a713a256b0318f9d9a03814d3006d Mon Sep 17 00:00:00 2001 From: naddeoa Date: Tue, 19 Sep 2023 20:07:09 -0700 Subject: [PATCH] Update init to allow suppression (#1362) The new init system can make for confusing demos for existing users and new use cases. Making this suppression option to allow us to avoid output when we don't need to see it. This is also the first example that was updated to use init, though its a kind of weird case since we don't actually want to display the output in this context. Co-authored-by: Anthony Naddeo Co-authored-by: Jamie Broomall <88007022+jamie256@users.noreply.github.com> --- python/test_notebooks/notebook_tests.py | 1 + python/whylogs/api/whylabs/session/config.py | 17 +++---- python/whylogs/api/whylabs/session/prompts.py | 23 ++++++---- .../api/whylabs/session/session_types.py | 45 +++++++++++++------ python/whylogs/extras/image_metric.py | 13 +++--- 5 files changed, 63 insertions(+), 36 deletions(-) diff --git a/python/test_notebooks/notebook_tests.py b/python/test_notebooks/notebook_tests.py index b8ef7bc1b6..b2eeb5758a 100644 --- a/python/test_notebooks/notebook_tests.py +++ b/python/test_notebooks/notebook_tests.py @@ -8,6 +8,7 @@ OUTPUT_NOTEBOOK = "output.ipynb" skip_notebooks = [ "Guest Session.ipynb", + "Single_Image_Tracing_Profile_to_WhyLabs.ipynb", "Pyspark_Profiling.ipynb", "Kafka_Example.ipynb", "Writing_to_WhyLabs.ipynb", diff --git a/python/whylogs/api/whylabs/session/config.py b/python/whylogs/api/whylabs/session/config.py index d5c639c7a0..e983690bbd 100644 --- a/python/whylogs/api/whylabs/session/config.py +++ b/python/whylogs/api/whylabs/session/config.py @@ -292,8 +292,8 @@ def reset_config(self) -> None: def notify_session_type(self) -> None: config_path = self.get_config_file_path() - il.message(f"Initializing session with config {config_path}") - il.message() + il.message(f"Initializing session with config {config_path}", ignore_suppress=True) + il.message(ignore_suppress=True) if self.session_type == SessionType.WHYLABS: self._notify_type_whylabs(self.require_api_key()) elif self.session_type == SessionType.LOCAL: @@ -309,22 +309,23 @@ def _notify_type_whylabs(self, api_key: str) -> None: else: org_id = self.get_org_id() or "not set" # Shouldn't be possible to be None at this point - il.success(f"Using session type: {SessionType.WHYLABS.name}") - il.option(f"org id: {org_id}") - il.option(f"api key: {parsed_api_key.api_key_id}") + il.success(f"Using session type: {SessionType.WHYLABS.name}", ignore_suppress=True) + il.option(f"org id: {org_id}", ignore_suppress=True) + il.option(f"api key: {parsed_api_key.api_key_id}", ignore_suppress=True) if default_dataset_id: il.option(f"default dataset: {default_dataset_id}") def _notify_type_anon(self) -> None: anonymous_session_id = self.get_session_id() - il.success(f"Using session type: {SessionType.WHYLABS_ANONYMOUS.name}") + il.success(f"Using session type: {SessionType.WHYLABS_ANONYMOUS.name}", ignore_suppress=True) id_text = "" if not anonymous_session_id else anonymous_session_id - il.option(f"session id: {id_text}") + il.option(f"session id: {id_text}", ignore_suppress=True) def _notify_type_local(self) -> None: il.success( f"Using session type: {SessionType.LOCAL.name}. " - "Profiles won't be uploaded or written anywhere automatically." + "Profiles won't be uploaded or written anywhere automatically.", + ignore_suppress=True, ) def _determine_session_type_prompt(self, init_config: InitConfig) -> SessionType: diff --git a/python/whylogs/api/whylabs/session/prompts.py b/python/whylogs/api/whylabs/session/prompts.py index ef04b5ebd4..e0a8e6bed7 100644 --- a/python/whylogs/api/whylabs/session/prompts.py +++ b/python/whylogs/api/whylabs/session/prompts.py @@ -11,14 +11,14 @@ def _get_user_choice(prompt: str, options: List[str]) -> int: - il.question(prompt) + il.question(prompt, ignore_suppress=True) for i, option in enumerate(options, 1): - il.option(f"{i}. {option}") + il.option(f"{i}. {option}", ignore_suppress=True) while True: try: sys.stdout.flush() - il.message() + il.message(ignore_suppress=True) choice = int(input("Enter a number from the list: ")) if 1 <= choice <= len(options): return choice @@ -35,6 +35,9 @@ def prompt_session_type(allow_anonymous: bool = True, allow_local: bool = False) if allow_local: options.append("Local. Don't upload data anywhere.") + if len(options) == 1: + return SessionType.WHYLABS + choice = _get_user_choice("What kind of session do you want to use?", options) return [SessionType.WHYLABS, SessionType.WHYLABS_ANONYMOUS, SessionType.LOCAL][choice - 1] @@ -42,7 +45,7 @@ def prompt_session_type(allow_anonymous: bool = True, allow_local: bool = False) def prompt_default_dataset_id() -> Optional[str]: try: sys.stdout.flush() - il.message() + il.message(ignore_suppress=True) default_dataset_id = input("[OPTIONAL] Enter a default dataset id to upload to: ").strip() return default_dataset_id except Exception: @@ -53,14 +56,15 @@ def prompt_api_key() -> ApiKey: while True: try: sys.stdout.flush() - il.message() + il.message(ignore_suppress=True) api_key = input( "Enter your WhyLabs api key. You can find it at https://hub.whylabsapp.com/settings/access-tokens: " ) return parse_api_key(api_key) except Exception: il.warning( - f"Couldn't parse the api key. Expected a key with the format 'key_id.key:org_id'. Got: {api_key}" + f"Couldn't parse the api key. Expected a key with the format 'key_id.key:org_id'. Got: {api_key}", + ignore_suppress=True, ) @@ -68,9 +72,12 @@ def prompt_org_id() -> str: while True: try: sys.stdout.flush() - il.message() + il.message(ignore_suppress=True) org_id = input("Enter your org id. You can find it at https://hub.whylabsapp.com/settings/access-tokens: ") validate_org_id(org_id) return org_id except Exception: - il.warning(f"Couldn't parse the org id. Expected an id that starts with 'org-'. Got: {org_id}") + il.warning( + f"Couldn't parse the org id. Expected an id that starts with 'org-'. Got: {org_id}", + ignore_suppress=True, + ) diff --git a/python/whylogs/api/whylabs/session/session_types.py b/python/whylogs/api/whylabs/session/session_types.py index 1ec96a0ae0..06e6b0a360 100644 --- a/python/whylogs/api/whylabs/session/session_types.py +++ b/python/whylogs/api/whylabs/session/session_types.py @@ -1,4 +1,5 @@ # Various common types to avoid circular dependencies +import os from dataclasses import dataclass from enum import Enum from typing import Callable, Optional, Set, Union @@ -27,22 +28,38 @@ def init_notebook_logging() -> None: InteractiveLogger._is_notebook = True @staticmethod - def message(message: str = "", log_fn: Optional[Callable] = None) -> None: + def __should_log(ignore_suppress: bool = False) -> bool: + """ + Returns true if we should log, false otherwise. + """ + if ignore_suppress: + return InteractiveLogger._is_notebook + else: + return not os.environ.get("WHYLOGS_SUPPRESS_LOG_OUTPUT") and InteractiveLogger._is_notebook + + @staticmethod + def message(message: str = "", log_fn: Optional[Callable] = None, ignore_suppress: bool = False) -> None: """ Log a message only if we're in a notebook environment. + + Args: + message: The message to log + log_fn: A function to log to instead of printing if we're not in a notebook. + ignore_suppress: If true, will log even if WHYLOGS_SUPPRESS_LOG_OUTPUT is set. It still needs + to be in a notebook though or it won't show. """ - if InteractiveLogger._is_notebook: + if InteractiveLogger.__should_log(ignore_suppress=ignore_suppress): print(message) elif log_fn is not None: log_fn(message) @staticmethod - def option(message: str) -> None: + def option(message: str, ignore_suppress: bool = False) -> None: """ Log an option line, which is anything that has multiple related lines in a row like multiple choices or a list things. """ - InteractiveLogger.message(f" โคท {message}") + InteractiveLogger.message(f" โคท {message}", ignore_suppress=ignore_suppress) @staticmethod def inspect(message: str) -> None: @@ -52,39 +69,39 @@ def inspect(message: str) -> None: InteractiveLogger.message(f"๐Ÿ” {message}") @staticmethod - def question(message: str) -> None: + def question(message: str, ignore_suppress: bool = False) -> None: """ Log a question. """ - InteractiveLogger.message(f"โ“ {message}") + InteractiveLogger.message(f"โ“ {message}", ignore_suppress=ignore_suppress) @staticmethod - def success(message: str) -> None: + def success(message: str, ignore_suppress: bool = False) -> None: """ Log a success line, which has a green checkmark. """ - InteractiveLogger.message(f"โœ… {message}") + InteractiveLogger.message(f"โœ… {message}", ignore_suppress=ignore_suppress) @staticmethod - def failure(message: str) -> None: + def failure(message: str, ignore_suppress: bool = False) -> None: """ Log a failure, which has a red x. """ - InteractiveLogger.message(f"โŒ {message}") + InteractiveLogger.message(f"โŒ {message}", ignore_suppress=ignore_suppress) @staticmethod - def warning(message: str, log_fn: Optional[Callable] = None) -> None: + def warning(message: str, log_fn: Optional[Callable] = None, ignore_suppress: bool = False) -> None: """ Log a warning, which has a warning sign. """ - InteractiveLogger.message(f"โš ๏ธ {message}", log_fn=log_fn) + InteractiveLogger.message(f"โš ๏ธ {message}", log_fn=log_fn, ignore_suppress=ignore_suppress) @staticmethod - def warning_once(message: str, log_fn: Optional[Callable] = None) -> None: + def warning_once(message: str, log_fn: Optional[Callable] = None, ignore_suppress: bool = False) -> None: """ Like warning, but only logs once. """ - if not InteractiveLogger._is_notebook: + if not InteractiveLogger.__should_log(ignore_suppress=ignore_suppress): return if hash(message) not in InteractiveLogger.__warnings: diff --git a/python/whylogs/extras/image_metric.py b/python/whylogs/extras/image_metric.py index da881a6fb6..8dfceed174 100644 --- a/python/whylogs/extras/image_metric.py +++ b/python/whylogs/extras/image_metric.py @@ -29,10 +29,10 @@ logger = logging.getLogger(__name__) try: - from PIL.Image import Image as ImageType - from PIL.ImageStat import Stat - from PIL.TiffImagePlugin import IFDRational - from PIL.TiffTags import TAGS + from PIL.Image import Image as ImageType # type: ignore + from PIL.ImageStat import Stat # type: ignore + from PIL.TiffImagePlugin import IFDRational # type: ignore + from PIL.TiffTags import TAGS # type: ignore except ImportError as e: ImageType = None # type: ignore logger.warning(str(e)) @@ -100,7 +100,7 @@ def get_pil_exif_metadata(img: ImageType) -> Dict: return metadata -def image_based_metadata(img): +def image_based_metadata(img: ImageType) -> Dict[str, int]: return { "ImagePixelWidth": img.width, "ImagePixelHeight": img.height, @@ -248,6 +248,7 @@ def log_image( images: Union[ImageType, List[ImageType], Dict[str, ImageType]], default_column_prefix: str = "image", schema: Optional[DatasetSchema] = None, + trace_id: Optional[str] = None, ) -> ResultSet: if isinstance(images, ImageType): images = {default_column_prefix: images} @@ -275,7 +276,7 @@ def resolve(self, name: str, why_type: DataType, column_schema: ColumnSchema) -> if not isinstance(schema.default_configs, ImageMetricConfig): raise ValueError("log_image requires DatasetSchema with an ImageMetricConfig as default_configs") - return why.log(row=images, schema=schema) + return why.log(row=images, schema=schema, trace_id=trace_id) # Register it so Multimetric and ProfileView can deserialize