diff --git a/python/examples/integrations/writers/Single_Image_Tracing_Profile_to_WhyLabs.ipynb b/python/examples/integrations/writers/Single_Image_Tracing_Profile_to_WhyLabs.ipynb new file mode 100644 index 0000000000..158be15843 --- /dev/null +++ b/python/examples/integrations/writers/Single_Image_Tracing_Profile_to_WhyLabs.ipynb @@ -0,0 +1,3029 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "-NPr95FQN4jX" + }, + "source": [ + "# Uploading and Retrieving Single Image Profiles to WhyLabs\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/whylabs/whylogs/blob/dev/felipe/single-image-example/python/examples/integrations/writers/Single_Image_Profile_to_WhyLabs.ipynb)\n", + "\n", + "In this example, we'll walk through the process of uploading single image profiles to WhyLabs.\n", + "\n", + "Additionally, we'll explain how to retrieve the previously uploaded original profiles.\n", + "\n", + "> Note: This example assumes that you have an organization configured for single profile uploads. If you don't have one, please contact the WhyLabs team." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P9-pHz-N8wur" + }, + "source": [ + "## Installing whylogs" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "ALO8hWy9N4jY" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mDEPRECATION: feast 0.22.4 has a non-standard dependency specifier googleapis-common-protos<2,>=1.52.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of feast or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: feast 0.22.4 has a non-standard dependency specifier PyYAML<7,>=5.4.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of feast or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: feast 0.22.4 has a non-standard dependency specifier dask<2022.02.0,>=2021.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of feast or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n", + "\u001b[0mNote: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install whylogs[embeddings,viz,image] -q" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w4kzfjmC-y-S" + }, + "source": [ + "## WhyLabs Credentials\n", + "\n", + "Let's set the WhyLabs credentials as environment variables. The WhyLabs Writer will check for the existence of these variables in order to send the profiles to your dashboard." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LgyC7GsWCzO1", + "outputId": "390dd873-7bfe-4fcd-d360-c3280f59c935" + }, + "outputs": [], + "source": [ + "import whylogs as why\n", + "import getpass\n", + "import os\n", + "\n", + "# We don't need to see the autogenerated upload links in this notebook\n", + "os.environ[\"WHYLOGS_SUPPRESS_LOG_OUTPUT\"] = \"True\"\n", + "\n", + "why.init(allow_anonymous=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m6BNxmRm8_gi" + }, + "source": [ + "## Defining the images\n", + "\n", + "For this example, we will create two images with the PIL library. We will log each image in single profiles in order to upload them to WhyLabs." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 529 + }, + "id": "4zHh_a7b8Pgv", + "outputId": "977b6d0a-3b3c-41eb-b4cd-9b1471002417" + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from PIL import Image\n", + "\n", + "img1 = Image.effect_mandelbrot((256, 256), (-3, -2.5, 2, 2.5), 9)\n", + "img2 = Image.effect_mandelbrot((256, 256), (-3, -2.5, 2, 2.5), 20)\n", + "\n", + "display(img1)\n", + "display(img2)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "h6DN2Jdc9YS7" + }, + "source": [ + "We will use the concept of trace id's to be able to trace our single image profiles. Let's generate uuids for both images and use the identifiers as our trace IDs.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Q5s-rTlKOTnR", + "outputId": "c4666b13-9ac9-4b1e-92f9-108c0ec96a65" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "trace ids generated: b8f567cd-779e-400d-a99c-a737a4257332, and b393df6b-bde0-45c2-8c5d-b304d8f27d1a\n" + ] + } + ], + "source": [ + "import uuid\n", + "\n", + "# Here we show that you can pass in your own string for a unique identifier as a trace_id\n", + "trace_id1 = str(uuid.uuid4())\n", + "trace_id2 = str(uuid.uuid4())\n", + "print(f\"trace ids generated: {trace_id1}, and {trace_id2}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jxauTm2v-P0b" + }, + "source": [ + "We can now call `log_image` to create profiles for each image. We will set the profile's trace ID by passing the uuids into each profile's metadada:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lUdxL7EGOaWv", + "outputId": "16c96e86-098a-4399-dd08-034cb06b976c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'whylabs.traceId': 'b8f567cd-779e-400d-a99c-a737a4257332', 'whylogs.creationTimestamp': '1694127199095'}\n", + "{'whylabs.traceId': 'b393df6b-bde0-45c2-8c5d-b304d8f27d1a', 'whylogs.creationTimestamp': '1694127199811'}\n" + ] + } + ], + "source": [ + "from whylogs.extras.image_metric import log_image\n", + "\n", + "profile1 = log_image(img1, trace_id=trace_id1).profile()\n", + "profile2 = log_image(img2, trace_id=trace_id2).profile()\n", + "\n", + "print(profile1.view()._metadata)\n", + "print(profile2.view()._metadata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jfNXAQ-gFFzT" + }, + "source": [ + "## Retrieving the Profiles\n", + "\n", + "To retrieve the profiles, we need to provide the proper Trace ID and make use of the whylabs client for the `get_profile_traces` API. Let's wrap the necessary steps into a single function to retrieve the profiles based on the WhyLabs writer and trace id:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lApPPk7QO4fR", + "outputId": "c0344eea-ef61-4fb5-869d-2cfea5418c9a" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloaded profiles with trace id {'whylogs.creationTimestamp': '1694127199095', 'whylabs.traceId': 'b8f567cd-779e-400d-a99c-a737a4257332'}, {'whylogs.creationTimestamp': '1694127199811', 'whylabs.traceId': 'b393df6b-bde0-45c2-8c5d-b304d8f27d1a'}\n" + ] + } + ], + "source": [ + "from whylogs.api.writer.whylabs import WhyLabsWriter\n", + "from whylabs_client.api.dataset_profile_api import DatasetProfileApi\n", + "from whylabs_client.model.profile_traces_response import ProfileTracesResponse\n", + "from whylogs.core import DatasetProfileView\n", + "import time\n", + "\n", + "whylabs_writer = WhyLabsWriter()\n", + "\n", + "def get_profile_from_whylabs(writer, trace_id:str):\n", + " dataset_api = DatasetProfileApi(writer._api_client)\n", + " download_url = None\n", + " for _ in range(15):\n", + " response: ProfileTracesResponse = dataset_api.get_profile_traces(\n", + " org_id=writer._org_id,\n", + " dataset_id=writer._dataset_id,\n", + " trace_id=trace_id,\n", + " )\n", + " traces = response.get(\"traces\")\n", + " if len(traces) == 0:\n", + " time.sleep(2)\n", + " else:\n", + " download_url = traces[0][\"download_url\"]\n", + " break\n", + " if download_url is None:\n", + " raise Exception(f\"No results found for trace id {trace_id}\")\n", + " headers = {\"Content-Type\": \"application/octet-stream\"}\n", + " downloaded_profile = writer._s3_pool.request(\"GET\", download_url, headers=headers, timeout=writer._timeout_seconds)\n", + " deserialized_view = DatasetProfileView.deserialize(downloaded_profile.data)\n", + " return deserialized_view\n", + "\n", + "downloaded_view_1 = get_profile_from_whylabs(whylabs_writer, trace_id1)\n", + "downloaded_view_2 = get_profile_from_whylabs(whylabs_writer, trace_id2)\n", + "\n", + "print(f\"Downloaded profiles with trace id {downloaded_view_1._metadata}, {downloaded_view_2._metadata}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ITtnwObQFLCk" + }, + "source": [ + "## Comparing the Profiles\n", + "\n", + "Let's make sure that the retrieved profiles are indeed equal to the uploaded ones. To do so, let's visualize each profile for the first image with the __Notebook Profile Visualizer__. This will give us a profile visualization with the calculated metrics for the image.\n", + "\n", + "Here's the report for the original profile:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "NBjHjqI0109t", + "outputId": "edd513bf-e4df-4def-9f25-38f344c64eea" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from whylogs.viz import NotebookProfileVisualizer\n", + "\n", + "visualization = NotebookProfileVisualizer()\n", + "visualization.set_profiles(target_profile_view=profile1.view())\n", + "\n", + "visualization.profile_summary()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Oz8QkkGuFl1D" + }, + "source": [ + "And this is the report for the retrieved profile. We should see the same features and values in both profiles." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "jw1Dw_rA47KE", + "outputId": "6026b4e9-2ffe-497b-d08e-cb6d5e439706" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "visualization.set_profiles(target_profile_view=downloaded_view_1)\n", + "\n", + "visualization.profile_summary()" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + }, + "vscode": { + "interpreter": { + "hash": "d39f874c9b8a97550ecbd783714b95e79c9b905449b34f44c40e3bf053b54b41" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/python/test_notebooks/notebook_tests.py b/python/test_notebooks/notebook_tests.py index b8ef7bc1b6..eb276a8aee 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", "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 04dc1d54a0..65ec9c83bf 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