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