From a738d80262d2fb1b290d22065fe1c2d06bae494e Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 16:32:42 -0700 Subject: [PATCH 1/8] Add image transformer Signed-off-by: Kevin Su --- flytekit/core/type_engine.py | 3 + flytekit/types/file/image.py | 82 +++++++++++++++++++ .../flytekitplugins/deck/renderer.py | 1 + 3 files changed, 86 insertions(+) create mode 100644 flytekit/types/file/image.py diff --git a/flytekit/core/type_engine.py b/flytekit/core/type_engine.py index 7b4f17eda5..bb0f56da50 100644 --- a/flytekit/core/type_engine.py +++ b/flytekit/core/type_engine.py @@ -874,6 +874,9 @@ def lazy_import_transformers(cls): register_bigquery_handlers() if is_imported("numpy"): from flytekit.types import numpy # noqa: F401 + if is_imported("PIL"): + print("PIL is imported") + from flytekit.types.file import image # noqa: F401 @classmethod def to_literal_type(cls, python_type: Type) -> LiteralType: diff --git a/flytekit/types/file/image.py b/flytekit/types/file/image.py new file mode 100644 index 0000000000..178db52e78 --- /dev/null +++ b/flytekit/types/file/image.py @@ -0,0 +1,82 @@ +import pathlib +import typing +from typing import Dict, Tuple, Type + +import PIL.Image + +from flytekit.core.context_manager import FlyteContext +from flytekit.core.type_engine import TypeEngine, TypeTransformer, TypeTransformerFailedError +from flytekit.models.core import types as _core_types +from flytekit.models.literals import Blob, BlobMetadata, Literal, Scalar +from flytekit.models.types import LiteralType + + +T = typing.TypeVar("T") + + +class PILImageTransformer(TypeTransformer[T]): + """ + TypeTransformer that supports np.ndarray as a native type. + """ + + FILE_FORMAT = "image" + + def __init__(self): + super().__init__(name="PIL.Image", t=PIL.Image.Image) + + def get_literal_type(self, t: Type[T]) -> LiteralType: + return LiteralType( + blob=_core_types.BlobType( + format=self.FILE_FORMAT, dimensionality=_core_types.BlobType.BlobDimensionality.SINGLE + ) + ) + + def to_literal( + self, ctx: FlyteContext, python_val: PIL.Image.Image, python_type: Type[T], expected: LiteralType + ) -> Literal: + + meta = BlobMetadata( + type=_core_types.BlobType( + format=self.FILE_FORMAT, dimensionality=_core_types.BlobType.BlobDimensionality.SINGLE + ) + ) + + local_path = ctx.file_access.get_random_local_path() + ".png" + pathlib.Path(local_path).parent.mkdir(parents=True, exist_ok=True) + print(local_path) + python_val.save(local_path) + + remote_path = ctx.file_access.get_random_remote_path(local_path) + ctx.file_access.put_data(local_path, remote_path, is_multipart=False) + return Literal(scalar=Scalar(blob=Blob(metadata=meta, uri=remote_path))) + + def to_python_value(self, ctx: FlyteContext, lv: Literal, expected_python_type: Type[T]) -> PIL.Image.Image: + try: + uri = lv.scalar.blob.uri + except AttributeError: + raise TypeTransformerFailedError(f"Cannot convert from {lv} to {expected_python_type}") + + local_path = ctx.file_access.get_random_local_path() + ctx.file_access.get_data(uri, local_path, is_multipart=False) + + return PIL.Image.open(local_path) + + def guess_python_type(self, literal_type: LiteralType) -> Type[T]: + if ( + literal_type.blob is not None + and literal_type.blob.dimensionality == _core_types.BlobType.BlobDimensionality.SINGLE + and literal_type.blob.format == self.FILE_FORMAT + ): + return PIL.Image.Image + + raise ValueError(f"Transformer {self} cannot reverse {literal_type}") + + def to_html(self, ctx: FlyteContext, python_val: PIL.Image.Image, expected_python_type: Type[T]) -> str: + try: + from flytekitplugins.deck import ImageRenderer + except ImportError: + return "failed" + return ImageRenderer().to_html(image_src=python_val) + + +TypeEngine.register(PILImageTransformer()) diff --git a/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py b/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py index c090ea6a46..551d30b5ae 100644 --- a/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py +++ b/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py @@ -7,6 +7,7 @@ import markdown import pandas as pd import PIL + import PIL.Image import plotly.express as px else: pd = lazy_module("pandas") From da9f6645bb5e99580e38d89ef6824b81a1054279 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 16:49:16 -0700 Subject: [PATCH 2/8] Add tests Signed-off-by: Kevin Su --- flytekit/core/type_engine.py | 1 - flytekit/types/file/image.py | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flytekit/core/type_engine.py b/flytekit/core/type_engine.py index bb0f56da50..c30a919f2f 100644 --- a/flytekit/core/type_engine.py +++ b/flytekit/core/type_engine.py @@ -875,7 +875,6 @@ def lazy_import_transformers(cls): if is_imported("numpy"): from flytekit.types import numpy # noqa: F401 if is_imported("PIL"): - print("PIL is imported") from flytekit.types.file import image # noqa: F401 @classmethod diff --git a/flytekit/types/file/image.py b/flytekit/types/file/image.py index 178db52e78..dc1fba63b4 100644 --- a/flytekit/types/file/image.py +++ b/flytekit/types/file/image.py @@ -1,6 +1,6 @@ import pathlib import typing -from typing import Dict, Tuple, Type +from typing import Type import PIL.Image @@ -72,11 +72,12 @@ def guess_python_type(self, literal_type: LiteralType) -> Type[T]: raise ValueError(f"Transformer {self} cannot reverse {literal_type}") def to_html(self, ctx: FlyteContext, python_val: PIL.Image.Image, expected_python_type: Type[T]) -> str: - try: - from flytekitplugins.deck import ImageRenderer - except ImportError: - return "failed" - return ImageRenderer().to_html(image_src=python_val) + import base64 + from io import BytesIO + buffered = BytesIO() + python_val.save(buffered, format="PNG") + img_base64 = base64.b64encode(buffered.getvalue()).decode() + return f'Rendered Image' TypeEngine.register(PILImageTransformer()) From 0f3282254ff3ea6623f9ec2e9c60e1eb3a11b0ad Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 16:59:28 -0700 Subject: [PATCH 3/8] updated tests Signed-off-by: Kevin Su --- tests/flytekit/unit/types/file/test_image.py | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/flytekit/unit/types/file/test_image.py diff --git a/tests/flytekit/unit/types/file/test_image.py b/tests/flytekit/unit/types/file/test_image.py new file mode 100644 index 0000000000..c5772d3647 --- /dev/null +++ b/tests/flytekit/unit/types/file/test_image.py @@ -0,0 +1,29 @@ +import io + +import requests + +from flytekit import task, workflow +import PIL.Image + + +@task(disable_deck=False) +def t1() -> PIL.Image.Image: + url = "https://miro.medium.com/v2/resize:fit:1400/1*0T9PjBnJB9H0Y4qrllkJtQ.png" + response = requests.get(url) + image_bytes = io.BytesIO(response.content) + im = PIL.Image.open(image_bytes) + return im + + +@task +def t2(im: PIL.Image.Image) -> PIL.Image.Image: + return im + + +@workflow +def wf(): + t2(im=t1()) + + +def test_image_transformer(): + wf() From 992410ca78b448bad66c1ab13d78c25926e43783 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 17:02:38 -0700 Subject: [PATCH 4/8] nit Signed-off-by: Kevin Su --- flytekit/types/file/image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flytekit/types/file/image.py b/flytekit/types/file/image.py index dc1fba63b4..d24d48760e 100644 --- a/flytekit/types/file/image.py +++ b/flytekit/types/file/image.py @@ -43,7 +43,6 @@ def to_literal( local_path = ctx.file_access.get_random_local_path() + ".png" pathlib.Path(local_path).parent.mkdir(parents=True, exist_ok=True) - print(local_path) python_val.save(local_path) remote_path = ctx.file_access.get_random_remote_path(local_path) From 0179767e82f7c451c324acee36b59250f5714e52 Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 17:27:16 -0700 Subject: [PATCH 5/8] updated tests Signed-off-by: Kevin Su --- flytekit/types/file/image.py | 4 ++-- tests/flytekit/unit/types/file/test_image.py | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/flytekit/types/file/image.py b/flytekit/types/file/image.py index d24d48760e..22d8f3b484 100644 --- a/flytekit/types/file/image.py +++ b/flytekit/types/file/image.py @@ -16,10 +16,10 @@ class PILImageTransformer(TypeTransformer[T]): """ - TypeTransformer that supports np.ndarray as a native type. + TypeTransformer that supports PIL.Image as a native type. """ - FILE_FORMAT = "image" + FILE_FORMAT = "PIL.Image" def __init__(self): super().__init__(name="PIL.Image", t=PIL.Image.Image) diff --git a/tests/flytekit/unit/types/file/test_image.py b/tests/flytekit/unit/types/file/test_image.py index c5772d3647..60a36544ef 100644 --- a/tests/flytekit/unit/types/file/test_image.py +++ b/tests/flytekit/unit/types/file/test_image.py @@ -1,18 +1,10 @@ -import io - -import requests - from flytekit import task, workflow import PIL.Image @task(disable_deck=False) def t1() -> PIL.Image.Image: - url = "https://miro.medium.com/v2/resize:fit:1400/1*0T9PjBnJB9H0Y4qrllkJtQ.png" - response = requests.get(url) - image_bytes = io.BytesIO(response.content) - im = PIL.Image.open(image_bytes) - return im + return PIL.Image.new("L", (100, 100), "black") @task From e8badeadb1305b55ee5c9a81a99ea2a0d222933b Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 17:42:35 -0700 Subject: [PATCH 6/8] lint Signed-off-by: Kevin Su --- flytekit/types/file/image.py | 2 +- tests/flytekit/unit/types/file/test_image.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flytekit/types/file/image.py b/flytekit/types/file/image.py index 22d8f3b484..d26389cf99 100644 --- a/flytekit/types/file/image.py +++ b/flytekit/types/file/image.py @@ -10,7 +10,6 @@ from flytekit.models.literals import Blob, BlobMetadata, Literal, Scalar from flytekit.models.types import LiteralType - T = typing.TypeVar("T") @@ -73,6 +72,7 @@ def guess_python_type(self, literal_type: LiteralType) -> Type[T]: def to_html(self, ctx: FlyteContext, python_val: PIL.Image.Image, expected_python_type: Type[T]) -> str: import base64 from io import BytesIO + buffered = BytesIO() python_val.save(buffered, format="PNG") img_base64 = base64.b64encode(buffered.getvalue()).decode() diff --git a/tests/flytekit/unit/types/file/test_image.py b/tests/flytekit/unit/types/file/test_image.py index 60a36544ef..5163162e00 100644 --- a/tests/flytekit/unit/types/file/test_image.py +++ b/tests/flytekit/unit/types/file/test_image.py @@ -1,6 +1,7 @@ -from flytekit import task, workflow import PIL.Image +from flytekit import task, workflow + @task(disable_deck=False) def t1() -> PIL.Image.Image: From e8a658ed0c89735823d012ed322fb6930afbba1b Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 17:47:07 -0700 Subject: [PATCH 7/8] pillow Signed-off-by: Kevin Su --- dev-requirements.in | 1 + plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.in b/dev-requirements.in index 3cb16d8d3b..8c968cd54b 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -26,6 +26,7 @@ torch<=1.12.1; python_version<'3.11' # pytorch 2 supports python 3.11 torch<=2.0.0; python_version>='3.11' or platform_system!='Windows' +pillow scikit-learn types-protobuf types-croniter diff --git a/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py b/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py index 551d30b5ae..b71ee832bf 100644 --- a/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py +++ b/plugins/flytekit-deck-standard/flytekitplugins/deck/renderer.py @@ -6,7 +6,6 @@ if TYPE_CHECKING: import markdown import pandas as pd - import PIL import PIL.Image import plotly.express as px else: From bfa45972f5d094f34f45a2f851870226256420ac Mon Sep 17 00:00:00 2001 From: Kevin Su Date: Wed, 18 Oct 2023 19:56:23 -0700 Subject: [PATCH 8/8] use enable_deck=True Signed-off-by: Kevin Su --- flytekit/experimental/eager_function.py | 2 +- plugins/flytekit-kf-pytorch/tests/test_elastic_task.py | 2 +- plugins/flytekit-mlflow/README.md | 2 +- plugins/flytekit-mlflow/tests/test_mlflow_tracking.py | 2 +- tests/flytekit/unit/types/file/test_image.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flytekit/experimental/eager_function.py b/flytekit/experimental/eager_function.py index 264d0d641a..e0f252e312 100644 --- a/flytekit/experimental/eager_function.py +++ b/flytekit/experimental/eager_function.py @@ -531,7 +531,7 @@ async def wrapper(*args, **kws): return task( wrapper, secret_requests=secret_requests, - disable_deck=False, + enable_deck=True, execution_mode=PythonFunctionTask.ExecutionBehavior.EAGER, **kwargs, ) diff --git a/plugins/flytekit-kf-pytorch/tests/test_elastic_task.py b/plugins/flytekit-kf-pytorch/tests/test_elastic_task.py index bced35a6df..8b05342846 100644 --- a/plugins/flytekit-kf-pytorch/tests/test_elastic_task.py +++ b/plugins/flytekit-kf-pytorch/tests/test_elastic_task.py @@ -120,7 +120,7 @@ def test_deck(start_method: str) -> None: @task( task_config=Elastic(nnodes=1, nproc_per_node=world_size, start_method=start_method), - disable_deck=False, + enable_deck=True, ) def train(): import os diff --git a/plugins/flytekit-mlflow/README.md b/plugins/flytekit-mlflow/README.md index 6cbee9cf59..6a9a794a9f 100644 --- a/plugins/flytekit-mlflow/README.md +++ b/plugins/flytekit-mlflow/README.md @@ -15,7 +15,7 @@ from flytekit import task, workflow from flytekitplugins.mlflow import mlflow_autolog import mlflow -@task(disable_deck=False) +@task(enable_deck=True) @mlflow_autolog(framework=mlflow.keras) def train_model(): ... diff --git a/plugins/flytekit-mlflow/tests/test_mlflow_tracking.py b/plugins/flytekit-mlflow/tests/test_mlflow_tracking.py index 613cbfcd76..3605c7ee2f 100644 --- a/plugins/flytekit-mlflow/tests/test_mlflow_tracking.py +++ b/plugins/flytekit-mlflow/tests/test_mlflow_tracking.py @@ -6,7 +6,7 @@ from flytekit import task -@task(disable_deck=False) +@task(enable_deck=True) @mlflow_autolog(framework=mlflow.keras) def train_model(epochs: int): fashion_mnist = tf.keras.datasets.fashion_mnist diff --git a/tests/flytekit/unit/types/file/test_image.py b/tests/flytekit/unit/types/file/test_image.py index 5163162e00..8f7469a457 100644 --- a/tests/flytekit/unit/types/file/test_image.py +++ b/tests/flytekit/unit/types/file/test_image.py @@ -3,7 +3,7 @@ from flytekit import task, workflow -@task(disable_deck=False) +@task(enable_deck=True) def t1() -> PIL.Image.Image: return PIL.Image.new("L", (100, 100), "black")