From 333c83140b1b5243bbe6f6db0765131ae50f9030 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Wed, 10 Nov 2021 12:28:12 +0100 Subject: [PATCH 01/29] took out that fullstop --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e82206e9abe..70a714cc92a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ ## What is ZenML? -Before: Sam struggles to productionalize ML | After: Sam finds Zen in her MLOps with ZenML. +Before: Sam struggles to productionalize ML | After: Sam finds Zen in her MLOps with ZenML :-------------------------:|:-------------------------: ![](docs/readme/sam_frustrated.jpg) | ![](docs/readme/sam_zen_mode.jpg) From 4fbeed7b2431b8011bfe52824747655837a74859 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 12 Nov 2021 14:31:44 +0100 Subject: [PATCH 02/29] Fianlly mute apache beam --- src/zenml/logger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/zenml/logger.py b/src/zenml/logger.py index a2cef782878..9cc29ea3b82 100644 --- a/src/zenml/logger.py +++ b/src/zenml/logger.py @@ -156,5 +156,8 @@ def init_logging() -> None: os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" set_root_verbosity() + # Mute apache_beam + logging.getLogger("apache_beam").setLevel(logging.WARNING) + # set absl logging absl_logging.set_verbosity(ABSL_LOGGING_VERBOSITY) From 571af369604b602a18dd5218cf0b03f3088e7aad Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 12 Nov 2021 14:33:44 +0100 Subject: [PATCH 03/29] rdbms_metadata_access_object muted --- src/zenml/logger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zenml/logger.py b/src/zenml/logger.py index 9cc29ea3b82..41daad27851 100644 --- a/src/zenml/logger.py +++ b/src/zenml/logger.py @@ -158,6 +158,7 @@ def init_logging() -> None: # Mute apache_beam logging.getLogger("apache_beam").setLevel(logging.WARNING) + logging.getLogger("rdbms_metadata_access_object").setLevel(logging.WARNING) # set absl logging absl_logging.set_verbosity(ABSL_LOGGING_VERBOSITY) From 5051a1fa4752c0812fe9bbe80e3f5a1e5cd595d5 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 12 Nov 2021 14:35:05 +0100 Subject: [PATCH 04/29] removed old api reference --- .../artifact_stores/base_artifact_store.md | 59 ---- .../artifact_stores/gcp_artifact_store.md | 30 -- .../api-reference/artifact_stores/index.md | 8 - .../artifact_stores/local_artifact_store.md | 30 -- .../api-reference/artifacts/base_artifact.md | 48 ---- .../api-reference/artifacts/data_artifact.md | 37 --- .../api-reference/artifacts/index.md | 8 - .../api-reference/artifacts/model_artifact.md | 32 --- docs/book/reference/api-reference/cli/base.md | 2 - docs/book/reference/api-reference/cli/cli.md | 4 - .../reference/api-reference/cli/config.md | 3 - .../reference/api-reference/cli/example.md | 2 - .../book/reference/api-reference/cli/index.md | 14 - .../reference/api-reference/cli/pipeline.md | 2 - .../book/reference/api-reference/cli/stack.md | 3 - docs/book/reference/api-reference/cli/step.md | 2 - .../book/reference/api-reference/cli/utils.md | 109 -------- .../reference/api-reference/cli/version.md | 2 - .../api-reference/config/constants.md | 2 - .../api-reference/config/global_config.md | 45 --- .../reference/api-reference/config/index.md | 7 - .../book/reference/api-reference/constants.md | 13 - .../api-reference/core/base_component.md | 69 ----- .../api-reference/core/component_factory.md | 37 --- .../reference/api-reference/core/constants.md | 2 - .../api-reference/core/git_wrapper.md | 92 ------ .../reference/api-reference/core/index.md | 14 - .../api-reference/core/local_service.md | 128 --------- .../api-reference/core/mapping_utils.md | 67 ----- .../book/reference/api-reference/core/repo.md | 65 ----- .../reference/api-reference/core/repo_test.md | 2 - .../reference/api-reference/core/utils.md | 35 --- docs/book/reference/api-reference/enums.md | 120 -------- .../reference/api-reference/exceptions.md | 85 ------ docs/book/reference/api-reference/index.md | 24 -- docs/book/reference/api-reference/io/index.md | 2 - docs/book/reference/api-reference/logger.md | 35 --- .../materializers/base_materializer.md | 49 ---- .../materializers/beam_materializer.md | 32 --- .../materializers/built_in_materializer.md | 27 -- .../default_materializer_registry.md | 47 ---- .../api-reference/materializers/index.md | 14 - .../materializers/keras_meterializer.md | 33 --- .../materializers/numpy_materializer.md | 30 -- .../materializers/pandas_materializer.md | 30 -- .../spec_materializer_registry.md | 28 -- .../materializers/tf_dataset_materializer.md | 27 -- .../metadata/base_metadata_store.md | 65 ----- .../reference/api-reference/metadata/index.md | 9 - .../metadata/mock_metadata_wrapper.md | 25 -- .../metadata/mysql_metadata_wrapper.md | 42 --- .../metadata/sqlite_metadata_wrapper.md | 33 --- .../airflow/airflow_dag_runner.md | 46 --- .../airflow/airflow_orchestrator.md | 29 -- .../orchestrators/airflow/index.md | 7 - .../orchestrators/base_orchestrator.md | 38 --- .../api-reference/orchestrators/index.md | 8 - .../orchestrators/local/index.md | 7 - .../orchestrators/local/local_dag_runner.md | 38 --- .../orchestrators/local/local_orchestrator.md | 29 -- .../api-reference/pipelines/base_pipeline.md | 42 --- .../api-reference/pipelines/index.md | 7 - .../pipelines/pipeline_decorator.md | 21 -- .../api-reference/post_execution/artifact.md | 45 --- .../api-reference/post_execution/index.md | 9 - .../api-reference/post_execution/pipeline.md | 34 --- .../post_execution/pipeline_run.md | 46 --- .../api-reference/post_execution/step.md | 85 ------ .../api-reference/stacks/base_stack.md | 61 ---- .../api-reference/stacks/constants.md | 2 - .../reference/api-reference/stacks/index.md | 7 - .../api-reference/steps/base_step.md | 68 ----- .../api-reference/steps/base_step_config.md | 17 -- .../reference/api-reference/steps/index.md | 11 - .../api-reference/steps/step_decorator.md | 20 -- .../steps/step_interfaces/base_interface.md | 2 - .../steps/step_interfaces/index.md | 6 - .../api-reference/steps/step_output.md | 13 - .../reference/api-reference/steps/utils.md | 34 --- .../api-reference/types/base_type.md | 14 - .../reference/api-reference/types/index.md | 7 - .../api-reference/types/pytorch_types.md | 25 -- .../api-reference/utils/analytics_utils.md | 49 ---- .../reference/api-reference/utils/index.md | 9 - .../api-reference/utils/path_utils.md | 264 ------------------ .../api-reference/utils/source_utils.md | 119 -------- .../api-reference/utils/yaml_utils.md | 60 ---- 87 files changed, 3009 deletions(-) delete mode 100644 docs/book/reference/api-reference/artifact_stores/base_artifact_store.md delete mode 100644 docs/book/reference/api-reference/artifact_stores/gcp_artifact_store.md delete mode 100644 docs/book/reference/api-reference/artifact_stores/index.md delete mode 100644 docs/book/reference/api-reference/artifact_stores/local_artifact_store.md delete mode 100644 docs/book/reference/api-reference/artifacts/base_artifact.md delete mode 100644 docs/book/reference/api-reference/artifacts/data_artifact.md delete mode 100644 docs/book/reference/api-reference/artifacts/index.md delete mode 100644 docs/book/reference/api-reference/artifacts/model_artifact.md delete mode 100644 docs/book/reference/api-reference/cli/base.md delete mode 100644 docs/book/reference/api-reference/cli/cli.md delete mode 100644 docs/book/reference/api-reference/cli/config.md delete mode 100644 docs/book/reference/api-reference/cli/example.md delete mode 100644 docs/book/reference/api-reference/cli/index.md delete mode 100644 docs/book/reference/api-reference/cli/pipeline.md delete mode 100644 docs/book/reference/api-reference/cli/stack.md delete mode 100644 docs/book/reference/api-reference/cli/step.md delete mode 100644 docs/book/reference/api-reference/cli/utils.md delete mode 100644 docs/book/reference/api-reference/cli/version.md delete mode 100644 docs/book/reference/api-reference/config/constants.md delete mode 100644 docs/book/reference/api-reference/config/global_config.md delete mode 100644 docs/book/reference/api-reference/config/index.md delete mode 100644 docs/book/reference/api-reference/constants.md delete mode 100644 docs/book/reference/api-reference/core/base_component.md delete mode 100644 docs/book/reference/api-reference/core/component_factory.md delete mode 100644 docs/book/reference/api-reference/core/constants.md delete mode 100644 docs/book/reference/api-reference/core/git_wrapper.md delete mode 100644 docs/book/reference/api-reference/core/index.md delete mode 100644 docs/book/reference/api-reference/core/local_service.md delete mode 100644 docs/book/reference/api-reference/core/mapping_utils.md delete mode 100644 docs/book/reference/api-reference/core/repo.md delete mode 100644 docs/book/reference/api-reference/core/repo_test.md delete mode 100644 docs/book/reference/api-reference/core/utils.md delete mode 100644 docs/book/reference/api-reference/enums.md delete mode 100644 docs/book/reference/api-reference/exceptions.md delete mode 100644 docs/book/reference/api-reference/index.md delete mode 100644 docs/book/reference/api-reference/io/index.md delete mode 100644 docs/book/reference/api-reference/logger.md delete mode 100644 docs/book/reference/api-reference/materializers/base_materializer.md delete mode 100644 docs/book/reference/api-reference/materializers/beam_materializer.md delete mode 100644 docs/book/reference/api-reference/materializers/built_in_materializer.md delete mode 100644 docs/book/reference/api-reference/materializers/default_materializer_registry.md delete mode 100644 docs/book/reference/api-reference/materializers/index.md delete mode 100644 docs/book/reference/api-reference/materializers/keras_meterializer.md delete mode 100644 docs/book/reference/api-reference/materializers/numpy_materializer.md delete mode 100644 docs/book/reference/api-reference/materializers/pandas_materializer.md delete mode 100644 docs/book/reference/api-reference/materializers/spec_materializer_registry.md delete mode 100644 docs/book/reference/api-reference/materializers/tf_dataset_materializer.md delete mode 100644 docs/book/reference/api-reference/metadata/base_metadata_store.md delete mode 100644 docs/book/reference/api-reference/metadata/index.md delete mode 100644 docs/book/reference/api-reference/metadata/mock_metadata_wrapper.md delete mode 100644 docs/book/reference/api-reference/metadata/mysql_metadata_wrapper.md delete mode 100644 docs/book/reference/api-reference/metadata/sqlite_metadata_wrapper.md delete mode 100644 docs/book/reference/api-reference/orchestrators/airflow/airflow_dag_runner.md delete mode 100644 docs/book/reference/api-reference/orchestrators/airflow/airflow_orchestrator.md delete mode 100644 docs/book/reference/api-reference/orchestrators/airflow/index.md delete mode 100644 docs/book/reference/api-reference/orchestrators/base_orchestrator.md delete mode 100644 docs/book/reference/api-reference/orchestrators/index.md delete mode 100644 docs/book/reference/api-reference/orchestrators/local/index.md delete mode 100644 docs/book/reference/api-reference/orchestrators/local/local_dag_runner.md delete mode 100644 docs/book/reference/api-reference/orchestrators/local/local_orchestrator.md delete mode 100644 docs/book/reference/api-reference/pipelines/base_pipeline.md delete mode 100644 docs/book/reference/api-reference/pipelines/index.md delete mode 100644 docs/book/reference/api-reference/pipelines/pipeline_decorator.md delete mode 100644 docs/book/reference/api-reference/post_execution/artifact.md delete mode 100644 docs/book/reference/api-reference/post_execution/index.md delete mode 100644 docs/book/reference/api-reference/post_execution/pipeline.md delete mode 100644 docs/book/reference/api-reference/post_execution/pipeline_run.md delete mode 100644 docs/book/reference/api-reference/post_execution/step.md delete mode 100644 docs/book/reference/api-reference/stacks/base_stack.md delete mode 100644 docs/book/reference/api-reference/stacks/constants.md delete mode 100644 docs/book/reference/api-reference/stacks/index.md delete mode 100644 docs/book/reference/api-reference/steps/base_step.md delete mode 100644 docs/book/reference/api-reference/steps/base_step_config.md delete mode 100644 docs/book/reference/api-reference/steps/index.md delete mode 100644 docs/book/reference/api-reference/steps/step_decorator.md delete mode 100644 docs/book/reference/api-reference/steps/step_interfaces/base_interface.md delete mode 100644 docs/book/reference/api-reference/steps/step_interfaces/index.md delete mode 100644 docs/book/reference/api-reference/steps/step_output.md delete mode 100644 docs/book/reference/api-reference/steps/utils.md delete mode 100644 docs/book/reference/api-reference/types/base_type.md delete mode 100644 docs/book/reference/api-reference/types/index.md delete mode 100644 docs/book/reference/api-reference/types/pytorch_types.md delete mode 100644 docs/book/reference/api-reference/utils/analytics_utils.md delete mode 100644 docs/book/reference/api-reference/utils/index.md delete mode 100644 docs/book/reference/api-reference/utils/path_utils.md delete mode 100644 docs/book/reference/api-reference/utils/source_utils.md delete mode 100644 docs/book/reference/api-reference/utils/yaml_utils.md diff --git a/docs/book/reference/api-reference/artifact_stores/base_artifact_store.md b/docs/book/reference/api-reference/artifact_stores/base_artifact_store.md deleted file mode 100644 index 9a2025b9f21..00000000000 --- a/docs/book/reference/api-reference/artifact_stores/base_artifact_store.md +++ /dev/null @@ -1,59 +0,0 @@ -Module zenml.artifact_stores.base_artifact_store -================================================ -Definition of an Artifact Store - -Classes -------- - -`BaseArtifactStore(**values: Any)` -: Base class for all ZenML Artifact Store. - - Every ZenML Artifact Store should override this class. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Descendants - - * zenml.artifact_stores.gcp_artifact_store.GCPArtifactStore - * zenml.artifact_stores.local_artifact_store.LocalArtifactStore - - ### Class variables - - `path: str` - : - - ### Static methods - - `get_component_name_from_uri(artifact_uri: str) ‑> str` - : Gets component name from artifact URI. - - Args: - artifact_uri: URI to artifact. - - Returns: - Name of the component (str). - - ### Methods - - `get_serialization_dir(self)` - : Gets the local path where artifacts are stored. - - `resolve_uri_locally(self, artifact_uri: str, path: str = None) ‑> str` - : Takes a URI that points within the artifact store, downloads the - URI locally, then returns local URI. - - Args: - artifact_uri: uri to artifact. - path: optional path to download to. If None, is inferred. - - Returns: - Locally resolved uri (str). \ No newline at end of file diff --git a/docs/book/reference/api-reference/artifact_stores/gcp_artifact_store.md b/docs/book/reference/api-reference/artifact_stores/gcp_artifact_store.md deleted file mode 100644 index 543eed61120..00000000000 --- a/docs/book/reference/api-reference/artifact_stores/gcp_artifact_store.md +++ /dev/null @@ -1,30 +0,0 @@ -Module zenml.artifact_stores.gcp_artifact_store -=============================================== - -Classes -------- - -`GCPArtifactStore(**values: Any)` -: Artifact Store for Google Cloud Storage based artifacts. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.artifact_stores.base_artifact_store.BaseArtifactStore - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `path: str` - : - - ### Static methods - - `must_be_gcs_path(v: str)` - : Validates that the path is a valid gcs path. \ No newline at end of file diff --git a/docs/book/reference/api-reference/artifact_stores/index.md b/docs/book/reference/api-reference/artifact_stores/index.md deleted file mode 100644 index 866266e1262..00000000000 --- a/docs/book/reference/api-reference/artifact_stores/index.md +++ /dev/null @@ -1,8 +0,0 @@ -Module zenml.artifact_stores -============================ - -Sub-modules ------------ -* [zenml.artifact_stores.base_artifact_store](/reference/zenml/artifact_stores/base_artifact_store.md) -* [zenml.artifact_stores.gcp_artifact_store](/reference/zenml/artifact_stores/gcp_artifact_store.md) -* [zenml.artifact_stores.local_artifact_store](/reference/zenml/artifact_stores/local_artifact_store.md) diff --git a/docs/book/reference/api-reference/artifact_stores/local_artifact_store.md b/docs/book/reference/api-reference/artifact_stores/local_artifact_store.md deleted file mode 100644 index d3c22d5a625..00000000000 --- a/docs/book/reference/api-reference/artifact_stores/local_artifact_store.md +++ /dev/null @@ -1,30 +0,0 @@ -Module zenml.artifact_stores.local_artifact_store -================================================= - -Classes -------- - -`LocalArtifactStore(**values: Any)` -: Artifact Store for local artifacts. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.artifact_stores.base_artifact_store.BaseArtifactStore - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `path: str` - : - - ### Static methods - - `must_be_local_path(v: str)` - : Validates that the path is a valid gcs path. \ No newline at end of file diff --git a/docs/book/reference/api-reference/artifacts/base_artifact.md b/docs/book/reference/api-reference/artifacts/base_artifact.md deleted file mode 100644 index 7c1075df808..00000000000 --- a/docs/book/reference/api-reference/artifacts/base_artifact.md +++ /dev/null @@ -1,48 +0,0 @@ -Module zenml.artifacts.base_artifact -==================================== - -Classes -------- - -`BaseArtifact(mlmd_artifact_type: Optional[ml_metadata.proto.metadata_store_pb2.ArtifactType] = None)` -: Base class for all ZenML artifacts. - - Every implementation of an artifact needs to inherit this class. - - While inheriting from this class there are a few things to consider: - - - Upon creation, each artifact class needs to be given a unique TYPE_NAME. - - Your artifact can feature different properties under the parameter - PROPERTIES which will be tracked throughout your pipeline runs. - - TODO [MEDIUM]: Write about the materializers - - Construct an instance of Artifact. - - Used by TFX internal implementation: create an empty Artifact with - type_name and optional split info specified. The remaining info will be - filled in during compiling and running time. The Artifact should be - transparent to end users and should not be initiated directly by pipeline - users. - - Args: - mlmd_artifact_type: Proto message defining the underlying ArtifactType. - Optional and intended for internal use. - - ### Ancestors (in MRO) - - * tfx.types.artifact.Artifact - * tfx.utils.json_utils.Jsonable - * abc.ABC - - ### Descendants - - * zenml.artifacts.data_artifact.DataArtifact - * zenml.artifacts.model_artifact.ModelArtifact - - ### Class variables - - `PROPERTIES` - : - - `TYPE_NAME` - : \ No newline at end of file diff --git a/docs/book/reference/api-reference/artifacts/data_artifact.md b/docs/book/reference/api-reference/artifacts/data_artifact.md deleted file mode 100644 index 457d5ed2d88..00000000000 --- a/docs/book/reference/api-reference/artifacts/data_artifact.md +++ /dev/null @@ -1,37 +0,0 @@ -Module zenml.artifacts.data_artifact -==================================== - -Classes -------- - -`DataArtifact(mlmd_artifact_type: Optional[ml_metadata.proto.metadata_store_pb2.ArtifactType] = None)` -: Base class for any ZenML data artifact - - The custom properties include a property to hold split names - - Construct an instance of Artifact. - - Used by TFX internal implementation: create an empty Artifact with - type_name and optional split info specified. The remaining info will be - filled in during compiling and running time. The Artifact should be - transparent to end users and should not be initiated directly by pipeline - users. - - Args: - mlmd_artifact_type: Proto message defining the underlying ArtifactType. - Optional and intended for internal use. - - ### Ancestors (in MRO) - - * zenml.artifacts.base_artifact.BaseArtifact - * tfx.types.artifact.Artifact - * tfx.utils.json_utils.Jsonable - * abc.ABC - - ### Class variables - - `PROPERTIES` - : - - `TYPE_NAME` - : \ No newline at end of file diff --git a/docs/book/reference/api-reference/artifacts/index.md b/docs/book/reference/api-reference/artifacts/index.md deleted file mode 100644 index 29a2211f120..00000000000 --- a/docs/book/reference/api-reference/artifacts/index.md +++ /dev/null @@ -1,8 +0,0 @@ -Module zenml.artifacts -====================== - -Sub-modules ------------ -* [zenml.artifacts.base_artifact](/reference/zenml/artifacts/base_artifact.md) -* [zenml.artifacts.data_artifact](/reference/zenml/artifacts/data_artifact.md) -* [zenml.artifacts.model_artifact](/reference/zenml/artifacts/model_artifact.md) diff --git a/docs/book/reference/api-reference/artifacts/model_artifact.md b/docs/book/reference/api-reference/artifacts/model_artifact.md deleted file mode 100644 index 7ba6b1c2d32..00000000000 --- a/docs/book/reference/api-reference/artifacts/model_artifact.md +++ /dev/null @@ -1,32 +0,0 @@ -Module zenml.artifacts.model_artifact -===================================== - -Classes -------- - -`ModelArtifact(mlmd_artifact_type: Optional[ml_metadata.proto.metadata_store_pb2.ArtifactType] = None)` -: Base class for any ZenML model artifact. - - Construct an instance of Artifact. - - Used by TFX internal implementation: create an empty Artifact with - type_name and optional split info specified. The remaining info will be - filled in during compiling and running time. The Artifact should be - transparent to end users and should not be initiated directly by pipeline - users. - - Args: - mlmd_artifact_type: Proto message defining the underlying ArtifactType. - Optional and intended for internal use. - - ### Ancestors (in MRO) - - * zenml.artifacts.base_artifact.BaseArtifact - * tfx.types.artifact.Artifact - * tfx.utils.json_utils.Jsonable - * abc.ABC - - ### Class variables - - `TYPE_NAME` - : \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/base.md b/docs/book/reference/api-reference/cli/base.md deleted file mode 100644 index 4181bad7609..00000000000 --- a/docs/book/reference/api-reference/cli/base.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.cli.base -===================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/cli.md b/docs/book/reference/api-reference/cli/cli.md deleted file mode 100644 index f551694e1ef..00000000000 --- a/docs/book/reference/api-reference/cli/cli.md +++ /dev/null @@ -1,4 +0,0 @@ -Module zenml.cli.cli -==================== -.. currentmodule:: ce_cli.cli -.. moduleauthor:: ZenML GmbH \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/config.md b/docs/book/reference/api-reference/cli/config.md deleted file mode 100644 index b090765cb8c..00000000000 --- a/docs/book/reference/api-reference/cli/config.md +++ /dev/null @@ -1,3 +0,0 @@ -Module zenml.cli.config -======================= -CLI for manipulating ZenML local and global config file. \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/example.md b/docs/book/reference/api-reference/cli/example.md deleted file mode 100644 index 55a9ea641aa..00000000000 --- a/docs/book/reference/api-reference/cli/example.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.cli.example -======================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/index.md b/docs/book/reference/api-reference/cli/index.md deleted file mode 100644 index d3718e7aea1..00000000000 --- a/docs/book/reference/api-reference/cli/index.md +++ /dev/null @@ -1,14 +0,0 @@ -Module zenml.cli -================ - -Sub-modules ------------ -* [zenml.cli.base](/reference/zenml/cli/base.md) -* [zenml.cli.cli](/reference/zenml/cli/cli.md) -* [zenml.cli.config](/reference/zenml/cli/config.md) -* [zenml.cli.example](/reference/zenml/cli/example.md) -* [zenml.cli.pipeline](/reference/zenml/cli/pipeline.md) -* [zenml.cli.stack](/reference/zenml/cli/stack.md) -* [zenml.cli.step](/reference/zenml/cli/step.md) -* [zenml.cli.utils](/reference/zenml/cli/utils.md) -* [zenml.cli.version](/reference/zenml/cli/version.md) diff --git a/docs/book/reference/api-reference/cli/pipeline.md b/docs/book/reference/api-reference/cli/pipeline.md deleted file mode 100644 index e6fccbe9c49..00000000000 --- a/docs/book/reference/api-reference/cli/pipeline.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.cli.pipeline -========================= \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/stack.md b/docs/book/reference/api-reference/cli/stack.md deleted file mode 100644 index e7b74b9b1fc..00000000000 --- a/docs/book/reference/api-reference/cli/stack.md +++ /dev/null @@ -1,3 +0,0 @@ -Module zenml.cli.stack -====================== -CLI for manipulating ZenML local and global config file. \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/step.md b/docs/book/reference/api-reference/cli/step.md deleted file mode 100644 index 819f4eed448..00000000000 --- a/docs/book/reference/api-reference/cli/step.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.cli.step -===================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/utils.md b/docs/book/reference/api-reference/cli/utils.md deleted file mode 100644 index d2fabd65fe7..00000000000 --- a/docs/book/reference/api-reference/cli/utils.md +++ /dev/null @@ -1,109 +0,0 @@ -Module zenml.cli.utils -====================== - -Functions ---------- - - -`confirmation(text: str, *args, **kwargs) ‑> bool` -: Echo a confirmation string on the CLI. - - Args: - text: Input text string. - *args: Args to be passed to click.confirm(). - **kwargs: Kwargs to be passed to click.confirm(). - - Returns: - Boolean based on user response. - - -`declare(text: str)` -: Echo a declaration on the CLI. - - Args: - text: Input text string. - - -`echo_component_list(component_list: Dict[str, zenml.core.base_component.BaseComponent])` -: Echoes a list of components in a pretty style. - - -`error(text: str)` -: Echo an error string on the CLI. - - Args: - text: Input text string. - - Raises: - click.ClickException when called. - - -`format_date(dt: datetime.datetime, format: str = '%Y-%m-%d %H:%M:%S') ‑> str` -: Format a date into a string. - - Args: - dt: Datetime object to be formatted. - format: The format in string you want the datetime formatted to. - - Returns: - Formatted string according to specification. - - -`format_timedelta(td: datetime.timedelta) ‑> str` -: Format a timedelta into a string. - - Args: - td: datetime.timedelta object to be formatted. - - Returns: - Formatted string according to specification. - - -`notice(text: str)` -: Echo a notice string on the CLI. - - Args: - text: Input text string. - - -`parse_unknown_options(args: List[str]) ‑> Dict[str, Any]` -: Parse unknown options from the cli. - - Args: - args: A list of strings from the CLI. - - Returns: - Dict of parsed args. - - -`pretty_print(obj: Any)` -: Pretty print an object on the CLI. - - Args: - obj: Any object with a __str__ method defined. - - -`question(text: str, *args, **kwargs) ‑> Any` -: Echo a question string on the CLI. - - Args: - text: Input text string. - *args: Args to be passed to click.prompt(). - **kwargs: Kwargs to be passed to click.prompt(). - - Returns: - The answer to the question of any type, usually string. - - -`title(text: str)` -: Echo a title formatted string on the CLI. - - Args: - text: Input text string. - - -`warning(text: str)` -: Echo a warning string on the CLI. - - Args: - text: Input text string. \ No newline at end of file diff --git a/docs/book/reference/api-reference/cli/version.md b/docs/book/reference/api-reference/cli/version.md deleted file mode 100644 index 6f00ead8368..00000000000 --- a/docs/book/reference/api-reference/cli/version.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.cli.version -======================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/config/constants.md b/docs/book/reference/api-reference/config/constants.md deleted file mode 100644 index dc0653f2bb0..00000000000 --- a/docs/book/reference/api-reference/config/constants.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.config.constants -============================= \ No newline at end of file diff --git a/docs/book/reference/api-reference/config/global_config.md b/docs/book/reference/api-reference/config/global_config.md deleted file mode 100644 index 63a5da5b7ea..00000000000 --- a/docs/book/reference/api-reference/config/global_config.md +++ /dev/null @@ -1,45 +0,0 @@ -Module zenml.config.global_config -================================= -Global config for the ZenML installation. - -Classes -------- - -`GlobalConfig(**data: Any)` -: Class definition for the global config. - - Defines global data such as unique user ID and whether they opted in - for analytics. - - We persist the attributes in the config file. For the global - config, we want to persist the data as soon as it is initialized for - the first time. - - ### Ancestors (in MRO) - - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `analytics_opt_in: bool` - : - - `repo_active_stacks: Optional[Dict[str, str]]` - : - - `user_id: uuid.UUID` - : - - ### Methods - - `get_serialization_dir(self) ‑> str` - : Gets the global config dir for installed package. - - `get_serialization_file_name(self) ‑> str` - : Gets the global config dir for installed package. - - `make_stack_active_for_repo(self, repo_path: str, stack_key: str)` - : Sets the active stack for a specific repository. \ No newline at end of file diff --git a/docs/book/reference/api-reference/config/index.md b/docs/book/reference/api-reference/config/index.md deleted file mode 100644 index fdbafadaba9..00000000000 --- a/docs/book/reference/api-reference/config/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.config -=================== - -Sub-modules ------------ -* [zenml.config.constants](/reference/zenml/config/constants.md) -* [zenml.config.global_config](/reference/zenml/config/global_config.md) diff --git a/docs/book/reference/api-reference/constants.md b/docs/book/reference/api-reference/constants.md deleted file mode 100644 index 91df4587f44..00000000000 --- a/docs/book/reference/api-reference/constants.md +++ /dev/null @@ -1,13 +0,0 @@ -Module zenml.constants -====================== - -Functions ---------- - - -`handle_bool_env_var(var: str, default=False)` -: Converts normal env var to boolean - - -`handle_int_env_var(var: str, default: int = 0)` -: Converts normal env var to int \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/base_component.md b/docs/book/reference/api-reference/core/base_component.md deleted file mode 100644 index a250d5c374d..00000000000 --- a/docs/book/reference/api-reference/core/base_component.md +++ /dev/null @@ -1,69 +0,0 @@ -Module zenml.core.base_component -================================ - -Classes -------- - -`BaseComponent(**values: Any)` -: Class definition for the base config. - - The base component class defines the basic serialization / deserialization - of various components used in ZenML. The logic of the serialization / - deserialization is as follows: - - * If a `uuid` is passed in, then the object is read from a file, so the - constructor becomes a query for an object that is assumed to already been - serialized. - * If a 'uuid` is NOT passed, then a new object is created with the default - args (and any other args that are passed), and therefore a fresh - serialization takes place. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Descendants - - * zenml.artifact_stores.base_artifact_store.BaseArtifactStore - * zenml.config.global_config.GlobalConfig - * zenml.core.local_service.LocalService - * zenml.metadata.base_metadata_store.BaseMetadataStore - * zenml.orchestrators.base_orchestrator.BaseOrchestrator - - ### Class variables - - `Config` - : Configuration of settings. - - `uuid: Optional[uuid.UUID]` - : - - ### Methods - - `delete(self)` - : Deletes the persisted state of this object. - - `get_serialization_dir(self) ‑> str` - : Return the dir where object is serialized. - - `get_serialization_file_name(self) ‑> str` - : Return the name of the file where object is serialized. This - has a sane default in cases where uuid is not passed externally, and - therefore reading from a serialize file is not an option for the table. - However, we still this function to go through without an exception, - therefore the sane default. - - `get_serialization_full_path(self) ‑> str` - : Returns the full path of the serialization file. - - `update(self)` - : Persist the current state of the component. - - Calling this will result in a persistent, stateful change in the - system. \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/component_factory.md b/docs/book/reference/api-reference/core/component_factory.md deleted file mode 100644 index da779ca7e29..00000000000 --- a/docs/book/reference/api-reference/core/component_factory.md +++ /dev/null @@ -1,37 +0,0 @@ -Module zenml.core.component_factory -=================================== -Factory to register all components. - -Classes -------- - -`ComponentFactory(name: str)` -: Definition of ComponentFactory to track all BaseComponent subclasses. - - All BaseComponents (including custom ones) are to be - registered here. - - Constructor for the factory. - - Args: - name: Unique name for the factory. - - ### Methods - - `get_components(self) ‑> Dict[str, Type[zenml.core.base_component.BaseComponent]]` - : Return all components - - `get_single_component(self, key: str) ‑> Type[zenml.core.base_component.BaseComponent]` - : Get a registered component from a key. - - `register(self, name: str) ‑> Callable` - : Class decorator to register component classes to the internal registry. - - Args: - name: The name of the component. - - Returns: - A class decorator which registers the class at this ComponentFactory instance. - - `register_component(self, key: str, component: Type[zenml.core.base_component.BaseComponent])` - : Registers a single component class for a given key. \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/constants.md b/docs/book/reference/api-reference/core/constants.md deleted file mode 100644 index 02640cc7448..00000000000 --- a/docs/book/reference/api-reference/core/constants.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.core.constants -=========================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/git_wrapper.md b/docs/book/reference/api-reference/core/git_wrapper.md deleted file mode 100644 index 20706a9e4b6..00000000000 --- a/docs/book/reference/api-reference/core/git_wrapper.md +++ /dev/null @@ -1,92 +0,0 @@ -Module zenml.core.git_wrapper -============================= -Wrapper class to handle Git integration - -Classes -------- - -`GitWrapper(repo_path: str)` -: Wrapper class for Git. - - This class is responsible for handling git interactions, primarily - handling versioning of different steps in pipelines. - - Initialize GitWrapper. Should be initialize by ZenML Repository. - Args: - repo_path: - - Raises: - InvalidGitRepositoryError: If repository is not a git repository. - NoSuchPathError: If the repo_path does not exist. - - ### Methods - - `add_gitignore(self, items: List[str])` - : Adds `items` to .gitignore, if .gitignore exists. Otherwise creates - and adds. - - Args: - items (list[str]): Items to add. - - `check_file_committed(self, file_path: str) ‑> bool` - : Checks file is committed. If yes, return True, else False. - - Args: - file_path (str): Path to any file within the ZenML repo. - - `check_module_clean(self, source: str)` - : Returns True if all files within source's module are committed. - - Args: - source (str): relative module path pointing to a Class. - - `checkout(self, sha_or_branch: str = None, directory: str = None)` - : Wrapper for git checkout - - Args: - sha_or_branch: hex string of len 40 representing git sha OR - name of branch - directory (str): relative path to directory to scope checkout - - `get_current_sha(self) ‑> str` - : Finds the git sha that each file within the module is currently on. - - `is_valid_source(self, source: str) ‑> bool` - : Checks whether the source_path is valid or not. - - Args: - source (str): class_source e.g. this.module.Class[@pin]. - - `load_source_path_class(self, source: str) ‑> Type` - : Loads a Python class from the source. - - Args: - source: class_source e.g. this.module.Class[@sha] - - `reset(self, directory: str = None)` - : Wrapper for `git reset HEAD `. - - Args: - directory (str): relative path to directory to scope checkout - - `resolve_class(self, class_: Type) ‑> str` - : Resolves - Args: - class_: A Python Class reference. - - Returns: source_path e.g. this.module.Class[@pin]. - - `resolve_class_source(self, class_source: str) ‑> str` - : Resolves class_source with an optional pin. - Takes source (e.g. this.module.ClassName), and appends relevant - sha to it if the files within `module` are all committed. If even one - file is not committed, then returns `source` unchanged. - - Args: - class_source (str): class_source e.g. this.module.Class - - `stash(self)` - : Wrapper for git stash - - `stash_pop(self)` - : Wrapper for git stash pop. Only pops if there's something to pop. \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/index.md b/docs/book/reference/api-reference/core/index.md deleted file mode 100644 index d1de4ca8a88..00000000000 --- a/docs/book/reference/api-reference/core/index.md +++ /dev/null @@ -1,14 +0,0 @@ -Module zenml.core -================= - -Sub-modules ------------ -* [zenml.core.base_component](/reference/zenml/core/base_component.md) -* [zenml.core.component_factory](/reference/zenml/core/component_factory.md) -* [zenml.core.constants](/reference/zenml/core/constants.md) -* [zenml.core.git_wrapper](/reference/zenml/core/git_wrapper.md) -* [zenml.core.local_service](/reference/zenml/core/local_service.md) -* [zenml.core.mapping_utils](/reference/zenml/core/mapping_utils.md) -* [zenml.core.repo](/reference/zenml/core/repo.md) -* [zenml.core.repo_test](/reference/zenml/core/repo_test.md) -* [zenml.core.utils](/reference/zenml/core/utils.md) diff --git a/docs/book/reference/api-reference/core/local_service.md b/docs/book/reference/api-reference/core/local_service.md deleted file mode 100644 index 395b3008f4e..00000000000 --- a/docs/book/reference/api-reference/core/local_service.md +++ /dev/null @@ -1,128 +0,0 @@ -Module zenml.core.local_service -=============================== - -Classes -------- - -`LocalService(**values: Any)` -: Definition of a local service that keeps track of all ZenML - components. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `artifact_store_map: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]` - : - - `metadata_store_map: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]` - : - - `orchestrator_map: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]` - : - - `stacks: Dict[str, zenml.stacks.base_stack.BaseStack]` - : - - ### Instance variables - - `artifact_stores: Dict[str, zenml.artifact_stores.base_artifact_store.BaseArtifactStore]` - : Returns all registered artifact stores. - - `metadata_stores: Dict[str, zenml.metadata.base_metadata_store.BaseMetadataStore]` - : Returns all registered metadata stores. - - `orchestrators: Dict[str, zenml.orchestrators.base_orchestrator.BaseOrchestrator]` - : Returns all registered orchestrators. - - ### Methods - - `delete(self)` - : Deletes the entire service. Dangerous operation - - `delete_artifact_store(self, key: str)` - : Delete an artifact_store. - - Args: - key: Unique key of artifact_store. - - `delete_metadata_store(self, key: str)` - : Delete a metadata store. - - Args: - key: Unique key of metadata store. - - `delete_orchestrator(self, key: str)` - : Delete a orchestrator. - - Args: - key: Unique key of orchestrator. - - `delete_stack(self, key: str)` - : Delete a stack specified with a key. - - Args: - key: Unique key of stack. - - `get_artifact_store(self, key: str) ‑> zenml.artifact_stores.base_artifact_store.BaseArtifactStore` - : Return a single artifact store based on key. - - Args: - key: Unique key of artifact store. - - Returns: - Stack specified by key. - - `get_metadata_store(self, key: str) ‑> zenml.metadata.base_metadata_store.BaseMetadataStore` - : Return a single metadata store based on key. - - Args: - key: Unique key of metadata store. - - Returns: - Metadata store specified by key. - - `get_orchestrator(self, key: str) ‑> zenml.orchestrators.base_orchestrator.BaseOrchestrator` - : Return a single orchestrator based on key. - - Args: - key: Unique key of orchestrator. - - Returns: - Orchestrator specified by key. - - `get_serialization_dir(self) ‑> str` - : The local service stores everything in the zenml config dir. - - `get_serialization_file_name(self) ‑> str` - : Return the name of the file where object is serialized. - - `get_stack(self, key: str) ‑> zenml.stacks.base_stack.BaseStack` - : Return a single stack based on key. - - Args: - key: Unique key of stack. - - Returns: - Stack specified by key. - - `register_artifact_store(*args, **kwargs)` - : Inner decorator function. - - `register_metadata_store(*args, **kwargs)` - : Inner decorator function. - - `register_orchestrator(*args, **kwargs)` - : Inner decorator function. - - `register_stack(*args, **kwargs)` - : Inner decorator function. \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/mapping_utils.md b/docs/book/reference/api-reference/core/mapping_utils.md deleted file mode 100644 index 7c765a2c973..00000000000 --- a/docs/book/reference/api-reference/core/mapping_utils.md +++ /dev/null @@ -1,67 +0,0 @@ -Module zenml.core.mapping_utils -=============================== - -Functions ---------- - - -`get_component_from_key(key: str, mapping: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]) ‑> zenml.core.base_component.BaseComponent` -: Given a key and a mapping, return an initialized component. - - Args: - key: Unique key. - mapping: Dict of type str -> UUIDSourceTuple. - - Returns: - An object which is a subclass of type BaseComponent. - - -`get_components_from_store(store_name: str, mapping: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]) ‑> Dict[str, zenml.core.base_component.BaseComponent]` -: Returns a list of components from a store. - - Args: - store_name: Name of the store. - mapping: Dict of type str -> UUIDSourceTuple. - - Returns: - A dict of objects which are a subclass of type BaseComponent. - - -`get_key_from_uuid(uuid: uuid.UUID, mapping: Dict[str, zenml.core.mapping_utils.UUIDSourceTuple]) ‑> str` -: Return they key that points to a certain uuid in a mapping. - - Args: - uuid: uuid to query. - mapping: Dict mapping keys to UUIDs and source information. - - Returns: - Returns the key from the mapping. - -Classes -------- - -`UUIDSourceTuple(**data: Any)` -: Container used to store UUID and source information - of a single BaseComponent subclass. - - Attributes: - uuid: Identifier of the BaseComponent - source: Contains the fully qualified class name and information - about a git hash/tag. E.g. foo.bar.BaseComponentSubclass@git_tag - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `source: str` - : - - `uuid: uuid.UUID` - : \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/repo.md b/docs/book/reference/api-reference/core/repo.md deleted file mode 100644 index b7fe8bafd5a..00000000000 --- a/docs/book/reference/api-reference/core/repo.md +++ /dev/null @@ -1,65 +0,0 @@ -Module zenml.core.repo -====================== -Base ZenML repository - -Classes -------- - -`Repository(path: str = None)` -: ZenML repository definition. - - Every ZenML project exists inside a ZenML repository. - - Construct reference a ZenML repository. - - Args: - path (str): Path to root of repository - - ### Static methods - - `init_repo(repo_path: str = '/home/hamza/workspace/maiot/github_temp/zenml', stack: zenml.stacks.base_stack.BaseStack = None, analytics_opt_in: bool = None)` - : Initializes a git repo with zenml. - - Args: - repo_path (str): path to root of a git repo - stack: Initial stack. - analytics_opt_in: opt-in flag for analytics code. - - Raises: - InvalidGitRepositoryError: If repository is not a git repository. - NoSuchPathError: If the repo_path does not exist. - - ### Methods - - `clean(self)` - : Deletes associated metadata store, pipelines dir and artifacts - - `get_active_stack(self) ‑> zenml.stacks.base_stack.BaseStack` - : Get the active stack from global config. - - Returns: - Currently active stack. - - `get_active_stack_key(*args, **kwargs)` - : Inner decorator function. - - `get_git_wrapper(self) ‑> zenml.core.git_wrapper.GitWrapper` - : Returns the git wrapper for the repo. - - `get_pipeline(self, pipeline_name: str, stack_key: Optional[str] = None) ‑> Optional[zenml.post_execution.pipeline.PipelineView]` - : Returns a pipeline for the given name or `None` if it doesn't exist. - - Args: - pipeline_name: Name of the pipeline. - stack_key: If specified, pipelines in the metadata store of the - given stack are returned. Otherwise pipelines in the metadata - store of the currently active stack are returned. - - `get_pipelines(*args, **kwargs)` - : Inner decorator function. - - `get_service(self) ‑> zenml.core.local_service.LocalService` - : Returns the active service. For now, always local. - - `set_active_stack(*args, **kwargs)` - : Inner decorator function. \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/repo_test.md b/docs/book/reference/api-reference/core/repo_test.md deleted file mode 100644 index 6f70c7dc76f..00000000000 --- a/docs/book/reference/api-reference/core/repo_test.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.core.repo_test -=========================== \ No newline at end of file diff --git a/docs/book/reference/api-reference/core/utils.md b/docs/book/reference/api-reference/core/utils.md deleted file mode 100644 index 4be6b8babf2..00000000000 --- a/docs/book/reference/api-reference/core/utils.md +++ /dev/null @@ -1,35 +0,0 @@ -Module zenml.core.utils -======================= - -Functions ---------- - - -`define_json_config_settings_source(config_dir: str, config_name: str) ‑> Callable` -: Define a function to essentially deserialize a model from a serialized - json config. - - Args: - config_dir: A path to a dir where we want the config file to exist. - config_name: Full name of config file. - - Returns: - A `json_config_settings_source` callable reading from the passed path. - - -`generate_customise_sources(file_dir: str, file_name: str)` -: Generate a customise_sources function as defined here: - https://pydantic-docs.helpmanual.io/usage/settings/. This function - generates a function that configures the priorities of the sources through - which the model is loaded. The important thing to note here is that the - `define_json_config_settings_source` is dynamically generates with the - provided file_dir and file_name. This allows us to dynamically generate - a file name for the serialization and deserialization of the model. - - Args: - file_dir: Dir where file is stored. - file_name: Name of the file to persist. - - Returns: - A `customise_sources` class method to be defined the a Pydantic - BaseSettings inner Config class. \ No newline at end of file diff --git a/docs/book/reference/api-reference/enums.md b/docs/book/reference/api-reference/enums.md deleted file mode 100644 index d1e022a2f09..00000000000 --- a/docs/book/reference/api-reference/enums.md +++ /dev/null @@ -1,120 +0,0 @@ -Module zenml.enums -================== - -Classes -------- - -`ArtifactStoreTypes(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: All supported Artifact Store types. - - ### Ancestors (in MRO) - - * builtins.str - * enum.Enum - - ### Class variables - - `base` - : - - `gcp` - : - - `local` - : - -`ExecutionStatus(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: Enum that represents the current status of a step or pipeline run. - - ### Ancestors (in MRO) - - * enum.Enum - - ### Class variables - - `COMPLETED` - : - - `FAILED` - : - - `RUNNING` - : - -`LoggingLevels(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: An enumeration. - - ### Ancestors (in MRO) - - * enum.Enum - - ### Class variables - - `DEBUG` - : - - `ERROR` - : - - `INFO` - : - - `NOTSET` - : - - `WARN` - : - -`MLMetadataTypes(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: All supported ML Metadata types. - - ### Ancestors (in MRO) - - * builtins.str - * enum.Enum - - ### Class variables - - `base` - : - - `mock` - : - - `mysql` - : - - `sqlite` - : - -`OrchestratorTypes(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: All supported Orchestrator types - - ### Ancestors (in MRO) - - * builtins.str - * enum.Enum - - ### Class variables - - `airflow` - : - - `base` - : - - `local` - : - -`StackTypes(value, names=None, *, module=None, qualname=None, type=None, start=1)` -: All supported Stack types. - - ### Ancestors (in MRO) - - * builtins.str - * enum.Enum - - ### Class variables - - `base` - : \ No newline at end of file diff --git a/docs/book/reference/api-reference/exceptions.md b/docs/book/reference/api-reference/exceptions.md deleted file mode 100644 index fd371704b97..00000000000 --- a/docs/book/reference/api-reference/exceptions.md +++ /dev/null @@ -1,85 +0,0 @@ -Module zenml.exceptions -======================= -ZenML specific exception definitions - -Classes -------- - -`AlreadyExistsException(message: str = None, name: str = '', resource_type: str = '')` -: Raises exception when the `name` already exist in the system but an - action is trying to create a resource with the same name. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`ArtifactInterfaceError(*args, **kwargs)` -: Raises exception when interacting with the Artifact interface - in an unsupported way. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`DoesNotExistException(name: str = '', reason: str = '', message='{} does not exist! This might be due to: {}')` -: Raises exception when the `name` does not exist in the system but an - action is being done that requires it to be present. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`EmptyDatasourceException(message='This datasource has not been used in any pipelines, therefore the associated data has no versions. Please use this datasouce in any ZenML pipeline with `pipeline.add_datasource(datasource)`')` -: Raises exception when a datasource data is accessed without running - an associated pipeline. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`GitException(message: str = 'There is a problem with git resolution. Please make sure that all relevant files are committed.')` -: Raises exception when a problem occurs in git resolution. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`InitializationException(message='ZenML config is none. Did you do `zenml init`?')` -: Raises exception when a function is run before zenml initialization. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`PipelineInterfaceError(*args, **kwargs)` -: Raises exception when interacting with the Pipeline interface - in an unsupported way. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`PipelineNotSucceededException(name: str = '', message: str = '{} is not yet completed successfully.')` -: Raises exception when trying to fetch artifacts from a not succeeded - pipeline. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException - -`StepInterfaceError(*args, **kwargs)` -: Raises exception when interacting with the Step interface - in an unsupported way. - - ### Ancestors (in MRO) - - * builtins.Exception - * builtins.BaseException \ No newline at end of file diff --git a/docs/book/reference/api-reference/index.md b/docs/book/reference/api-reference/index.md deleted file mode 100644 index e79f4809639..00000000000 --- a/docs/book/reference/api-reference/index.md +++ /dev/null @@ -1,24 +0,0 @@ -Module zenml -============ - -Sub-modules ------------ -* [zenml.artifact_stores](/reference/zenml/artifact_stores/index.md) -* [zenml.artifacts](/reference/zenml/artifacts/index.md) -* [zenml.cli](/reference/zenml/cli/index.md) -* [zenml.config](/reference/zenml/config/index.md) -* [zenml.constants](/reference/zenml/constants.md) -* [zenml.core](/reference/zenml/core/index.md) -* [zenml.enums](/reference/zenml/enums.md) -* [zenml.exceptions](/reference/zenml/exceptions.md) -* [zenml.io](/reference/zenml/io/index.md) -* [zenml.logger](/reference/zenml/logger.md) -* [zenml.materializers](/reference/zenml/materializers/index.md) -* [zenml.metadata](/reference/zenml/metadata/index.md) -* [zenml.orchestrators](/reference/zenml/orchestrators/index.md) -* [zenml.pipelines](/reference/zenml/pipelines/index.md) -* [zenml.post_execution](/reference/zenml/post_execution/index.md) -* [zenml.stacks](/reference/zenml/stacks/index.md) -* [zenml.steps](/reference/zenml/steps/index.md) -* [zenml.types](/reference/zenml/types/index.md) -* [zenml.utils](/reference/zenml/utils/index.md) diff --git a/docs/book/reference/api-reference/io/index.md b/docs/book/reference/api-reference/io/index.md deleted file mode 100644 index 7e9846a4a79..00000000000 --- a/docs/book/reference/api-reference/io/index.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.io -=============== diff --git a/docs/book/reference/api-reference/logger.md b/docs/book/reference/api-reference/logger.md deleted file mode 100644 index 1f0f7cd7e44..00000000000 --- a/docs/book/reference/api-reference/logger.md +++ /dev/null @@ -1,35 +0,0 @@ -Module zenml.logger -=================== - -Functions ---------- - - -`get_console_handler() ‑> Any` -: Get console handler for logging. - - -`get_file_handler() ‑> Any` -: Return a file handler for logging. - - -`get_logger(logger_name) ‑> Any` -: Main function to get logger name,. - - Args: - logger_name: Name of logger to initialize. - - Returns: - A logger object. - - -`get_logging_level() ‑> zenml.enums.LoggingLevels` -: Get logging level from the env variable. - - -`init_logging()` -: Initialize logging with default levels. - - -`set_root_verbosity()` -: Set the root verbosity. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/base_materializer.md b/docs/book/reference/api-reference/materializers/base_materializer.md deleted file mode 100644 index ba62100177e..00000000000 --- a/docs/book/reference/api-reference/materializers/base_materializer.md +++ /dev/null @@ -1,49 +0,0 @@ -Module zenml.materializers.base_materializer -============================================ - -Classes -------- - -`BaseMaterializer(artifact: BaseArtifact)` -: Base Materializer to realize artifact data. - - Initializes a materializer with the given artifact. - - ### Descendants - - * zenml.materializers.beam_materializer.BeamMaterializer - * zenml.materializers.built_in_materializer.BuiltInMaterializer - * zenml.materializers.keras_meterializer.KerasMaterializer - * zenml.materializers.numpy_materializer.NumpyMaterializer - * zenml.materializers.pandas_materializer.PandasMaterializer - * zenml.materializers.tf_dataset_materializer.TensorflowDatasetMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> Any` - : Write logic here to handle input of the step function. - - Args: - data_type: What type the input should be materialized as. - Returns: - Any object that is to be passed into the relevant artifact in the - step. - - `handle_return(self, data: Any) ‑> None` - : Write logic here to handle return of the step function. - - Args: - Any object that is specified as an input artifact of the step. - -`BaseMaterializerMeta(*args, **kwargs)` -: Metaclass responsible for registering different BaseMaterializer - subclasses for reading/writing artifacts. - - ### Ancestors (in MRO) - - * builtins.type \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/beam_materializer.md b/docs/book/reference/api-reference/materializers/beam_materializer.md deleted file mode 100644 index e8fcba0caae..00000000000 --- a/docs/book/reference/api-reference/materializers/beam_materializer.md +++ /dev/null @@ -1,32 +0,0 @@ -Module zenml.materializers.beam_materializer -============================================ - -Classes -------- - -`BeamMaterializer(artifact: BaseArtifact)` -: Materializer to read data to and from beam. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> Any` - : Reads all files inside the artifact directory and materializes them - as a beam compatible output. - - `handle_return(self, pipeline: apache_beam.pipeline.Pipeline)` - : Appends a beam.io.WriteToParquet at the end of a beam pipeline - and therefore persists the results. - - Args: - pipeline: A beam.pipeline object. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/built_in_materializer.md b/docs/book/reference/api-reference/materializers/built_in_materializer.md deleted file mode 100644 index 31e1e08df5b..00000000000 --- a/docs/book/reference/api-reference/materializers/built_in_materializer.md +++ /dev/null @@ -1,27 +0,0 @@ -Module zenml.materializers.built_in_materializer -================================================ - -Classes -------- - -`BuiltInMaterializer(artifact: BaseArtifact)` -: Read/Write JSON files. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> Any` - : Reads basic primitive types from json. - - `handle_return(self, data: Any)` - : Handles basic built-in types and stores them as json \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/default_materializer_registry.md b/docs/book/reference/api-reference/materializers/default_materializer_registry.md deleted file mode 100644 index f41fbc82513..00000000000 --- a/docs/book/reference/api-reference/materializers/default_materializer_registry.md +++ /dev/null @@ -1,47 +0,0 @@ -Module zenml.materializers.default_materializer_registry -======================================================== - -Classes -------- - -`DefaultMaterializerRegistry()` -: Matches a python type to a default materializer. - - ### Class variables - - `materializer_types: ClassVar[Dict[Type[Any], Type[_ForwardRef('BaseMaterializer')]]]` - : - - ### Static methods - - `register_materializer_type(key: Type[Any], type_: Type[_ForwardRef('BaseMaterializer')])` - : Registers a new materializer. - - Args: - key: Indicates the type of an object. - type_: A BaseMaterializer subclass. - - ### Methods - - `get_materializer_types(self) ‑> Dict[Type[Any], Type[_ForwardRef('BaseMaterializer')]]` - : Get all registered materializers. - - `get_single_materializer_type(self, key: Type[Any]) ‑> BaseMaterializer` - : Get a single materializers based on the key. - - Args: - key: Indicates the type of an object. - - Returns: - Instance of a `BaseMaterializer` subclass initialized with the - artifact of this factory. - - `is_registered(self, key: Type[Any]) ‑> bool` - : Returns true if key type is registered, else returns False. - - `register_and_overwrite_type(self, key: Type[Any], type_: Type[_ForwardRef('BaseMaterializer')])` - : Registers a new materializer and also overwrites a default if set. - - Args: - key: Indicates the type of an object. - type_: A BaseMaterializer subclass. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/index.md b/docs/book/reference/api-reference/materializers/index.md deleted file mode 100644 index 6fca507be05..00000000000 --- a/docs/book/reference/api-reference/materializers/index.md +++ /dev/null @@ -1,14 +0,0 @@ -Module zenml.materializers -========================== - -Sub-modules ------------ -* [zenml.materializers.base_materializer](/reference/zenml/materializers/base_materializer.md) -* [zenml.materializers.beam_materializer](/reference/zenml/materializers/beam_materializer.md) -* [zenml.materializers.built_in_materializer](/reference/zenml/materializers/built_in_materializer.md) -* [zenml.materializers.default_materializer_registry](/reference/zenml/materializers/default_materializer_registry.md) -* [zenml.materializers.keras_meterializer](/reference/zenml/materializers/keras_meterializer.md) -* [zenml.materializers.numpy_materializer](/reference/zenml/materializers/numpy_materializer.md) -* [zenml.materializers.pandas_materializer](/reference/zenml/materializers/pandas_materializer.md) -* [zenml.materializers.spec_materializer_registry](/reference/zenml/materializers/spec_materializer_registry.md) -* [zenml.materializers.tf_dataset_materializer](/reference/zenml/materializers/tf_dataset_materializer.md) diff --git a/docs/book/reference/api-reference/materializers/keras_meterializer.md b/docs/book/reference/api-reference/materializers/keras_meterializer.md deleted file mode 100644 index 7c034ddfbd0..00000000000 --- a/docs/book/reference/api-reference/materializers/keras_meterializer.md +++ /dev/null @@ -1,33 +0,0 @@ -Module zenml.materializers.keras_meterializer -============================================= - -Classes -------- - -`KerasMaterializer(artifact: BaseArtifact)` -: Materializer to read/write Keras models. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> keras.engine.training.Model` - : Reads and returns a Keras model. - - Returns: - A tf.keras.Model model. - - `handle_return(self, model: keras.engine.training.Model)` - : Writes a keras model. - - Args: - model: A tf.keras.Model model. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/numpy_materializer.md b/docs/book/reference/api-reference/materializers/numpy_materializer.md deleted file mode 100644 index 9cfd8627ca8..00000000000 --- a/docs/book/reference/api-reference/materializers/numpy_materializer.md +++ /dev/null @@ -1,30 +0,0 @@ -Module zenml.materializers.numpy_materializer -============================================= - -Classes -------- - -`NumpyMaterializer(artifact: BaseArtifact)` -: Materializer to read data to and from pandas. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> numpy.ndarray` - : Reads numpy array from parquet file. - - `handle_return(self, arr: numpy.ndarray)` - : Writes a np.ndarray to the artifact store as a parquet file. - - Args: - arr: The numpy array to write. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/pandas_materializer.md b/docs/book/reference/api-reference/materializers/pandas_materializer.md deleted file mode 100644 index 24af2659219..00000000000 --- a/docs/book/reference/api-reference/materializers/pandas_materializer.md +++ /dev/null @@ -1,30 +0,0 @@ -Module zenml.materializers.pandas_materializer -============================================== - -Classes -------- - -`PandasMaterializer(artifact: BaseArtifact)` -: Materializer to read data to and from pandas. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> pandas.core.frame.DataFrame` - : Reads pd.Dataframe from a parquet file. - - `handle_return(self, df: pandas.core.frame.DataFrame)` - : Writes a pandas dataframe to the specified filename. - - Args: - df: The pandas dataframe to write. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/spec_materializer_registry.md b/docs/book/reference/api-reference/materializers/spec_materializer_registry.md deleted file mode 100644 index 74ad2cd2abd..00000000000 --- a/docs/book/reference/api-reference/materializers/spec_materializer_registry.md +++ /dev/null @@ -1,28 +0,0 @@ -Module zenml.materializers.spec_materializer_registry -===================================================== - -Classes -------- - -`SpecMaterializerRegistry()` -: Matches spec of a step to a materializer. - - Materializer types registry. - - ### Methods - - `get_materializer_types(self) ‑> Dict[str, Type[_ForwardRef('BaseMaterializer')]]` - : Get all registered materializers. - - `get_single_materializer_type(self, key: str) ‑> Type[_ForwardRef('BaseMaterializer')]` - : Gets a single pre-registered materializer type based on `key`. - - `is_registered(self, key: Type[Any]) ‑> bool` - : Returns true if key type is registered, else returns False. - - `register_materializer_type(self, key: str, type_: Type[_ForwardRef('BaseMaterializer')])` - : Registers a new materializer. - - Args: - key: Name of input or output parameter. - type_: A BaseMaterializer subclass. \ No newline at end of file diff --git a/docs/book/reference/api-reference/materializers/tf_dataset_materializer.md b/docs/book/reference/api-reference/materializers/tf_dataset_materializer.md deleted file mode 100644 index 7c433401db4..00000000000 --- a/docs/book/reference/api-reference/materializers/tf_dataset_materializer.md +++ /dev/null @@ -1,27 +0,0 @@ -Module zenml.materializers.tf_dataset_materializer -================================================== - -Classes -------- - -`TensorflowDatasetMaterializer(artifact: BaseArtifact)` -: Materializer to read data to and from beam. - - Initializes a materializer with the given artifact. - - ### Ancestors (in MRO) - - * zenml.materializers.base_materializer.BaseMaterializer - - ### Class variables - - `ASSOCIATED_TYPES` - : - - ### Methods - - `handle_input(self, data_type: Type) ‑> Any` - : Reads data into tf.data.Dataset - - `handle_return(self, dataset: tensorflow.python.data.ops.dataset_ops.DatasetV2)` - : Persists a tf.data.Dataset object. \ No newline at end of file diff --git a/docs/book/reference/api-reference/metadata/base_metadata_store.md b/docs/book/reference/api-reference/metadata/base_metadata_store.md deleted file mode 100644 index 2c01095deea..00000000000 --- a/docs/book/reference/api-reference/metadata/base_metadata_store.md +++ /dev/null @@ -1,65 +0,0 @@ -Module zenml.metadata.base_metadata_store -========================================= - -Classes -------- - -`BaseMetadataStore(**values: Any)` -: Metadata store base class to track metadata of zenml first class - citizens. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Descendants - - * zenml.metadata.mock_metadata_wrapper.MockMetadataStore - * zenml.metadata.mysql_metadata_wrapper.MySQLMetadataStore - * zenml.metadata.sqlite_metadata_wrapper.SQLiteMetadataStore - - ### Instance variables - - `store` - : General property that hooks into TFX metadata store. - - ### Methods - - `get_pipeline(self, pipeline_name: str) ‑> Optional[zenml.post_execution.pipeline.PipelineView]` - : Returns a pipeline for the given name. - - `get_pipeline_run_steps(self, pipeline_run: zenml.post_execution.pipeline_run.PipelineRunView) ‑> Dict[str, zenml.post_execution.step.StepView]` - : Gets all steps for the given pipeline run. - - `get_pipeline_runs(self, pipeline: zenml.post_execution.pipeline.PipelineView) ‑> List[zenml.post_execution.pipeline_run.PipelineRunView]` - : Gets all runs for the given pipeline. - - `get_pipelines(self) ‑> List[zenml.post_execution.pipeline.PipelineView]` - : Returns a list of all pipelines stored in this metadata store. - - `get_serialization_dir(self) ‑> str` - : Gets the local path where artifacts are stored. - - `get_step_artifacts(self, step: zenml.post_execution.step.StepView) ‑> Tuple[Dict[str, zenml.post_execution.artifact.ArtifactView], Dict[str, zenml.post_execution.artifact.ArtifactView]]` - : Returns input and output artifacts for the given step. - - Args: - step: The step for which to get the artifacts. - - Returns: - A tuple (inputs, outputs) where inputs and outputs - are both OrderedDicts mapping artifact names - to the input and output artifacts respectively. - - `get_step_status(self, step: zenml.post_execution.step.StepView) ‑> zenml.enums.ExecutionStatus` - : - - `get_tfx_metadata_config(self)` - : Return tfx metadata config. \ No newline at end of file diff --git a/docs/book/reference/api-reference/metadata/index.md b/docs/book/reference/api-reference/metadata/index.md deleted file mode 100644 index be778f16565..00000000000 --- a/docs/book/reference/api-reference/metadata/index.md +++ /dev/null @@ -1,9 +0,0 @@ -Module zenml.metadata -===================== - -Sub-modules ------------ -* [zenml.metadata.base_metadata_store](/reference/zenml/metadata/base_metadata_store.md) -* [zenml.metadata.mock_metadata_wrapper](/reference/zenml/metadata/mock_metadata_wrapper.md) -* [zenml.metadata.mysql_metadata_wrapper](/reference/zenml/metadata/mysql_metadata_wrapper.md) -* [zenml.metadata.sqlite_metadata_wrapper](/reference/zenml/metadata/sqlite_metadata_wrapper.md) diff --git a/docs/book/reference/api-reference/metadata/mock_metadata_wrapper.md b/docs/book/reference/api-reference/metadata/mock_metadata_wrapper.md deleted file mode 100644 index 549cdc8e487..00000000000 --- a/docs/book/reference/api-reference/metadata/mock_metadata_wrapper.md +++ /dev/null @@ -1,25 +0,0 @@ -Module zenml.metadata.mock_metadata_wrapper -=========================================== - -Classes -------- - -`MockMetadataStore(**values: Any)` -: Mock metadata store. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.metadata.base_metadata_store.BaseMetadataStore - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Methods - - `get_tfx_metadata_config(self)` - : Return tfx metadata config for mock metadata store. \ No newline at end of file diff --git a/docs/book/reference/api-reference/metadata/mysql_metadata_wrapper.md b/docs/book/reference/api-reference/metadata/mysql_metadata_wrapper.md deleted file mode 100644 index d2f2d899595..00000000000 --- a/docs/book/reference/api-reference/metadata/mysql_metadata_wrapper.md +++ /dev/null @@ -1,42 +0,0 @@ -Module zenml.metadata.mysql_metadata_wrapper -============================================ - -Classes -------- - -`MySQLMetadataStore(**values: Any)` -: MySQL backend for ZenML metadata store. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.metadata.base_metadata_store.BaseMetadataStore - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `database: str` - : - - `host: str` - : - - `password: str` - : - - `port: int` - : - - `username: str` - : - - ### Methods - - `get_tfx_metadata_config(self)` - : Return tfx metadata config for mysql metadata store. \ No newline at end of file diff --git a/docs/book/reference/api-reference/metadata/sqlite_metadata_wrapper.md b/docs/book/reference/api-reference/metadata/sqlite_metadata_wrapper.md deleted file mode 100644 index 72e47191021..00000000000 --- a/docs/book/reference/api-reference/metadata/sqlite_metadata_wrapper.md +++ /dev/null @@ -1,33 +0,0 @@ -Module zenml.metadata.sqlite_metadata_wrapper -============================================= - -Classes -------- - -`SQLiteMetadataStore(**data: Any)` -: SQLite backend for ZenML metadata store. - - Constructor for MySQL MetadataStore for ZenML. - - ### Ancestors (in MRO) - - * zenml.metadata.base_metadata_store.BaseMetadataStore - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `uri: str` - : - - ### Static methods - - `uri_must_be_local(v)` - : Validator to ensure uri is local - - ### Methods - - `get_tfx_metadata_config(self)` - : Return tfx metadata config for sqlite metadata store. \ No newline at end of file diff --git a/docs/book/reference/api-reference/orchestrators/airflow/airflow_dag_runner.md b/docs/book/reference/api-reference/orchestrators/airflow/airflow_dag_runner.md deleted file mode 100644 index 631f5b6b141..00000000000 --- a/docs/book/reference/api-reference/orchestrators/airflow/airflow_dag_runner.md +++ /dev/null @@ -1,46 +0,0 @@ -Module zenml.orchestrators.airflow.airflow_dag_runner -===================================================== -Definition of Airflow TFX runner. This is an unmodified copy from the TFX -source code (outside of superficial, stylistic changes) - -Classes -------- - -`AirflowDagRunner(config: Union[Dict[str, Any], zenml.orchestrators.airflow.airflow_dag_runner.AirflowPipelineConfig, NoneType] = None)` -: Tfx runner on Airflow. - - Creates an instance of AirflowDagRunner. - - Args: - config: Optional Airflow pipeline config for customizing the - launching of each component. - - ### Ancestors (in MRO) - - * tfx.orchestration.tfx_runner.TfxRunner - * abc.ABC - - ### Methods - - `run(self, tfx_pipeline: tfx.orchestration.pipeline.Pipeline)` - : Deploys given logical pipeline on Airflow. - - Args: - tfx_pipeline: Logical pipeline containing pipeline args and comps. - - Returns: - An Airflow DAG. - -`AirflowPipelineConfig(airflow_dag_config: Optional[Dict[str, Any]] = None, **kwargs)` -: Pipeline config for AirflowDagRunner. - - Creates an instance of AirflowPipelineConfig. - Args: - airflow_dag_config: Configs of Airflow DAG model. See - https://airflow.apache.org/_api/airflow/models/dag/index.html#airflow.models.dag.DAG - for the full spec. - **kwargs: keyword args for PipelineConfig. - - ### Ancestors (in MRO) - - * tfx.orchestration.config.pipeline_config.PipelineConfig \ No newline at end of file diff --git a/docs/book/reference/api-reference/orchestrators/airflow/airflow_orchestrator.md b/docs/book/reference/api-reference/orchestrators/airflow/airflow_orchestrator.md deleted file mode 100644 index 4f1f6ed4cc3..00000000000 --- a/docs/book/reference/api-reference/orchestrators/airflow/airflow_orchestrator.md +++ /dev/null @@ -1,29 +0,0 @@ -Module zenml.orchestrators.airflow.airflow_orchestrator -======================================================= - -Classes -------- - -`AirflowOrchestrator(**values: Any)` -: Orchestrator responsible for running pipelines using Airflow. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.orchestrators.base_orchestrator.BaseOrchestrator - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Methods - - `run(self, zenml_pipeline: BasePipeline, **kwargs)` - : Prepares the pipeline so it can be run in Airflow. - - Args: - zenml_pipeline: The pipeline to run. - **kwargs: Unused argument to conform with base class signature. \ No newline at end of file diff --git a/docs/book/reference/api-reference/orchestrators/airflow/index.md b/docs/book/reference/api-reference/orchestrators/airflow/index.md deleted file mode 100644 index c990f679952..00000000000 --- a/docs/book/reference/api-reference/orchestrators/airflow/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.orchestrators.airflow -================================== - -Sub-modules ------------ -* [zenml.orchestrators.airflow.airflow_dag_runner](/reference/zenml/orchestrators/airflow/airflow_dag_runner.md) -* [zenml.orchestrators.airflow.airflow_orchestrator](/reference/zenml/orchestrators/airflow/airflow_orchestrator.md) diff --git a/docs/book/reference/api-reference/orchestrators/base_orchestrator.md b/docs/book/reference/api-reference/orchestrators/base_orchestrator.md deleted file mode 100644 index 502ce458cd5..00000000000 --- a/docs/book/reference/api-reference/orchestrators/base_orchestrator.md +++ /dev/null @@ -1,38 +0,0 @@ -Module zenml.orchestrators.base_orchestrator -============================================ - -Classes -------- - -`BaseOrchestrator(**values: Any)` -: Base Orchestrator class to orchestrate ZenML pipelines. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Descendants - - * zenml.orchestrators.airflow.airflow_orchestrator.AirflowOrchestrator - * zenml.orchestrators.local.local_orchestrator.LocalOrchestrator - - ### Methods - - `get_serialization_dir(self) ‑> str` - : Gets the local path where artifacts are stored. - - `run(self, zenml_pipeline: BasePipeline, **kwargs)` - : Abstract method to run a pipeline. Overwrite this in subclasses - with a concrete implementation on how to run the given pipeline. - - Args: - zenml_pipeline: The pipeline to run. - **kwargs: Potential additional parameters used in subclass - implementations. \ No newline at end of file diff --git a/docs/book/reference/api-reference/orchestrators/index.md b/docs/book/reference/api-reference/orchestrators/index.md deleted file mode 100644 index d954e367323..00000000000 --- a/docs/book/reference/api-reference/orchestrators/index.md +++ /dev/null @@ -1,8 +0,0 @@ -Module zenml.orchestrators -========================== - -Sub-modules ------------ -* [zenml.orchestrators.airflow](/reference/zenml/orchestrators/airflow/index.md) -* [zenml.orchestrators.base_orchestrator](/reference/zenml/orchestrators/base_orchestrator.md) -* [zenml.orchestrators.local](/reference/zenml/orchestrators/local/index.md) diff --git a/docs/book/reference/api-reference/orchestrators/local/index.md b/docs/book/reference/api-reference/orchestrators/local/index.md deleted file mode 100644 index caae55334cd..00000000000 --- a/docs/book/reference/api-reference/orchestrators/local/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.orchestrators.local -================================ - -Sub-modules ------------ -* [zenml.orchestrators.local.local_dag_runner](/reference/zenml/orchestrators/local/local_dag_runner.md) -* [zenml.orchestrators.local.local_orchestrator](/reference/zenml/orchestrators/local/local_orchestrator.md) diff --git a/docs/book/reference/api-reference/orchestrators/local/local_dag_runner.md b/docs/book/reference/api-reference/orchestrators/local/local_dag_runner.md deleted file mode 100644 index 5560fa085f6..00000000000 --- a/docs/book/reference/api-reference/orchestrators/local/local_dag_runner.md +++ /dev/null @@ -1,38 +0,0 @@ -Module zenml.orchestrators.local.local_dag_runner -================================================= -Definition of Beam TFX runner. Inspired by local dag runner implementation -by Google at: https://github.com/tensorflow/tfx/blob/master/tfx/orchestration -/local/local_dag_runner.py - -Functions ---------- - - -`format_timedelta_pretty(seconds: float) ‑> str` -: Format a float representing seconds into a string. - - Args: - seconds: result of a time.time() - time.time(). - - Returns: - Pretty formatted string according to specification. - -Classes -------- - -`LocalDagRunner()` -: Local TFX DAG runner. - - Initializes LocalDagRunner as a TFX orchestrator. - - ### Ancestors (in MRO) - - * tfx.orchestration.portable.tfx_runner.TfxRunner - - ### Methods - - `run(self, pipeline: tfx.orchestration.pipeline.Pipeline) ‑> None` - : Runs given logical pipeline locally. - - Args: - pipeline: Logical pipeline containing pipeline args and components. \ No newline at end of file diff --git a/docs/book/reference/api-reference/orchestrators/local/local_orchestrator.md b/docs/book/reference/api-reference/orchestrators/local/local_orchestrator.md deleted file mode 100644 index 13abf53d70e..00000000000 --- a/docs/book/reference/api-reference/orchestrators/local/local_orchestrator.md +++ /dev/null @@ -1,29 +0,0 @@ -Module zenml.orchestrators.local.local_orchestrator -=================================================== - -Classes -------- - -`LocalOrchestrator(**values: Any)` -: Orchestrator responsible for running pipelines locally. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * zenml.orchestrators.base_orchestrator.BaseOrchestrator - * zenml.core.base_component.BaseComponent - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Methods - - `run(self, zenml_pipeline: BasePipeline, **pipeline_args)` - : Runs a pipeline locally. - - Args: - zenml_pipeline: The pipeline to run. - **pipeline_args: Unused kwargs to conform with base signature. \ No newline at end of file diff --git a/docs/book/reference/api-reference/pipelines/base_pipeline.md b/docs/book/reference/api-reference/pipelines/base_pipeline.md deleted file mode 100644 index 1bd3d1524c1..00000000000 --- a/docs/book/reference/api-reference/pipelines/base_pipeline.md +++ /dev/null @@ -1,42 +0,0 @@ -Module zenml.pipelines.base_pipeline -==================================== - -Classes -------- - -`BasePipeline(*args, **kwargs)` -: Base ZenML pipeline. - - ### Class variables - - `NAME` - : - - `STEP_SPEC` - : - - ### Instance variables - - `name: str` - : Name of pipeline is always equal to self.NAME - - `stack: zenml.stacks.base_stack.BaseStack` - : Returns the stack for this pipeline. - - `steps: Dict` - : Returns a dictionary of pipeline steps. - - ### Methods - - `connect(self, *args, **kwargs)` - : Function that connects inputs and outputs of the pipeline steps. - - `run(*args, **kwargs)` - : Inner decorator function. - -`BasePipelineMeta(*args, **kwargs)` -: Pipeline Metaclass responsible for validating the pipeline definition. - - ### Ancestors (in MRO) - - * builtins.type \ No newline at end of file diff --git a/docs/book/reference/api-reference/pipelines/index.md b/docs/book/reference/api-reference/pipelines/index.md deleted file mode 100644 index da159c30d6d..00000000000 --- a/docs/book/reference/api-reference/pipelines/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.pipelines -====================== - -Sub-modules ------------ -* [zenml.pipelines.base_pipeline](/reference/zenml/pipelines/base_pipeline.md) -* [zenml.pipelines.pipeline_decorator](/reference/zenml/pipelines/pipeline_decorator.md) diff --git a/docs/book/reference/api-reference/pipelines/pipeline_decorator.md b/docs/book/reference/api-reference/pipelines/pipeline_decorator.md deleted file mode 100644 index a928ccb5961..00000000000 --- a/docs/book/reference/api-reference/pipelines/pipeline_decorator.md +++ /dev/null @@ -1,21 +0,0 @@ -Module zenml.pipelines.pipeline_decorator -========================================= - -Functions ---------- - - -`pipeline(*, name: str = None, enable_cache: bool = True) ‑> Callable[..., Type[zenml.pipelines.base_pipeline.BasePipeline]]` -: Outer decorator function for the creation of a ZenML pipeline - - In order to be able work with parameters such as "name", it features a - nested decorator structure. - - Args: - _func: Optional func from outside. - name: str, the given name for the pipeline - enable_cache: Whether to use cache or not. - - Returns: - the inner decorator which creates the pipeline class based on the - ZenML BasePipeline \ No newline at end of file diff --git a/docs/book/reference/api-reference/post_execution/artifact.md b/docs/book/reference/api-reference/post_execution/artifact.md deleted file mode 100644 index 373184d4e9c..00000000000 --- a/docs/book/reference/api-reference/post_execution/artifact.md +++ /dev/null @@ -1,45 +0,0 @@ -Module zenml.post_execution.artifact -==================================== - -Classes -------- - -`ArtifactView(id_: int, type_: str, uri: str, materializer: str)` -: Post-execution artifact class which can be used to read - artifact data that was created during a pipeline execution. - - Initializes a post-execution artifact object. - - In most cases `ArtifactView` objects should not be created manually but - retrieved from a `StepView` via the `inputs` or `outputs` properties. - - Args: - id_: The artifact id. - type_: The type of this artifact. - uri: Specifies where the artifact data is stored. - materializer: Information needed to restore the materializer - that was used to write this artifact. - - ### Instance variables - - `type: str` - : Returns the artifact type. - - `uri: str` - : Returns the URI where the artifact data is stored. - - ### Methods - - `read(self, output_data_type: Type, materializer_class: Optional[Type[zenml.materializers.base_materializer.BaseMaterializer]] = None) ‑> Any` - : Materializes the data stored in this artifact. - - Args: - output_data_type: The datatype to which the materializer should - read, will be passed to the materializers `handle_input` method. - materializer_class: The class of the materializer that should be - used to read the artifact data. If no materializer class is - given, we use the materializer that was used to write the - artifact during execution of the pipeline. - - Returns: - The materialized data. \ No newline at end of file diff --git a/docs/book/reference/api-reference/post_execution/index.md b/docs/book/reference/api-reference/post_execution/index.md deleted file mode 100644 index 7553ae37522..00000000000 --- a/docs/book/reference/api-reference/post_execution/index.md +++ /dev/null @@ -1,9 +0,0 @@ -Module zenml.post_execution -=========================== - -Sub-modules ------------ -* [zenml.post_execution.artifact](/reference/zenml/post_execution/artifact.md) -* [zenml.post_execution.pipeline](/reference/zenml/post_execution/pipeline.md) -* [zenml.post_execution.pipeline_run](/reference/zenml/post_execution/pipeline_run.md) -* [zenml.post_execution.step](/reference/zenml/post_execution/step.md) diff --git a/docs/book/reference/api-reference/post_execution/pipeline.md b/docs/book/reference/api-reference/post_execution/pipeline.md deleted file mode 100644 index e4da8518913..00000000000 --- a/docs/book/reference/api-reference/post_execution/pipeline.md +++ /dev/null @@ -1,34 +0,0 @@ -Module zenml.post_execution.pipeline -==================================== - -Classes -------- - -`PipelineView(id_: int, name: str, metadata_store: BaseMetadataStore)` -: Post-execution pipeline class which can be used to query - pipeline-related information from the metadata store. - - Initializes a post-execution pipeline object. - - In most cases `PipelineView` objects should not be created manually - but retrieved using the `get_pipelines()` method of a - `zenml.core.repo.Repository` instead. - - Args: - id_: The context id of this pipeline. - name: The name of this pipeline. - metadata_store: The metadata store which should be used to fetch - additional information related to this pipeline. - - ### Instance variables - - `name: str` - : Returns the name of the pipeline. - - ### Methods - - `get_runs(self) ‑> List[zenml.post_execution.pipeline_run.PipelineRunView]` - : Returns all stored runs of this pipeline. - - The runs are returned in chronological order, so the latest - run will be the last element in this list. \ No newline at end of file diff --git a/docs/book/reference/api-reference/post_execution/pipeline_run.md b/docs/book/reference/api-reference/post_execution/pipeline_run.md deleted file mode 100644 index f068b6aac97..00000000000 --- a/docs/book/reference/api-reference/post_execution/pipeline_run.md +++ /dev/null @@ -1,46 +0,0 @@ -Module zenml.post_execution.pipeline_run -======================================== - -Classes -------- - -`PipelineRunView(id_: int, name: str, executions: List[ml_metadata.proto.metadata_store_pb2.Execution], metadata_store: BaseMetadataStore)` -: Post-execution pipeline run class which can be used to query - steps and artifact information associated with a pipeline execution. - - Initializes a post-execution pipeline run object. - - In most cases `PipelineRunView` objects should not be created manually - but retrieved from a `PipelineView` object instead. - - Args: - id_: The context id of this pipeline run. - name: The name of this pipeline run. - executions: All executions associated with this pipeline run. - metadata_store: The metadata store which should be used to fetch - additional information related to this pipeline run. - - ### Instance variables - - `name: str` - : Returns the name of the pipeline run. - - `status: zenml.enums.ExecutionStatus` - : Returns the current status of the pipeline run. - - `steps: List[zenml.post_execution.step.StepView]` - : Returns all steps that were executed as part of this pipeline run. - - ### Methods - - `get_step(self, name: str) ‑> zenml.post_execution.step.StepView` - : Returns a step for the given name. - - Args: - name: The name of the step to return. - - Raises: - KeyError: If there is no step with the given name. - - `get_step_names(self) ‑> List[str]` - : Returns a list of all step names. \ No newline at end of file diff --git a/docs/book/reference/api-reference/post_execution/step.md b/docs/book/reference/api-reference/post_execution/step.md deleted file mode 100644 index 8e5fcb747d4..00000000000 --- a/docs/book/reference/api-reference/post_execution/step.md +++ /dev/null @@ -1,85 +0,0 @@ -Module zenml.post_execution.step -================================ - -Classes -------- - -`StepView(id_: int, name: str, parameters: Dict[str, Any], metadata_store: BaseMetadataStore)` -: Post-execution step class which can be used to query - artifact information associated with a pipeline step. - - Initializes a post-execution step object. - - In most cases `StepView` objects should not be created manually - but retrieved from a `PipelineRunView` object instead. - - Args: - id_: The execution id of this step. - name: The name of this step. - parameters: Parameters that were used to run this step. - metadata_store: The metadata store which should be used to fetch - additional information related to this step. - - ### Instance variables - - `inputs: List[zenml.post_execution.artifact.ArtifactView]` - : Returns a list of input artifacts that were used to run this step. - - These artifacts are in the same order as defined in the signature of - the step function. - - `name: str` - : Returns the step name. - - This name is equal to the name argument passed to the @step decorator - or the actual function name if no explicit name was given. - - Examples: - # the step name will be "my_step" - @step(name="my_step") - def my_step_function(...) - - # the step name will be "my_step_function" - @step - def my_step_function(...) - - `outputs: List[zenml.post_execution.artifact.ArtifactView]` - : Returns a list of output artifacts that were written by this step. - - These artifacts are in the same order as defined in the signature of - the step function. - - `parameters: Dict[str, Any]` - : The parameters used to run this step. - - `status: zenml.enums.ExecutionStatus` - : Returns the current status of the step. - - ### Methods - - `get_input(self, name: str) ‑> zenml.post_execution.artifact.ArtifactView` - : Returns an input artifact for the given name. - - Args: - name: The name of the input artifact to return. - - Raises: - KeyError: If there is no input artifact with the given name. - - `get_input_names(self) ‑> List[str]` - : Returns a list of all input artifact names. - - `get_output(self, name: str) ‑> zenml.post_execution.artifact.ArtifactView` - : Returns an output artifact for the given name. - - Args: - name: The name of the output artifact to return. - - Raises: - KeyError: If there is no output artifact with the given name. - - `get_output_names(self) ‑> List[str]` - : Returns a list of all output artifact names. - - If a step only has a single output, it will have the - default name `output`. \ No newline at end of file diff --git a/docs/book/reference/api-reference/stacks/base_stack.md b/docs/book/reference/api-reference/stacks/base_stack.md deleted file mode 100644 index c91d847c25f..00000000000 --- a/docs/book/reference/api-reference/stacks/base_stack.md +++ /dev/null @@ -1,61 +0,0 @@ -Module zenml.stacks.base_stack -============================== - -Classes -------- - -`BaseStack(**values: Any)` -: Base stack for ZenML. - - A ZenML stack brings together an Metadata Store, an Artifact Store, and - an Orchestrator, the trifecta of the environment required to run a ZenML - pipeline. A ZenML stack also happens to be a pydantic `BaseSettings` - class, which means that there are multiple ways to use it. - - * You can set it via env variables. - * You can set it through the config yaml file. - * You can set it in code by initializing an object of this class, and - passing it to pipelines as a configuration. - - In the case where a value is specified for the same Settings field in - multiple ways, the selected value is determined as follows (in descending - order of priority): - - * Arguments passed to the Settings class initializer. - * Environment variables, e.g. zenml_var as described above. - * Variables loaded from a config yaml file. - * The default field values. - - ### Ancestors (in MRO) - - * pydantic.env_settings.BaseSettings - * pydantic.main.BaseModel - * pydantic.utils.Representation - - ### Class variables - - `Config` - : Configuration of settings. - - `artifact_store_name: str` - : - - `metadata_store_name: str` - : - - `orchestrator_name: str` - : - - `stack_type: zenml.enums.StackTypes` - : - - ### Instance variables - - `artifact_store: zenml.artifact_stores.base_artifact_store.BaseArtifactStore` - : Returns the artifact store of this stack. - - `metadata_store: zenml.metadata.base_metadata_store.BaseMetadataStore` - : Returns the metadata store of this stack. - - `orchestrator: zenml.orchestrators.base_orchestrator.BaseOrchestrator` - : Returns the orchestrator of this stack. \ No newline at end of file diff --git a/docs/book/reference/api-reference/stacks/constants.md b/docs/book/reference/api-reference/stacks/constants.md deleted file mode 100644 index 819d42e0b8b..00000000000 --- a/docs/book/reference/api-reference/stacks/constants.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.stacks.constants -============================= \ No newline at end of file diff --git a/docs/book/reference/api-reference/stacks/index.md b/docs/book/reference/api-reference/stacks/index.md deleted file mode 100644 index 69b77e86e39..00000000000 --- a/docs/book/reference/api-reference/stacks/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.stacks -=================== - -Sub-modules ------------ -* [zenml.stacks.base_stack](/reference/zenml/stacks/base_stack.md) -* [zenml.stacks.constants](/reference/zenml/stacks/constants.md) diff --git a/docs/book/reference/api-reference/steps/base_step.md b/docs/book/reference/api-reference/steps/base_step.md deleted file mode 100644 index 893874a5d5a..00000000000 --- a/docs/book/reference/api-reference/steps/base_step.md +++ /dev/null @@ -1,68 +0,0 @@ -Module zenml.steps.base_step -============================ - -Functions ---------- - - -`check_dict_keys_match(x: Dict, y: Dict) ‑> bool` -: Checks whether there is even one key shared between two dicts. - - Returns: - True if there is a shared key, otherwise False. - -Classes -------- - -`BaseStep(*args, **kwargs)` -: The base implementation of a ZenML Step which will be inherited by all - the other step implementations - - ### Class variables - - `CONFIG` - : - - `INPUT_SIGNATURE` - : - - `OUTPUT_SIGNATURE` - : - - ### Instance variables - - `component` - : Returns a TFX component. - - ### Methods - - `process(self, *args, **kwargs)` - : Abstract method for core step logic. - - `resolve_signature_materializers(self, signature: Dict[str, Type], is_input: bool = True) ‑> None` - : Takes either the INPUT_SIGNATURE and OUTPUT_SIGNATURE and resolves - the materializers for them in the `spec_materializer_registry`. - - Args: - signature: Either self.INPUT_SIGNATURE or self.OUTPUT_SIGNATURE. - is_input: If True, then self.INPUT_SPEC used, else self.OUTPUT_SPEC. - - `with_return_materializers(self, materializers: Union[Type[zenml.materializers.base_materializer.BaseMaterializer], Dict[str, Type[zenml.materializers.base_materializer.BaseMaterializer]]])` - : Inject materializers from the outside. If one materializer is passed - in then all outputs are assigned that materializer. If a dict is passed - in then we make sure the output names match. - - Args: - materializers: Either a `Type[BaseMaterializer]`, or a - dict that maps {output_name: Type[BaseMaterializer]}. - -`BaseStepMeta(*args, **kwargs)` -: Meta class for `BaseStep`. - - Checks whether everything passed in: - * Has a matching materializer. - * Is a subclass of the Config class - - ### Ancestors (in MRO) - - * builtins.type \ No newline at end of file diff --git a/docs/book/reference/api-reference/steps/base_step_config.md b/docs/book/reference/api-reference/steps/base_step_config.md deleted file mode 100644 index 8c2de6fa7eb..00000000000 --- a/docs/book/reference/api-reference/steps/base_step_config.md +++ /dev/null @@ -1,17 +0,0 @@ -Module zenml.steps.base_step_config -=================================== - -Classes -------- - -`BaseStepConfig(**data: Any)` -: Base configuration class to pass execution params into a step. - - Create a new model by parsing and validating input data from keyword arguments. - - Raises ValidationError if the input data cannot be parsed to form a valid model. - - ### Ancestors (in MRO) - - * pydantic.main.BaseModel - * pydantic.utils.Representation \ No newline at end of file diff --git a/docs/book/reference/api-reference/steps/index.md b/docs/book/reference/api-reference/steps/index.md deleted file mode 100644 index 7bba8676c31..00000000000 --- a/docs/book/reference/api-reference/steps/index.md +++ /dev/null @@ -1,11 +0,0 @@ -Module zenml.steps -================== - -Sub-modules ------------ -* [zenml.steps.base_step](/reference/zenml/steps/base_step.md) -* [zenml.steps.base_step_config](/reference/zenml/steps/base_step_config.md) -* [zenml.steps.step_decorator](/reference/zenml/steps/step_decorator.md) -* [zenml.steps.step_interfaces](/reference/zenml/steps/step_interfaces/index.md) -* [zenml.steps.step_output](/reference/zenml/steps/step_output.md) -* [zenml.steps.utils](/reference/zenml/steps/utils.md) diff --git a/docs/book/reference/api-reference/steps/step_decorator.md b/docs/book/reference/api-reference/steps/step_decorator.md deleted file mode 100644 index 4cde32b0e0e..00000000000 --- a/docs/book/reference/api-reference/steps/step_decorator.md +++ /dev/null @@ -1,20 +0,0 @@ -Module zenml.steps.step_decorator -================================= - -Functions ---------- - - -`step(*, name: str = None) ‑> Callable[..., zenml.steps.base_step.BaseStep]` -: Outer decorator function for the creation of a ZenML step - - In order to be able work with parameters such as `name`, it features a - nested decorator structure. - - Args: - _func: Optional func from outside. - name (required) the given name for the step. - - Returns: - the inner decorator which creates the step class based on the - ZenML BaseStep \ No newline at end of file diff --git a/docs/book/reference/api-reference/steps/step_interfaces/base_interface.md b/docs/book/reference/api-reference/steps/step_interfaces/base_interface.md deleted file mode 100644 index 30c1f1f6ba9..00000000000 --- a/docs/book/reference/api-reference/steps/step_interfaces/base_interface.md +++ /dev/null @@ -1,2 +0,0 @@ -Module zenml.steps.step_interfaces.base_interface -================================================= \ No newline at end of file diff --git a/docs/book/reference/api-reference/steps/step_interfaces/index.md b/docs/book/reference/api-reference/steps/step_interfaces/index.md deleted file mode 100644 index b2c60bfeae0..00000000000 --- a/docs/book/reference/api-reference/steps/step_interfaces/index.md +++ /dev/null @@ -1,6 +0,0 @@ -Module zenml.steps.step_interfaces -================================== - -Sub-modules ------------ -* [zenml.steps.step_interfaces.base_interface](/reference/zenml/steps/step_interfaces/base_interface.md) diff --git a/docs/book/reference/api-reference/steps/step_output.md b/docs/book/reference/api-reference/steps/step_output.md deleted file mode 100644 index 50037c1f357..00000000000 --- a/docs/book/reference/api-reference/steps/step_output.md +++ /dev/null @@ -1,13 +0,0 @@ -Module zenml.steps.step_output -============================== - -Classes -------- - -`Output(**kwargs)` -: A named tuple with a default name that cannot be overriden. - - ### Methods - - `items(self) ‑> Tuple[str, Type]` - : Yields a tuple of type (output_name, output_type). \ No newline at end of file diff --git a/docs/book/reference/api-reference/steps/utils.md b/docs/book/reference/api-reference/steps/utils.md deleted file mode 100644 index fa877663862..00000000000 --- a/docs/book/reference/api-reference/steps/utils.md +++ /dev/null @@ -1,34 +0,0 @@ -Module zenml.steps.utils -======================== -The collection of utility functions/classes are inspired by their original -implementation of the Tensorflow Extended team, which can be found here: - -https://github.com/tensorflow/tfx/blob/master/tfx/dsl/component/experimental -/decorators.py - -This version is heavily adjusted to work with the Pipeline-Step paradigm which -is proposed by ZenML. - -Functions ---------- - - -`do_types_match(type_a: Type, type_b: Type) ‑> bool` -: Check whether type_a and type_b match. - - Args: - type_a: First Type to check. - type_b: Second Type to check. - - Returns: - True if types match, otherwise False. - - -`generate_component(step) ‑> Callable[..., Any]` -: Utility function which converts a ZenML step into a TFX Component - - Args: - step: a ZenML step instance - - Returns: - component: the class of the corresponding TFX component \ No newline at end of file diff --git a/docs/book/reference/api-reference/types/base_type.md b/docs/book/reference/api-reference/types/base_type.md deleted file mode 100644 index 7aaf2e56721..00000000000 --- a/docs/book/reference/api-reference/types/base_type.md +++ /dev/null @@ -1,14 +0,0 @@ -Module zenml.types.base_type -============================ - -Classes -------- - -`BaseType(*args, **kwargs)` -: type(object_or_name, bases, dict) - type(object) -> the object's type - type(name, bases, dict) -> a new type - - ### Ancestors (in MRO) - - * builtins.type \ No newline at end of file diff --git a/docs/book/reference/api-reference/types/index.md b/docs/book/reference/api-reference/types/index.md deleted file mode 100644 index 24335363f87..00000000000 --- a/docs/book/reference/api-reference/types/index.md +++ /dev/null @@ -1,7 +0,0 @@ -Module zenml.types -================== - -Sub-modules ------------ -* [zenml.types.base_type](/reference/zenml/types/base_type.md) -* [zenml.types.pytorch_types](/reference/zenml/types/pytorch_types.md) diff --git a/docs/book/reference/api-reference/types/pytorch_types.md b/docs/book/reference/api-reference/types/pytorch_types.md deleted file mode 100644 index e3344e3a520..00000000000 --- a/docs/book/reference/api-reference/types/pytorch_types.md +++ /dev/null @@ -1,25 +0,0 @@ -Module zenml.types.pytorch_types -================================ - -Classes -------- - -`TorchDict(*args, **kwargs)` -: A type of dict that represents saving a model. - - ### Ancestors (in MRO) - - * typing.Dict - * builtins.dict - * typing.MutableMapping - * collections.abc.MutableMapping - * typing.Mapping - * collections.abc.Mapping - * typing.Collection - * collections.abc.Collection - * collections.abc.Sized - * typing.Iterable - * collections.abc.Iterable - * typing.Container - * collections.abc.Container - * typing.Generic \ No newline at end of file diff --git a/docs/book/reference/api-reference/utils/analytics_utils.md b/docs/book/reference/api-reference/utils/analytics_utils.md deleted file mode 100644 index 0a78eca469e..00000000000 --- a/docs/book/reference/api-reference/utils/analytics_utils.md +++ /dev/null @@ -1,49 +0,0 @@ -Module zenml.utils.analytics_utils -================================== -Analytics code for ZenML - -Functions ---------- - - -`get_segment_key() ‑> str` -: Get key for authorizing to Segment backend. - - Returns: - Segment key as a string. - - Raises: - requests.exceptions.RequestException if request times out. - - -`get_system_info() ‑> Dict` -: Returns system info as a dict. - - Returns: - A dict of system information. - - -`in_docker()` -: Returns: True if running in a Docker container, else False - - -`initialize_telemetry()` -: Initializes telemetry with the right key - - -`parametrized(dec)` -: This is a meta-decorator, that is, a decorator for decorators. - As a decorator is a function, it actually works as a regular decorator - with arguments: - - -`track(*args: Any, **kwargs: Any)` -: Internal layer - - -`track_event(event: str, metadata: Dict = None)` -: Track segment event if user opted-in. - - Args: - event: Name of event to track in segment. - metadata: Dict of metadata to track. \ No newline at end of file diff --git a/docs/book/reference/api-reference/utils/index.md b/docs/book/reference/api-reference/utils/index.md deleted file mode 100644 index d87f83108f6..00000000000 --- a/docs/book/reference/api-reference/utils/index.md +++ /dev/null @@ -1,9 +0,0 @@ -Module zenml.utils -================== - -Sub-modules ------------ -* [zenml.utils.analytics_utils](/reference/zenml/utils/analytics_utils.md) -* [zenml.utils.path_utils](/reference/zenml/utils/path_utils.md) -* [zenml.utils.source_utils](/reference/zenml/utils/source_utils.md) -* [zenml.utils.yaml_utils](/reference/zenml/utils/yaml_utils.md) diff --git a/docs/book/reference/api-reference/utils/path_utils.md b/docs/book/reference/api-reference/utils/path_utils.md deleted file mode 100644 index 6027279fa0a..00000000000 --- a/docs/book/reference/api-reference/utils/path_utils.md +++ /dev/null @@ -1,264 +0,0 @@ -Module zenml.utils.path_utils -============================= -File utilities - -Functions ---------- - - -`append_file(file_path: str, file_contents: str)` -: Appends file_contents to file. - - Args: - file_path: Local path in filesystem. - file_contents: Contents of file. - - -`copy(source: str, destination: str, overwrite: bool = False)` -: Copies dir from source to destination. - - Args: - source(str): Path to copy from. - destination(str): Path to copy to. - overwrite: boolean, if false, then throws an error before overwrite. - - -`copy_dir(source_dir: str, destination_dir: str, overwrite: bool = False)` -: Copies dir from source to destination. - - Args: - source_dir: Path to copy from. - destination_dir: Path to copy to. - overwrite: Boolean, if false, then throws an error before overwrite. - - -`create_dir_if_not_exists(dir_path: str)` -: Creates directory if it does not exist. - - Args: - dir_path(str): Local path in filesystem. - - -`create_dir_recursive_if_not_exists(dir_path: str)` -: Creates directory recursively if it does not exist. - - Args: - dir_path: Local path in filesystem. - - -`create_file_if_not_exists(file_path: str, file_contents: str = '{}')` -: Creates directory if it does not exist. - - Args: - file_path: Local path in filesystem. - file_contents: Contents of file. - - -`create_tarfile(source_dir: str, output_filename: str = 'zipped.tar.gz', exclude_function: Callable = None)` -: Create a compressed representation of source_dir. - - Args: - source_dir: Path to source dir. - output_filename: Name of outputted gz. - exclude_function: Function that determines whether to exclude file. - - -`extract_tarfile(source_tar: str, output_dir: str)` -: Untars a compressed tar file to output_dir. - - Args: - source_tar: Path to a tar compressed file. - output_dir: Directory where to uncompress. - - -`file_exists(path: str) ‑> bool` -: Returns true if file exists at path. - - Args: - path: Local path in filesystem. - - Returns: - True if file exists, else False. - - -`find_files(dir_path, pattern) ‑> List[str]` -: Find files in a directory that match pattern. - - Args: - dir_path: Path to directory. - pattern: pattern like *.png. - - Yields: - All matching filenames if found, else None. - - -`get_grandparent(dir_path: str) ‑> str` -: Get grandparent of dir. - - Args: - dir_path: Path to directory. - - Returns: - The input paths parents parent. - - -`get_parent(dir_path: str) ‑> str` -: Get parent of dir. - - Args: - dir_path(str): Path to directory. - - Returns: - Parent (stem) of the dir as a string. - - -`get_zenml_config_dir(path: str = '/home/hamza/workspace/maiot/github_temp/zenml') ‑> str` -: Recursive function to find the zenml config starting from path. - - Args: - path (Default value = os.getcwd()): Path to check. - - Returns: - The full path with the resolved zenml directory. - - Raises: - InitializationException if directory not found until root of OS. - - -`get_zenml_dir(path: str = '/home/hamza/workspace/maiot/github_temp/zenml') ‑> str` -: Recursive function to find the zenml config starting from path. - - Args: - path (Default value = os.getcwd()): Path to check. - - Returns: - The full path with the resolved zenml directory. - - Raises: - InitializationException if directory not found until root of OS. - - -`is_dir(dir_path: str) ‑> bool` -: Returns true if dir_path points to a dir. - - Args: - dir_path: Local path in filesystem. - - Returns: - True if is dir, else False. - - -`is_gcs_path(path: str) ‑> bool` -: Returns True if path is on Google Cloud Storage. - - Args: - path: Any path as a string. - - Returns: - True if gcs path, else False. - - -`is_remote(path: str) ‑> bool` -: Returns True if path exists remotely. - - Args: - path: Any path as a string. - - Returns: - True if remote path, else False. - - -`is_root(path: str) ‑> bool` -: Returns true if path has no parent in local filesystem. - - Args: - path: Local path in filesystem. - - Returns: - True if root, else False. - - -`is_zenml_dir(path: str) ‑> bool` -: Check if dir is a zenml dir or not. - - Args: - path: Path to the root. - - Returns: - True if path contains a zenml dir, False if not. - - -`list_dir(dir_path: str, only_file_names: bool = False) ‑> List[str]` -: Returns a list of files under dir. - - Args: - dir_path: Path in filesystem. - only_file_names: Returns only file names if True. - - Returns: - List of full qualified paths. - - -`load_csv_header(csv_path: str) ‑> List[str]` -: Gets header column of csv and returns list. - - Args: - csv_path: Path to csv file. - - -`move(source: str, destination: str, overwrite: bool = False)` -: Moves dir from source to destination. Can be used to rename. - - Args: - source: Local path to copy from. - destination: Local path to copy to. - overwrite: boolean, if false, then throws an error before overwrite. - - -`read_file_contents_as_string(file_path: str)` -: Reads contents of file. - - Args: - file_path: Path to file. - - -`resolve_relative_path(path: str) ‑> str` -: Takes relative path and resolves it absolutely. - - Args: - path: Local path in filesystem. - - Returns: - Resolved path. - - -`rm_dir(dir_path: str)` -: Deletes dir recursively. Dangerous operation. - - Args: - dir_path: Dir to delete. - - -`rm_file(file_path: str)` -: Deletes file. Dangerous operation. - - Args: - file_path: Path of file to delete. - - -`walk(dir_path) ‑> Iterable[Tuple[Union[bytes, str], List[Union[bytes, str]], List[Union[bytes, str]]]]` -: Walks down the dir_path. - - Args: - dir_path: Path of dir to walk down. - - Returns: - Iterable of tuples to walk down. - - -`write_file_contents_as_string(file_path: str, content: str)` -: Writes contents of file. - - Args: - file_path: Path to file. - content: Contents of file. \ No newline at end of file diff --git a/docs/book/reference/api-reference/utils/source_utils.md b/docs/book/reference/api-reference/utils/source_utils.md deleted file mode 100644 index 7af89714312..00000000000 --- a/docs/book/reference/api-reference/utils/source_utils.md +++ /dev/null @@ -1,119 +0,0 @@ -Module zenml.utils.source_utils -=============================== -These utils are predicated on the following definitions: - -* class_source: This is a python-import type path to a class, e.g. -some.mod.class -* module_source: This is a python-import type path to a module, e.g. some.mod -* file_path, relative_path, absolute_path: These are file system paths. -* source: This is a class_source or module_source. If it is a class_source, it -can also be optionally pinned. -* pin: Whatever comes after the `@` symbol from a source, usually the git sha -or the version of zenml as a string. - -Functions ---------- - - -`create_zenml_pin() ‑> str` -: Creates a ZenML pin for source pinning from release version. - - -`get_absolute_path_from_module_source(module: str) ‑> str` -: Get a directory path from module source. - - E.g. `zenml.core.step` will return `full/path/to/zenml/core/step`. - - Args: - module: A module e.g. `zenml.core.step`. - - -`get_class_source_from_source(source: str) ‑> Optional[str]` -: Gets class source from source, i.e. module.path@version, returns version. - - Args: - source: source pointing to potentially pinned sha. - - -`get_module_source_from_class(class_: Union[Type, str]) ‑> Optional[str]` -: Takes class input and returns module_source. If class is already string - then returns the same. - - Args: - class_: object of type class. - - -`get_module_source_from_file_path(file_path: str) ‑> str` -: Gets module_source from a file_path. E.g. `/home/myrepo/step/trainer.py` - returns `myrepo.step.trainer` if `myrepo` is the root of the repo. - - Args: - file_path: Absolute file path to a file within the module. - - -`get_module_source_from_source(source: str) ‑> str` -: Gets module source from source. E.g. `some.module.file.class@version`, - returns `some.module`. - - Args: - source: source pointing to potentially pinned sha. - - -`get_path_from_source(source: str) ‑> str` -: Get file path from source - - Args: - source: class_source e.g. this.module.Class. - - -`get_pin_from_source(source: str) ‑> Optional[str]` -: Gets pin from source, i.e. module.path@pin, returns pin. - - Args: - source: class_source e.g. this.module.Class[@pin]. - - -`get_relative_path_from_module_source(module_source: str) ‑> str` -: Get a directory path from module, relative to root of repository. - - E.g. zenml.core.step will return zenml/core/step. - - Args: - module_source: A module e.g. zenml.core.step - - -`is_standard_pin(pin: str) ‑> bool` -: Returns True if pin is valid ZenML pin, else False. - - Args: - pin: potential ZenML pin like 'zenml_0.1.1' - - -`is_standard_source(source: str) ‑> bool` -: Returns True if source is a standard ZenML source. - - Args: - source (str): class_source e.g. this.module.Class[@pin]. - - -`load_source_path_class(source: str) ‑> Type` -: Loads a Python class from the source. - - Args: - source: class_source e.g. this.module.Class[@sha] - - -`resolve_class(class_: Type) ‑> str` -: Resolves a a class into a serializable source string. - - Args: - class_: A Python Class reference. - - Returns: source_path e.g. this.module.Class. - - -`resolve_standard_source(source: str) ‑> str` -: Creates a ZenML pin for source pinning from release version. - - Args: - source: class_source e.g. this.module.Class. \ No newline at end of file diff --git a/docs/book/reference/api-reference/utils/yaml_utils.md b/docs/book/reference/api-reference/utils/yaml_utils.md deleted file mode 100644 index 2549448bee7..00000000000 --- a/docs/book/reference/api-reference/utils/yaml_utils.md +++ /dev/null @@ -1,60 +0,0 @@ -Module zenml.utils.yaml_utils -============================= - -Functions ---------- - - -`is_yaml(file_path: str) ‑> bool` -: Returns True if file_path is YAML, else False - - Args: - file_path: Path to YAML file. - - Returns: - True if is yaml, else False. - - -`read_json(file_path: str) ‑> Any` -: Read JSON on file path and returns contents as dict. - - Args: - file_path: Path to JSON file. - - -`read_yaml(file_path: str) ‑> Dict` -: Read YAML on file path and returns contents as dict. - - Args: - file_path(str): Path to YAML file. - - Returns: - Contents of the file in a dict. - - Raises: - FileNotFoundError if file does not exist. - - -`write_json(file_path: str, contents: Dict)` -: Write contents as JSON format to file_path. - - Args: - file_path: Path to JSON file. - contents: Contents of JSON file as dict. - - Returns: - Contents of the file in a dict. - - Raises: - FileNotFoundError if directory does not exist. - - -`write_yaml(file_path: str, contents: Dict)` -: Write contents as YAML format to file_path. - - Args: - file_path: Path to YAML file. - contents: Contents of YAML file as dict. - - Raises: - FileNotFoundError if directory does not exist. \ No newline at end of file From 51a1c04941865972cdcc7e04926399660863d96c Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 12 Nov 2021 16:35:28 +0100 Subject: [PATCH 05/29] Added base visualizer and facet visualzier --- .../post_execution/visualizers/__init__.py | 0 .../visualizers/base_visualizer.py | 27 +++++++ .../facet_statistics_visualizer.py | 72 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/zenml/post_execution/visualizers/__init__.py create mode 100644 src/zenml/post_execution/visualizers/base_visualizer.py create mode 100644 src/zenml/post_execution/visualizers/facet_statistics_visualizer.py diff --git a/src/zenml/post_execution/visualizers/__init__.py b/src/zenml/post_execution/visualizers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/zenml/post_execution/visualizers/base_visualizer.py b/src/zenml/post_execution/visualizers/base_visualizer.py new file mode 100644 index 00000000000..3325c673b7e --- /dev/null +++ b/src/zenml/post_execution/visualizers/base_visualizer.py @@ -0,0 +1,27 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +from zenml.logger import get_logger + +logger = get_logger(__name__) + + +class BaseVisualizer: + """The base implementation of a ZenML Visualizer.""" + + @abstractmethod + def visualize(self) -> None: + """Method to visualize components""" diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py new file mode 100644 index 00000000000..ddd20da2300 --- /dev/null +++ b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py @@ -0,0 +1,72 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +import base64 +import sys +import tempfile +import webbrowser +from abc import abstractmethod + +import pandas as pd +from facets_overview.generic_feature_statistics_generator import ( + GenericFeatureStatisticsGenerator, +) + +from zenml.logger import get_logger +from zenml.utils import path_utils + +logger = get_logger(__name__) + + +HTML_TEMPLATE = """ + + +""" + + +class FacetStatisticsVisualizer: + """The base implementation of a ZenML Visualizer.""" + + @abstractmethod + def visualize(self, df: pd.DataFrame, magic: bool = False) -> None: + """Method to visualize components""" + proto = GenericFeatureStatisticsGenerator().ProtoFromDataFrames( + [{"name": "Facet Overview", "table": df}] + ) + protostr = base64.b64encode(proto.SerializeToString()).decode("utf-8") + h = HTML_TEMPLATE.replace("protostr", protostr) + + if magic: + + if "ipykernel" not in sys.modules: + raise EnvironmentError( + "The magic functions are only usable " + "in a Jupyter notebook." + ) + from IPython.core.display import HTML, display + + display(HTML(h)) + else: + with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f: + path_utils.write_file_contents_as_string(f.name, h) + url = f"file:///{f.name}" + webbrowser.open(url, new=2) From 756c75fe1e219b7f47702398e125bed47f245233 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Sun, 14 Nov 2021 10:43:57 +0100 Subject: [PATCH 06/29] Visualizers now read from stats.html --- .../facet_statistics_visualizer.py | 23 ++++++---------- .../post_execution/visualizers/stats.html | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 src/zenml/post_execution/visualizers/stats.html diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py index ddd20da2300..5f9c29e6664 100644 --- a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. import base64 +import os import sys import tempfile import webbrowser @@ -29,20 +30,6 @@ logger = get_logger(__name__) -HTML_TEMPLATE = """ - - -""" - - class FacetStatisticsVisualizer: """The base implementation of a ZenML Visualizer.""" @@ -53,7 +40,13 @@ def visualize(self, df: pd.DataFrame, magic: bool = False) -> None: [{"name": "Facet Overview", "table": df}] ) protostr = base64.b64encode(proto.SerializeToString()).decode("utf-8") - h = HTML_TEMPLATE.replace("protostr", protostr) + + template = os.path.join( + os.path.abspath(os.path.dirname(__file__)), "stats.html" + ) + html_template = path_utils.read_file_contents_as_string(template) + + h = html_template.replace("protostr", protostr) if magic: diff --git a/src/zenml/post_execution/visualizers/stats.html b/src/zenml/post_execution/visualizers/stats.html new file mode 100644 index 00000000000..aa0ddce8133 --- /dev/null +++ b/src/zenml/post_execution/visualizers/stats.html @@ -0,0 +1,26 @@ + + + + \ No newline at end of file From 114ed24b8489abdd576e2822a588c6c21fa11c7d Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Sun, 14 Nov 2021 10:44:12 +0100 Subject: [PATCH 07/29] Added visualizer example --- examples/visualizers/README.md | 1 + examples/visualizers/__init__.py | 0 examples/visualizers/run.py | 133 +++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 examples/visualizers/README.md create mode 100644 examples/visualizers/__init__.py create mode 100644 examples/visualizers/run.py diff --git a/examples/visualizers/README.md b/examples/visualizers/README.md new file mode 100644 index 00000000000..2fd9f957002 --- /dev/null +++ b/examples/visualizers/README.md @@ -0,0 +1 @@ +TBD \ No newline at end of file diff --git a/examples/visualizers/__init__.py b/examples/visualizers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/visualizers/run.py b/examples/visualizers/run.py new file mode 100644 index 00000000000..9d4572392f5 --- /dev/null +++ b/examples/visualizers/run.py @@ -0,0 +1,133 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + + +import numpy as np +from zenml.post_execution.visualizers.facet_statistics_visualizer import ( + FacetStatisticsVisualizer, +) +import pandas as pd +import tensorflow as tf + +from zenml.pipelines import pipeline +from zenml.steps import step +from zenml.steps.step_output import Output +from zenml.core.repo import Repository + + +@step +def importer() -> Output( + X_train=np.ndarray, y_train=np.ndarray, X_test=np.ndarray, y_test=np.ndarray +): + """Download the MNIST data store it as numpy arrays.""" + (X_train, y_train), ( + X_test, + y_test, + ) = tf.keras.datasets.boston_housing.load_data() + return X_train, y_train, X_test, y_test + + +@step +def trainer( + X_train: np.ndarray, + y_train: np.ndarray, +) -> tf.keras.Model: + """A simple Keras Model to train on the data.""" + model = tf.keras.Sequential() + model.add(tf.keras.layers.Dense(64, activation='relu', input_shape=(X_train.shape[1],))) + model.add(tf.keras.layers.Dense(64, activation='relu')) + model.add(tf.keras.layers.Dense(1)) + + model.compile( + optimizer=tf.keras.optimizers.Adam(0.001), + loss=tf.keras.losses.MeanSquaredError(), + metrics=["mae"], + ) + + model.fit(X_train, y_train) + + # write model + return model + + +@step +def evaluator( + X_test: np.ndarray, + y_test: np.ndarray, + model: tf.keras.Model, +) -> float: + """Calculate the accuracy on the test set""" + test_acc = model.evaluate(X_test, y_test, verbose=2) + return test_acc + + +@pipeline +def boston_housing_pipeline( + importer, + trainer, + evaluator, +): + """Links all the steps together in a pipeline""" + X_train, y_train, X_test, y_test = importer() + model = trainer(X_train=X_train, y_train=y_train) + evaluator(X_test=X_test, y_test=y_test, model=model) + + +def convert_np_to_pandas(X, y): + cols = [ + "CRIM", + "ZN", + "INDUS", + "CHAS", + "NOX", + "RM", + "AGE", + "DIS", + "RAD", + "TAX", + "PTRATIO", + "B", + "STAT", + "MEDV", + ] + data = {} + for i in range(0, X.shape[0]): + for col in cols: + data[col] = X[i] + data['target'] = y[i] + return pd.DataFrame(data) + +if __name__ == "__main__": + # Run the pipeline + p = boston_housing_pipeline( + importer=importer(), + trainer=trainer(), + evaluator=evaluator(), + ) + p.run() + + repo = Repository() + pipe = repo.get_pipelines()[-1] + importer_outputs = pipe.runs[-1].get_step(name="importer").outputs + X_train = importer_outputs["X_train"].read() + X_test = importer_outputs["X_test"].read() + y_train = importer_outputs["y_train"].read() + y_test = importer_outputs["y_test"].read() + + train_df = convert_np_to_pandas(X_train, y_train) + test_df = convert_np_to_pandas(X_test, y_test) + + + + FacetStatisticsVisualizer().visualize(train_df) From 1e1530bfa3ff48c3f8fd31f092be76e963dc9978 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Sun, 14 Nov 2021 10:45:45 +0100 Subject: [PATCH 08/29] Updated stats example --- examples/visualizers/run.py | 45 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/examples/visualizers/run.py b/examples/visualizers/run.py index 9d4572392f5..4c3873598a8 100644 --- a/examples/visualizers/run.py +++ b/examples/visualizers/run.py @@ -14,16 +14,16 @@ import numpy as np -from zenml.post_execution.visualizers.facet_statistics_visualizer import ( - FacetStatisticsVisualizer, -) import pandas as pd import tensorflow as tf +from zenml.core.repo import Repository from zenml.pipelines import pipeline +from zenml.post_execution.visualizers.facet_statistics_visualizer import ( + FacetStatisticsVisualizer, +) from zenml.steps import step from zenml.steps.step_output import Output -from zenml.core.repo import Repository @step @@ -45,8 +45,12 @@ def trainer( ) -> tf.keras.Model: """A simple Keras Model to train on the data.""" model = tf.keras.Sequential() - model.add(tf.keras.layers.Dense(64, activation='relu', input_shape=(X_train.shape[1],))) - model.add(tf.keras.layers.Dense(64, activation='relu')) + model.add( + tf.keras.layers.Dense( + 64, activation="relu", input_shape=(X_train.shape[1],) + ) + ) + model.add(tf.keras.layers.Dense(64, activation="relu")) model.add(tf.keras.layers.Dense(1)) model.compile( @@ -105,29 +109,32 @@ def convert_np_to_pandas(X, y): for i in range(0, X.shape[0]): for col in cols: data[col] = X[i] - data['target'] = y[i] + data["target"] = y[i] return pd.DataFrame(data) -if __name__ == "__main__": - # Run the pipeline - p = boston_housing_pipeline( - importer=importer(), - trainer=trainer(), - evaluator=evaluator(), - ) - p.run() +def visualize_statistics(): repo = Repository() pipe = repo.get_pipelines()[-1] importer_outputs = pipe.runs[-1].get_step(name="importer").outputs X_train = importer_outputs["X_train"].read() - X_test = importer_outputs["X_test"].read() y_train = importer_outputs["y_train"].read() - y_test = importer_outputs["y_test"].read() + + # X_test = importer_outputs["X_test"].read() + # y_test = importer_outputs["y_test"].read() + # test_df = convert_np_to_pandas(X_test, y_test) train_df = convert_np_to_pandas(X_train, y_train) - test_df = convert_np_to_pandas(X_test, y_test) + FacetStatisticsVisualizer().visualize(train_df) +if __name__ == "__main__": + # Run the pipeline + p = boston_housing_pipeline( + importer=importer(), + trainer=trainer(), + evaluator=evaluator(), + ) + p.run() - FacetStatisticsVisualizer().visualize(train_df) + visualize_statistics() From e51baa83069396f3ff822267e5250f84af1ad467 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Sun, 14 Nov 2021 21:01:14 +0100 Subject: [PATCH 09/29] Visualizer now takes steps as input --- .../facet_statistics_visualizer.py | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py index 5f9c29e6664..ca0576a9cd1 100644 --- a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py @@ -18,13 +18,16 @@ import tempfile import webbrowser from abc import abstractmethod +from typing import Dict, List, Text import pandas as pd from facets_overview.generic_feature_statistics_generator import ( GenericFeatureStatisticsGenerator, ) +from IPython.core.display import HTML, display from zenml.logger import get_logger +from zenml.post_execution.step import StepView from zenml.utils import path_utils logger = get_logger(__name__) @@ -34,10 +37,31 @@ class FacetStatisticsVisualizer: """The base implementation of a ZenML Visualizer.""" @abstractmethod - def visualize(self, df: pd.DataFrame, magic: bool = False) -> None: - """Method to visualize components""" + def visualize(self, step: StepView, magic: bool = False) -> None: + """Method to visualize components + + Args: + step: StepView fetched from run.get_step(). + magic: Whether to render in a Jupyter notebook or not. + """ + datasets = [] + for output_name, artifact_view in step.outputs.items(): + df = artifact_view.read() + datasets.append({"name": output_name, "table": df}) + h = self.generate_html(datasets) + self.generate_facet(h, magic) + + def generate_html(self, datasets: List[Dict[Text, pd.DataFrame]]) -> str: + """Generates html for facet. + + Args: + datasets: List of dicts of dataframes to be visualized as stats. + + Returns: + HTML template with proto string embedded. + """ proto = GenericFeatureStatisticsGenerator().ProtoFromDataFrames( - [{"name": "Facet Overview", "table": df}] + datasets ) protostr = base64.b64encode(proto.SerializeToString()).decode("utf-8") @@ -47,16 +71,20 @@ def visualize(self, df: pd.DataFrame, magic: bool = False) -> None: html_template = path_utils.read_file_contents_as_string(template) h = html_template.replace("protostr", protostr) + return h - if magic: + def generate_facet(self, h: str, magic: bool = False) -> None: + """Generate a Facet Overview + Args: + h: HTML represented as a string. + magic: Whether to magically materialize facet in a notebook. + """ + if magic: if "ipykernel" not in sys.modules: raise EnvironmentError( - "The magic functions are only usable " - "in a Jupyter notebook." + "The magic functions are only usable in a Jupyter notebook." ) - from IPython.core.display import HTML, display - display(HTML(h)) else: with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f: From 675392a5d6f3a37078f22aa9bf93e9c29aff5a73 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Sun, 14 Nov 2021 21:01:44 +0100 Subject: [PATCH 10/29] Visualizer example updated --- examples/visualizers/run.py | 94 +++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 52 deletions(-) diff --git a/examples/visualizers/run.py b/examples/visualizers/run.py index 4c3873598a8..74e36fada68 100644 --- a/examples/visualizers/run.py +++ b/examples/visualizers/run.py @@ -13,7 +13,6 @@ # permissions and limitations under the License. -import numpy as np import pandas as pd import tensorflow as tf @@ -25,29 +24,52 @@ from zenml.steps import step from zenml.steps.step_output import Output +FEATURE_COLS = [ + "CRIM", + "ZN", + "INDUS", + "CHAS", + "NOX", + "RM", + "AGE", + "DIS", + "RAD", + "TAX", + "PTRATIO", + "B", + "STAT", + # "MEDV", +] +TARGET_COL_NAME = "target" + + +def convert_np_to_pandas(X, y): + df = pd.DataFrame(X, columns=FEATURE_COLS) + df[TARGET_COL_NAME] = y + return df + @step -def importer() -> Output( - X_train=np.ndarray, y_train=np.ndarray, X_test=np.ndarray, y_test=np.ndarray -): +def importer() -> Output(train_df=pd.DataFrame, test_df=pd.DataFrame): """Download the MNIST data store it as numpy arrays.""" (X_train, y_train), ( X_test, y_test, ) = tf.keras.datasets.boston_housing.load_data() - return X_train, y_train, X_test, y_test + train_df = convert_np_to_pandas(X_train, y_train) + test_df = convert_np_to_pandas(X_test, y_test) + return train_df, test_df @step -def trainer( - X_train: np.ndarray, - y_train: np.ndarray, -) -> tf.keras.Model: +def trainer(train_df: pd.DataFrame) -> tf.keras.Model: """A simple Keras Model to train on the data.""" model = tf.keras.Sequential() model.add( tf.keras.layers.Dense( - 64, activation="relu", input_shape=(X_train.shape[1],) + 64, + activation="relu", + input_shape=(len(FEATURE_COLS),), ) ) model.add(tf.keras.layers.Dense(64, activation="relu")) @@ -59,7 +81,7 @@ def trainer( metrics=["mae"], ) - model.fit(X_train, y_train) + model.fit(train_df[FEATURE_COLS], train_df[TARGET_COL_NAME]) # write model return model @@ -67,12 +89,13 @@ def trainer( @step def evaluator( - X_test: np.ndarray, - y_test: np.ndarray, + test_df: pd.DataFrame, model: tf.keras.Model, ) -> float: """Calculate the accuracy on the test set""" - test_acc = model.evaluate(X_test, y_test, verbose=2) + test_acc = model.evaluate( + test_df[FEATURE_COLS].values, test_df[TARGET_COL_NAME].values, verbose=2 + ) return test_acc @@ -83,49 +106,16 @@ def boston_housing_pipeline( evaluator, ): """Links all the steps together in a pipeline""" - X_train, y_train, X_test, y_test = importer() - model = trainer(X_train=X_train, y_train=y_train) - evaluator(X_test=X_test, y_test=y_test, model=model) - - -def convert_np_to_pandas(X, y): - cols = [ - "CRIM", - "ZN", - "INDUS", - "CHAS", - "NOX", - "RM", - "AGE", - "DIS", - "RAD", - "TAX", - "PTRATIO", - "B", - "STAT", - "MEDV", - ] - data = {} - for i in range(0, X.shape[0]): - for col in cols: - data[col] = X[i] - data["target"] = y[i] - return pd.DataFrame(data) + train_df, test_df = importer() + model = trainer(train_df=train_df) + evaluator(test_df=test_df, model=model) def visualize_statistics(): repo = Repository() pipe = repo.get_pipelines()[-1] - importer_outputs = pipe.runs[-1].get_step(name="importer").outputs - X_train = importer_outputs["X_train"].read() - y_train = importer_outputs["y_train"].read() - - # X_test = importer_outputs["X_test"].read() - # y_test = importer_outputs["y_test"].read() - # test_df = convert_np_to_pandas(X_test, y_test) - - train_df = convert_np_to_pandas(X_train, y_train) - FacetStatisticsVisualizer().visualize(train_df) + importer_outputs = pipe.runs[-1].get_step(name="importer") + FacetStatisticsVisualizer().visualize(importer_outputs) if __name__ == "__main__": From 103c0362f5b2365575be5c2dce304afe48301096 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Wed, 17 Nov 2021 13:32:43 +0100 Subject: [PATCH 11/29] Added base classes and setup --- .../visualizers/base_pipeline_visualizer.py | 28 +++++++++++++++++++ ..._visualizer.py => base_step_visualizer.py} | 9 +++--- .../facet_statistics_visualizer.py | 10 +++++-- stats.html | 26 +++++++++++++++++ 4 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 src/zenml/post_execution/visualizers/base_pipeline_visualizer.py rename src/zenml/post_execution/visualizers/{base_visualizer.py => base_step_visualizer.py} (75%) create mode 100644 stats.html diff --git a/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py new file mode 100644 index 00000000000..fa2bbf0cc33 --- /dev/null +++ b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py @@ -0,0 +1,28 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +from zenml.logger import get_logger +from zenml.post_execution.pipeline import PipelineView + +logger = get_logger(__name__) + + +class BaseStepVisualizer: + """The base implementation of a ZenML Step Visualizer.""" + + @abstractmethod + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + """Method to visualize pipelines""" diff --git a/src/zenml/post_execution/visualizers/base_visualizer.py b/src/zenml/post_execution/visualizers/base_step_visualizer.py similarity index 75% rename from src/zenml/post_execution/visualizers/base_visualizer.py rename to src/zenml/post_execution/visualizers/base_step_visualizer.py index 3325c673b7e..1ee37b54b89 100644 --- a/src/zenml/post_execution/visualizers/base_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_step_visualizer.py @@ -15,13 +15,14 @@ from abc import abstractmethod from zenml.logger import get_logger +from zenml.post_execution.step import StepView logger = get_logger(__name__) -class BaseVisualizer: - """The base implementation of a ZenML Visualizer.""" +class BaseStepVisualizer: + """The base implementation of a ZenML Step Visualizer.""" @abstractmethod - def visualize(self) -> None: - """Method to visualize components""" + def visualize(self, step: StepView, *args, **kwargs) -> None: + """Method to visualize steps.""" diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py index ca0576a9cd1..3e6d4610b89 100644 --- a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py @@ -28,16 +28,21 @@ from zenml.logger import get_logger from zenml.post_execution.step import StepView +from zenml.post_execution.visualizers.base_step_visualizer import ( + BaseStepVisualizer, +) from zenml.utils import path_utils logger = get_logger(__name__) -class FacetStatisticsVisualizer: +class FacetStatisticsVisualizer(BaseStepVisualizer): """The base implementation of a ZenML Visualizer.""" @abstractmethod - def visualize(self, step: StepView, magic: bool = False) -> None: + def visualize( + self, step: StepView, magic: bool = False, *args, **kwargs + ) -> None: """Method to visualize components Args: @@ -90,4 +95,5 @@ def generate_facet(self, h: str, magic: bool = False) -> None: with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f: path_utils.write_file_contents_as_string(f.name, h) url = f"file:///{f.name}" + logger.info("Opening %s in a new browser.." % f.name) webbrowser.open(url, new=2) diff --git a/stats.html b/stats.html new file mode 100644 index 00000000000..aa0ddce8133 --- /dev/null +++ b/stats.html @@ -0,0 +1,26 @@ + + + + \ No newline at end of file From 9a306f59ce04334dffaa85636b86900d8967484d Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Thu, 18 Nov 2021 10:10:30 +0100 Subject: [PATCH 12/29] Added check for pandas dataframes --- .../visualizers/facet_statistics_visualizer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py index 3e6d4610b89..98e9ba44946 100644 --- a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py @@ -52,7 +52,14 @@ def visualize( datasets = [] for output_name, artifact_view in step.outputs.items(): df = artifact_view.read() - datasets.append({"name": output_name, "table": df}) + if type(df) is not pd.DataFrame: + logger.warning( + "`%s` is not a pd.DataFrame. You can only visualize " + "statistics of steps that output pandas dataframes. " + "Skipping this output.." % output_name + ) + else: + datasets.append({"name": output_name, "table": df}) h = self.generate_html(datasets) self.generate_facet(h, magic) From 8a2b0eab73960adccd17dd003fa3645db18dee69 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 09:26:56 +0100 Subject: [PATCH 13/29] Merged and fixed conflict --- run.py | 8 + src/zenml/post_execution/pipeline.py | 36 ++++ .../post_execution/visualizers/lineage.py | 195 ++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 src/zenml/post_execution/visualizers/lineage.py diff --git a/run.py b/run.py index 285a05d8348..06a2025f21a 100644 --- a/run.py +++ b/run.py @@ -111,3 +111,11 @@ def my_pipeline( # needed for airflow DAG = split_pipeline.run() + + +from zenml.core.repo import Repository + +pipeline = Repository().get_pipelines()[-1] +pipeline.display_types +display_artifacts +display_properties \ No newline at end of file diff --git a/src/zenml/post_execution/pipeline.py b/src/zenml/post_execution/pipeline.py index 5bafd9540a1..4f3f27c2615 100644 --- a/src/zenml/post_execution/pipeline.py +++ b/src/zenml/post_execution/pipeline.py @@ -105,3 +105,39 @@ def __eq__(self, other: Any) -> bool: and self._metadata_store.uuid == other._metadata_store.uuid ) return NotImplemented + + def display_types(self, types): + # Helper function to render dataframes for the artifact and execution + # types + table = {"id": [], "name": []} + for a_type in types: + table["id"].append(a_type.id) + table["name"].append(a_type.name) + return table + + def display_artifacts(self, store, artifacts): + # Helper function to render dataframes for the input artifacts + table = {"artifact id": [], "type": [], "uri": []} + for a in artifacts: + table["artifact id"].append(a.id) + artifact_type = store.get_artifact_types_by_id([a.type_id])[0] + table["type"].append(artifact_type.name) + table["uri"].append(a.uri) + return table + + def display_properties(self, node): + # Helper function to render dataframes for artifact and execution + # properties + table = {"property": [], "value": []} + for k, v in node.properties.items(): + table["property"].append(k) + table["value"].append( + v.string_value if v.HasField("string_value") else v.int_value + ) + for k, v in node.custom_properties.items(): + table["property"].append(k) + table["value"].append( + v.string_value if v.HasField("string_value") else v.int_value + ) + return table + diff --git a/src/zenml/post_execution/visualizers/lineage.py b/src/zenml/post_execution/visualizers/lineage.py new file mode 100644 index 00000000000..ae5dae61188 --- /dev/null +++ b/src/zenml/post_execution/visualizers/lineage.py @@ -0,0 +1,195 @@ +import json + +import pandas as pd +import plotly.express as px + +from zenml.core.repo import Repository + +steps_to_artifacts = { + "datasource": ["DataGen", "DataSchema", "DataStatistics"], + "preprocesser": ["Transform"], + "sequencer": ["Sequencer", "SequencerStatistics", "SequencerSchema"], + "split": ["SplitGen", "SplitStatistics", "SplitSchema"], + "ImageGen": ["ImageGen"], +} + +artifact_types = ["output"] + + +def read_lineage_info(repo: Repository): + store = repo.get_active_stack().metadata_store + pipelines = repo.get_pipelines() + + result = [] + + for p in pipelines: + # Get the config and status of the pipeline + steps_config = p.get_steps_config()["steps"] + components_status = store.get_components_status(p) + + # Handle datasource + for a_type in artifact_types: + s = "datasource" + for component in steps_to_artifacts[s]: + artifacts = store.get_artifacts_by_component_and_type( + p, component, a_type + ) + e = store.get_execution_by_component(p, component)[0] + for a in artifacts: + result.append( + { + "pipeline": p.name, + "step": s, + "status": "committed", + "source": p.datasource._source, + "args": p.datasource._source_args, + "artifact": store.store.get_artifact_types_by_id( + [a.type_id] + )[0].name, + "artifact_type": a_type, + "artifact_uri": a.uri, + "artifact_id": a.id, + "execution_id": e.id, + } + ) + + # Handle steps + for s, config in steps_config.items(): + for component in steps_to_artifacts[s]: + artifacts = store.get_artifacts_by_component_and_type( + p, component, a_type + ) + e = store.get_execution_by_component(p, component)[0] + for a in artifacts: + result.append( + { + "pipeline": p.name, + "step": s, + "status": components_status[component], + "source": config["source"], + "args": config["args"], + "artifact": store.store.get_artifact_types_by_id( + [a.type_id] + )[ + 0 + ].name, + "artifact_type": a_type, + "artifact_uri": a.uri, + "artifact_id": a.id, + "execution_id": e.id, + } + ) + + # Handle additional components + extra_components = ["ImageGen"] + for s in extra_components: + for component in steps_to_artifacts[s]: + artifacts = store.get_artifacts_by_component_and_type( + p, component, a_type + ) + e = store.get_execution_by_component(p, component)[0] + for a in artifacts: + result.append( + { + "pipeline": p.name, + "step": s, + "status": components_status[s], + "source": f"zenml@{component}", + "args": "", + "artifact": store.store.get_artifact_types_by_id( + [a.type_id] + )[ + 0 + ].name, + "artifact_type": a_type, + "artifact_uri": a.uri, + "artifact_id": a.id, + "execution_id": e.id, + } + ) + + return result + + +def artifact_lineage_graph(repo: Repository): + result_df = pd.DataFrame(read_lineage_info(repo)) + + fig = px.treemap( + result_df, + path=["pipeline", "step", "artifact"], + hover_data={ + "source": True, + "args": True, + }, + color="status", + color_discrete_map={ + "(?)": "purple", + "committed": "orange", + "complete": "green", + "cached": "lightgreen", + }, + ) + fig.update_annotations(hoverinfo="skip") + + return fig + + +def pipeline_lineage_graph(repo: Repository): + category_df = {} + + for step_artifact in read_lineage_info(repo): + pipeline = step_artifact["pipeline"] + step = step_artifact["step"] + + if pipeline not in category_df: + category_df[pipeline] = {"pipeline": pipeline} + + category_df[pipeline].update( + { + f"{step}_status": step_artifact["status"], + f"{step}_source": step_artifact["source"] + .split("@")[0] + .split(".")[-1], + f"{step}_args": step_artifact["args"], + } + ) + + if f"{step}_artifact_ids" not in category_df[pipeline]: + category_df[pipeline][f"{step}_artifact_ids"] = set() + + category_df[pipeline][f"{step}_artifact_ids"].add( + step_artifact["artifact_id"] + ) + + for pipeline in category_df: + for step in steps_to_artifacts: + source = category_df[pipeline][f"{step}_source"] + output_artifacts = json.dumps( + sorted(category_df[pipeline][f"{step}_artifact_ids"]) + ) + + category_df[pipeline][step] = f"{source}-{output_artifacts}" + + category_df = pd.DataFrame.from_dict(category_df, "index") + + category_df = category_df.reset_index() + + fig = px.parallel_categories( + category_df, + dimensions=[ + "pipeline", + "datasource", + "sequencer", + "split", + "preprocesser", + "ImageGen", + ], + color=category_df.index, + labels="status", + ) + + return fig + + +repo = Repository() +artifact_lineage_graph(repo) From 331d5ffaf47be65cf785d218df905324da86e776 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 10:24:20 +0100 Subject: [PATCH 14/29] Added producer_step logic --- src/zenml/metadata/base_metadata_store.py | 114 ++++++++++++++-------- src/zenml/post_execution/artifact.py | 26 ++++- src/zenml/post_execution/lineage.md | 34 +++++++ src/zenml/post_execution/pipeline.py | 36 ------- 4 files changed, 134 insertions(+), 76 deletions(-) create mode 100644 src/zenml/post_execution/lineage.md diff --git a/src/zenml/metadata/base_metadata_store.py b/src/zenml/metadata/base_metadata_store.py index b41f9b785fa..13a892943ed 100644 --- a/src/zenml/metadata/base_metadata_store.py +++ b/src/zenml/metadata/base_metadata_store.py @@ -44,6 +44,8 @@ logger = get_logger(__name__) +BASE_STEP_PREFIX = "zenml.steps.base_step." + # TODO [ENG-132]: can we remove this registration? @metadata_store_factory.register(MLMetadataTypes.base) # type: ignore[misc] @@ -66,6 +68,57 @@ def get_tfx_metadata_config(self) -> metadata_store_pb2.ConnectionConfig: """Return tfx metadata config.""" raise NotImplementedError + @property + def step_type_mapping(self) -> Dict[int, str]: + """Maps type_id's to step names.""" + return { + type_.id: type_.name for type_ in self.store.get_execution_types() + } + + def _check_if_executions_belong_to_pipeline( + self, + executions: List[proto.Execution], + pipeline: PipelineView, + ) -> bool: + """Returns `True` if the executions are associated with the pipeline + context.""" + for execution in executions: + associated_contexts = self.store.get_contexts_by_execution( + execution.id + ) + for context in associated_contexts: + if context.id == pipeline._id: # noqa + return True + return False + + def _get_step_view_from_execution( + self, execution: proto.Execution + ) -> StepView: + """Get original StepView from an execution. + + Args: + execution: proto.Execution object from mlmd store. + + Returns: + Original `StepView` derived from the proto.Execution. + """ + step_name = self.step_type_mapping[execution.type_id] + # TODO [ENG-134]: why is the name like this? + if step_name.startswith(BASE_STEP_PREFIX): + step_name = step_name[len(BASE_STEP_PREFIX) :] + + step_parameters = { + k: json.loads(v.string_value) + for k, v in execution.custom_properties.items() + } + + return StepView( + id_=execution.id, + name=step_name, + parameters=step_parameters, + metadata_store=self, + ) + def get_serialization_dir(self) -> str: """Gets the local path where artifacts are stored.""" return os.path.join( @@ -104,22 +157,6 @@ def get_pipeline(self, pipeline_name: str) -> Optional[PipelineView]: logger.info("No pipelines found for name '%s'", pipeline_name) return None - def _check_if_executions_belong_to_pipeline( - self, - executions: List[proto.Execution], - pipeline: PipelineView, - ) -> bool: - """Returns `True` if the executions are associated with the pipeline - context.""" - for execution in executions: - associated_contexts = self.store.get_contexts_by_execution( - execution.id - ) - for context in associated_contexts: - if context.id == pipeline._id: # noqa - return True - return False - def get_pipeline_runs( self, pipeline: PipelineView ) -> Dict[str, PipelineRunView]: @@ -179,33 +216,12 @@ def get_pipeline_run_steps( self, pipeline_run: PipelineRunView ) -> Dict[str, StepView]: """Gets all steps for the given pipeline run.""" - # maps type_id's to step names - step_type_mapping: Dict[int, str] = { - type_.id: type_.name for type_ in self.store.get_execution_types() - } - steps: Dict[str, StepView] = OrderedDict() # reverse the executions as they get returned in reverse chronological # order from the metadata store for execution in reversed(pipeline_run._executions): # noqa - step_name = step_type_mapping[execution.type_id] - # TODO [ENG-134]: why is the name like this? - step_prefix = "zenml.steps.base_step." - if step_name.startswith(step_prefix): - step_name = step_name[len(step_prefix) :] - - step_parameters = { - k: json.loads(v.string_value) - for k, v in execution.custom_properties.items() - } - - step = StepView( - id_=execution.id, - name=step_name, - parameters=step_parameters, - metadata_store=self, - ) - steps[step_name] = step + step_name = self.step_type_mapping[execution.type_id] + steps[step_name] = self._get_step_view_from_execution(execution) logger.debug( "Fetched %d steps for pipeline run '%s'.", @@ -271,6 +287,7 @@ def get_step_artifacts( uri=artifact_proto.uri, materializer=materializer, data_type=data_type, + metadata_store=self, ) if event_proto.type == event_proto.INPUT: @@ -287,6 +304,25 @@ def get_step_artifacts( return inputs, outputs + def get_producer_step_from_artifact( + self, artifact: ArtifactView + ) -> StepView: + """Returns original StepView from an ArtifactView. + + Args: + artifact: ArtifactView to be queried. + + Returns: + Original StepView that produced the artifact. + """ + executions_ids = set( + event.execution_id + for event in self.store.get_events_by_artifact_ids([artifact.id]) + if event.type == event.OUTPUT + ) + execution = self.store.get_executions_by_id(executions_ids)[0] + return self._get_step_view_from_execution(execution) + class Config: """Configuration of settings.""" diff --git a/src/zenml/post_execution/artifact.py b/src/zenml/post_execution/artifact.py index e270a1e24db..47bb914bb28 100644 --- a/src/zenml/post_execution/artifact.py +++ b/src/zenml/post_execution/artifact.py @@ -12,12 +12,16 @@ # or implied. See the License for the specific language governing # permissions and limitations under the License. -from typing import Any, Optional, Type +from typing import TYPE_CHECKING, Any, Optional, Type from zenml.logger import get_logger from zenml.materializers.base_materializer import BaseMaterializer from zenml.utils import source_utils +if TYPE_CHECKING: + from zenml.metadata.base_metadata_store import BaseMetadataStore + from zenml.post_execution.step import StepView + logger = get_logger(__name__) @@ -33,6 +37,7 @@ def __init__( uri: str, materializer: str, data_type: str, + metadata_store: "BaseMetadataStore", ): """Initializes a post-execution artifact object. @@ -48,12 +53,21 @@ def __init__( data_type: The type of data that was passed to the materializer when writing that artifact. Will be used as a default type to read the artifact. + metadata_store: The metadata store which should be used to fetch + additional information related to this pipeline. """ self._id = id_ self._type = type_ self._uri = uri self._materializer = materializer self._data_type = data_type + self._producer_step = None + self._metadata_store = metadata_store + + @property + def id(self) -> int: + """Returns the artifact id.""" + return self.id @property def type(self) -> str: @@ -65,6 +79,16 @@ def uri(self) -> str: """Returns the URI where the artifact data is stored.""" return self._uri + @property + def producer_step(self) -> "StepView": + """Returns the original StepView that produced the artifact.""" + # TODO [LOW]: Replace with artifact.id instead of passing self if + # required. + self._producer_step = ( + self._metadata_store.get_producer_step_from_artifact(self) + ) + return self._producer_step + def read( self, output_data_type: Optional[Type[Any]] = None, diff --git a/src/zenml/post_execution/lineage.md b/src/zenml/post_execution/lineage.md new file mode 100644 index 00000000000..8ee131742eb --- /dev/null +++ b/src/zenml/post_execution/lineage.md @@ -0,0 +1,34 @@ +# Pipeline Lineage + +```shell +pipeline = repo.get_pipelines()[-1] +run = pipeline.get_runs()[-1] + +run.get_lineage() + +``` + +# Lineage Object +```shell +class LineageObject: + parent = None + children = List + is_root: bool = False + + def get_children(): + pass + +class Lineage: + lineage_objects: List[LineageObject] + root_node: LineageObject +``` + + +# ArtifactViews +* Create a relation that identifies which step an artifact was produced by (`artifact.producer_step`) +* Create a relation that identifies which steps used an artifact (`artifact.consumer_steps`) + +``` + +def get_ +``` \ No newline at end of file diff --git a/src/zenml/post_execution/pipeline.py b/src/zenml/post_execution/pipeline.py index 4f3f27c2615..5bafd9540a1 100644 --- a/src/zenml/post_execution/pipeline.py +++ b/src/zenml/post_execution/pipeline.py @@ -105,39 +105,3 @@ def __eq__(self, other: Any) -> bool: and self._metadata_store.uuid == other._metadata_store.uuid ) return NotImplemented - - def display_types(self, types): - # Helper function to render dataframes for the artifact and execution - # types - table = {"id": [], "name": []} - for a_type in types: - table["id"].append(a_type.id) - table["name"].append(a_type.name) - return table - - def display_artifacts(self, store, artifacts): - # Helper function to render dataframes for the input artifacts - table = {"artifact id": [], "type": [], "uri": []} - for a in artifacts: - table["artifact id"].append(a.id) - artifact_type = store.get_artifact_types_by_id([a.type_id])[0] - table["type"].append(artifact_type.name) - table["uri"].append(a.uri) - return table - - def display_properties(self, node): - # Helper function to render dataframes for artifact and execution - # properties - table = {"property": [], "value": []} - for k, v in node.properties.items(): - table["property"].append(k) - table["value"].append( - v.string_value if v.HasField("string_value") else v.int_value - ) - for k, v in node.custom_properties.items(): - table["property"].append(k) - table["value"].append( - v.string_value if v.HasField("string_value") else v.int_value - ) - return table - From 282b3b316f8d2fb052d7c7bf72c893fd73ccfd22 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 11:59:25 +0100 Subject: [PATCH 15/29] add bool to build in materializer --- src/zenml/materializers/built_in_materializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zenml/materializers/built_in_materializer.py b/src/zenml/materializers/built_in_materializer.py index 1e7e9cc01d1..47bcd63a434 100644 --- a/src/zenml/materializers/built_in_materializer.py +++ b/src/zenml/materializers/built_in_materializer.py @@ -33,6 +33,7 @@ class BuiltInMaterializer(BaseMaterializer): float, list, tuple, + bool, ] def handle_input(self, data_type: Type[Any]) -> Any: From a6d34c288f1ed9f605c966f77493662acf0c6e4c Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 11:59:51 +0100 Subject: [PATCH 16/29] Added cached to step status --- src/zenml/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/zenml/enums.py b/src/zenml/enums.py index db70e63e3c3..4c6130a4cbd 100644 --- a/src/zenml/enums.py +++ b/src/zenml/enums.py @@ -65,6 +65,7 @@ class ExecutionStatus(Enum): FAILED = "failed" COMPLETED = "completed" RUNNING = "running" + CACHED = "cached" class LoggingLevels(Enum): From c15ceb5343d63b2c1c6c76aaf9d5c61197a1c6f9 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 12:00:08 +0100 Subject: [PATCH 17/29] added ID property to step --- src/zenml/post_execution/step.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/zenml/post_execution/step.py b/src/zenml/post_execution/step.py index a211e881309..3d35e0d40bf 100644 --- a/src/zenml/post_execution/step.py +++ b/src/zenml/post_execution/step.py @@ -53,6 +53,11 @@ def __init__( self._inputs: Dict[str, ArtifactView] = {} self._outputs: Dict[str, ArtifactView] = {} + @property + def id(self) -> int: + """Returns the step id.""" + return self._id + @property def name(self) -> str: """Returns the step name. From 16d8a03b3a73e9d0c5ed4c246eea4223f69d727b Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 12:00:27 +0100 Subject: [PATCH 18/29] Status added to pipeline run --- src/zenml/post_execution/pipeline_run.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/zenml/post_execution/pipeline_run.py b/src/zenml/post_execution/pipeline_run.py index 453925d87af..a6171100cfe 100644 --- a/src/zenml/post_execution/pipeline_run.py +++ b/src/zenml/post_execution/pipeline_run.py @@ -71,7 +71,9 @@ def status(self) -> ExecutionStatus: if any(status == ExecutionStatus.FAILED for status in step_statuses): return ExecutionStatus.FAILED elif all( - status == ExecutionStatus.COMPLETED for status in step_statuses + status == ExecutionStatus.COMPLETED + or status == ExecutionStatus.CACHED + for status in step_statuses ): return ExecutionStatus.COMPLETED else: From f2331364ac936ce48a6da955c874a86364793729 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 12:01:05 +0100 Subject: [PATCH 19/29] added logic for parent step and caching for lineage --- src/zenml/metadata/base_metadata_store.py | 9 ++++++--- src/zenml/post_execution/artifact.py | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/zenml/metadata/base_metadata_store.py b/src/zenml/metadata/base_metadata_store.py index 13a892943ed..dd5ef8da9db 100644 --- a/src/zenml/metadata/base_metadata_store.py +++ b/src/zenml/metadata/base_metadata_store.py @@ -220,8 +220,8 @@ def get_pipeline_run_steps( # reverse the executions as they get returned in reverse chronological # order from the metadata store for execution in reversed(pipeline_run._executions): # noqa - step_name = self.step_type_mapping[execution.type_id] - steps[step_name] = self._get_step_view_from_execution(execution) + step = self._get_step_view_from_execution(execution) + steps[step.name] = step logger.debug( "Fetched %d steps for pipeline run '%s'.", @@ -236,10 +236,12 @@ def get_step_status(self, step: StepView) -> ExecutionStatus: proto = self.store.get_executions_by_id([step._id])[0] # noqa state = proto.last_known_state - if state == proto.COMPLETE or state == proto.CACHED: + if state == proto.COMPLETE: return ExecutionStatus.COMPLETED elif state == proto.RUNNING: return ExecutionStatus.RUNNING + elif state == proto.CACHED: + return ExecutionStatus.CACHED else: return ExecutionStatus.FAILED @@ -288,6 +290,7 @@ def get_step_artifacts( materializer=materializer, data_type=data_type, metadata_store=self, + parent_step_id=step.id, ) if event_proto.type == event_proto.INPUT: diff --git a/src/zenml/post_execution/artifact.py b/src/zenml/post_execution/artifact.py index 47bb914bb28..f3efffa9e6a 100644 --- a/src/zenml/post_execution/artifact.py +++ b/src/zenml/post_execution/artifact.py @@ -38,6 +38,7 @@ def __init__( materializer: str, data_type: str, metadata_store: "BaseMetadataStore", + parent_step_id: int, ): """Initializes a post-execution artifact object. @@ -55,6 +56,7 @@ def __init__( to read the artifact. metadata_store: The metadata store which should be used to fetch additional information related to this pipeline. + parent_step_id: The ID of the parent step. """ self._id = id_ self._type = type_ @@ -63,11 +65,12 @@ def __init__( self._data_type = data_type self._producer_step = None self._metadata_store = metadata_store + self._parent_step_id = parent_step_id @property def id(self) -> int: """Returns the artifact id.""" - return self.id + return self._id @property def type(self) -> str: @@ -79,6 +82,12 @@ def uri(self) -> str: """Returns the URI where the artifact data is stored.""" return self._uri + @property + def parent_step_id(self) -> int: + """Returns the ID of the parent step. This need not be equivalent to + the ID of the producer step.""" + return self._parent_step_id + @property def producer_step(self) -> "StepView": """Returns the original StepView that produced the artifact.""" @@ -89,6 +98,12 @@ def producer_step(self) -> "StepView": ) return self._producer_step + @property + def is_cached(self) -> bool: + """Returns True if artifact was cached in a previous run, else False.""" + # self._metadata_store. + return self.producer_step.id != self.parent_step_id + def read( self, output_data_type: Optional[Type[Any]] = None, From 095f5e0ee9d3ef85c545d2c1979962eba9f68244 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 12:04:10 +0100 Subject: [PATCH 20/29] disabled facets_overview if not installed --- .../post_execution/visualizers/__init__.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/zenml/post_execution/visualizers/__init__.py b/src/zenml/post_execution/visualizers/__init__.py index e69de29bb2d..559cfb95472 100644 --- a/src/zenml/post_execution/visualizers/__init__.py +++ b/src/zenml/post_execution/visualizers/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from zenml.logger import get_logger + +logger = get_logger(__name__) + + +try: + from zenml.post_execution.visualizers.facet_statistics_visualizer import ( # noqa + FacetStatisticsVisualizer, + ) +except ImportError: + logger.debug("`facets_overview` not installed.") From e3a03a8dc1b37482e16a8b5519270c004611f134 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 12:04:45 +0100 Subject: [PATCH 21/29] Edited run to be for lineage --- run.py | 122 ++++++++++++++++++++++----------------------------------- 1 file changed, 46 insertions(+), 76 deletions(-) diff --git a/run.py b/run.py index 06a2025f21a..4dccfcf9df6 100644 --- a/run.py +++ b/run.py @@ -13,109 +13,79 @@ # permissions and limitations under the License. import os -from typing import Type + +from zenml.core.repo import Repository os.environ["ZENML_DEBUG"] = "true" import pandas as pd -import tensorflow as tf -from zenml.materializers.pandas_materializer import PandasMaterializer from zenml.pipelines import pipeline from zenml.steps import step -from zenml.steps.base_step_config import BaseStepConfig -from zenml.steps.step_output import Output - - -class StepConfig(BaseStepConfig): - basic_param_1: int = 1 - basic_param_2: str = 2 - - -class PandasJSONMaterializer(PandasMaterializer): - DATA_FILENAME = "data.json" - - def handle_input(self, data_type: Type) -> pd.DataFrame: - """Reads all files inside the artifact directory and concatenates - them to a pandas dataframe.""" - return pd.read_json(os.path.join(self.artifact.uri, self.DATA_FILENAME)) - - def handle_return(self, df: pd.DataFrame): - """Writes a pandas dataframe to the specified filename. - - Args: - df: The pandas dataframe to write. - """ - filepath = os.path.join(self.artifact.uri, self.DATA_FILENAME) - df.to_json(filepath) - - -@step -def number_returner( - config: StepConfig, -) -> Output(number=int, non_number=int): - return config.basic_param_1 + int(config.basic_param_2), "test" @step -def import_dataframe_csv(sum: int) -> pd.DataFrame: - return pd.DataFrame({"sum": [sum]}) +def importer() -> pd.DataFrame: + return pd.DataFrame({"X_train": [1, 2, 3], "y_train": [0, 0, 0]}) @step -def import_dataframe_json(sum: int) -> pd.DataFrame: - return pd.DataFrame({"sum": [sum]}) +def preprocesser(df: pd.DataFrame) -> pd.DataFrame: + return df @step -def tf_dataset_step() -> tf.data.Dataset: - return tf.data.Dataset.from_tensor_slices([8, 3, 0, 8, 2, 1]) +def trainer(df: pd.DataFrame) -> int: + return 2 @step -def last_step_1(df: pd.DataFrame, dataset: tf.data.Dataset) -> pd.DataFrame: - return df +def evaluator(df: pd.DataFrame, model: int) -> int: + return 2 @step -def last_step_2(df: pd.DataFrame) -> pd.DataFrame: - return df +def deployer(model: int, evaluation_results: int) -> bool: + return True -@pipeline(enable_cache=False) -def my_pipeline( - step_1, - step_2_1, - step_2_2, - step_3_1, - step_3_2, - tf_dataset, -): - number, non_number = step_1() - df_csv = step_2_1(sum=number) - df_json = step_2_2(sum=number) - - dataset = tf_dataset() - step_3_1(df=df_csv, dataset=dataset) - step_3_2(df=df_json) +@pipeline +def my_pipeline(importer, preprocesser, trainer, evaluator, deployer): + df = importer() + preprocessed_df = preprocesser(df=df) + model = trainer(df=preprocessed_df) + evaluation_results = evaluator(df=df, model=model) + deployer(model=model, evaluation_results=evaluation_results) # Pipeline -split_pipeline = my_pipeline( - step_1=number_returner(config=StepConfig(basic_param_2="2")), - step_2_1=import_dataframe_csv(), - step_2_2=import_dataframe_json(), - step_3_1=last_step_1(), - step_3_2=last_step_2(), - tf_dataset=tf_dataset_step(), +lineage_pipeline = my_pipeline( + importer=importer(), + preprocesser=preprocesser(), + trainer=trainer(), + evaluator=evaluator(), + deployer=deployer(), ) -# needed for airflow -DAG = split_pipeline.run() - - -from zenml.core.repo import Repository +lineage_pipeline.run() pipeline = Repository().get_pipelines()[-1] -pipeline.display_types -display_artifacts -display_properties \ No newline at end of file + +for run in pipeline.runs: + try: + deployer_step = run.get_step(name="deployer") + trainer_step = run.get_step(name="trainer") + deployed_model_artifact = deployer_step.inputs["model"] + trained_model_artifact = trainer_step.output + + # lets do the lineage + print( + f"trained_model_artifact was produced by: " + f"{trained_model_artifact.producer_step.id} and is_cached: " + f"{trained_model_artifact.is_cached} step cached: " + f"{trainer_step.status}" + ) + except Exception as e: + if "No step found for name `deployer`" in str(e): + pass + else: + raise e From 0b2066075ce16db54e627e8f505de490a340a31e Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 14:36:49 +0100 Subject: [PATCH 22/29] Examples updated --- .../assets/statistics_boston_housing.png | Bin 0 -> 121447 bytes examples/README.md | 1 + examples/{visualizers => }/__init__.py | 0 examples/visualizers/README.md | 1 - examples/visualizers/statistics/README.md | 64 ++++++++++++++++++ examples/visualizers/statistics/__init__.py | 0 examples/visualizers/{ => statistics}/run.py | 0 .../base_pipeline_run_visualizer.py | 28 ++++++++ .../pipeline_lineage_visualizer.py | 31 +++++++++ .../pipeline_run_lineage_visualizer.py | 28 ++++++++ 10 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 docs/book/.gitbook/assets/statistics_boston_housing.png rename examples/{visualizers => }/__init__.py (100%) delete mode 100644 examples/visualizers/README.md create mode 100644 examples/visualizers/statistics/README.md create mode 100644 examples/visualizers/statistics/__init__.py rename examples/visualizers/{ => statistics}/run.py (100%) create mode 100644 src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py create mode 100644 src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py create mode 100644 src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py diff --git a/docs/book/.gitbook/assets/statistics_boston_housing.png b/docs/book/.gitbook/assets/statistics_boston_housing.png new file mode 100644 index 0000000000000000000000000000000000000000..5737bfa6fd2524c7d712edd29b21a876fd7a99c3 GIT binary patch literal 121447 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

xV;30)bh&8 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 z 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 M_LNMLwEoTi1wgtiGXMYp literal 0 HcmV?d00001 diff --git a/examples/README.md b/examples/README.md index d8b776f2a30..b852a40bdb0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,6 +6,7 @@ Here you can find a list of practical examples on how you can use ZenML with bri - **low_level_guide**: All the code for the low-level API guide found in the [docs](https://docs.zenml.io). - **not_so_quickstart**: Shows of the modularity of the pipelines with hot-swapping of Tensorflow, PyTorch, and scikit-learn trainers. - **quickstart**: The official quickstart tutorial. +- **visualizers**: A set of examples to show-case standard `Visualizers` for ZenML. For each of these examples, ZenML provides a handly CLI command to pull them directly into your local environment. First install `zenml`: diff --git a/examples/visualizers/__init__.py b/examples/__init__.py similarity index 100% rename from examples/visualizers/__init__.py rename to examples/__init__.py diff --git a/examples/visualizers/README.md b/examples/visualizers/README.md deleted file mode 100644 index 2fd9f957002..00000000000 --- a/examples/visualizers/README.md +++ /dev/null @@ -1 +0,0 @@ -TBD \ No newline at end of file diff --git a/examples/visualizers/statistics/README.md b/examples/visualizers/statistics/README.md new file mode 100644 index 00000000000..56fbc0e9bd3 --- /dev/null +++ b/examples/visualizers/statistics/README.md @@ -0,0 +1,64 @@ +# Visualize statistics +This examples show-cases the built-in `FacetStatisticsVisualizer` using the [Facets Overview](https://pypi.org/project/facets-overview/) integration. +[Facets](https://pair-code.github.io/facets/) is an awesome project that helps user visualize large amounts of data in a coherent way. + +## Visualizers +Visualizers are python classes that take post-execution view objects (e.g. `PipelineView`, `PipelineRunView`, `StepView`, etc.) and create +visualizations for them. ZenML will support many standard ones but one can always extend them using the `BaseVisualization` classes. + +## Overview +Here, we are using the [Boston Housing Price Regression](https://keras.io/api/datasets/boston_housing/) dataset. We create a simple pipeline that +return two pd.DataFrames, one for the training data and one for the test data. In the post-execution workflow we then plug in the visualization class +that visualizes the statistics of these dataframes for us. + +This visualization is produced with the following code: + +```python +from zenml.core.repo import Repository +from zenml.post_execution.visualizers.facet_statistics_visualizer import ( + FacetStatisticsVisualizer, +) + +repo = Repository() +pipe = repo.get_pipelines()[-1] +importer_outputs = pipe.runs[-1].get_step(name="importer") +FacetStatisticsVisualizer().visualize(importer_outputs) +``` + +It produces the following visualization: + +![Statistics for boston housing dataset](../../../docs/book/.gitbook/assets/statistics_boston_housing.png) + + + +## Run it locally + +### Pre-requisites +In order to run this example, you need to install and initialize ZenML: + +```shell +# install CLI +pip install zenml tensorflow facets-overview + +# pull example +zenml example pull visualizers +cd zenml_examples/visualizers/statistics + +# initialize +git init +zenml init +``` + +### Run the project +Now we're ready. Execute: + +```bash +python run.py +``` + +### Clean up +In order to clean up, delete the remaining zenml references. + +```shell +rm -rf zenml_examples +``` \ No newline at end of file diff --git a/examples/visualizers/statistics/__init__.py b/examples/visualizers/statistics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/visualizers/run.py b/examples/visualizers/statistics/run.py similarity index 100% rename from examples/visualizers/run.py rename to examples/visualizers/statistics/run.py diff --git a/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py new file mode 100644 index 00000000000..f1415d44f9b --- /dev/null +++ b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py @@ -0,0 +1,28 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +from zenml.logger import get_logger +from zenml.post_execution.pipeline import PipelineView + +logger = get_logger(__name__) + + +class BasePipelineVisualizer: + """The base implementation of a ZenML Step Visualizer.""" + + @abstractmethod + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + """Method to visualize pipelines""" diff --git a/src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py b/src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py new file mode 100644 index 00000000000..a9cd8eb604f --- /dev/null +++ b/src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py @@ -0,0 +1,31 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +from zenml.logger import get_logger +from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.visualizers.base_pipeline_visualizer import ( + BasePipelineVisualizer, +) + +logger = get_logger(__name__) + + +class PipelineLineageVisualizer(BasePipelineVisualizer): + """The base implementation of a ZenML Step Visualizer.""" + + @abstractmethod + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + """Method to visualize pipelines""" diff --git a/src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py b/src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py new file mode 100644 index 00000000000..b447332afc8 --- /dev/null +++ b/src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py @@ -0,0 +1,28 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +from zenml.logger import get_logger +from zenml.post_execution.pipeline import PipelineView + +logger = get_logger(__name__) + + +class PipelineLineageVisualizer: + """The base implementation of a ZenML Step Visualizer.""" + + @abstractmethod + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + """Method to visualize pipelines""" From 6c72cdf9f3dff330aea78747817801fd42909f21 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 14:42:08 +0100 Subject: [PATCH 23/29] Added more structure to the visualizers --- examples/visualizers/statistics/README.md | 2 +- examples/visualizers/statistics/run.py | 2 +- .../post_execution/visualizers/__init__.py | 12 +- .../base_pipeline_run_visualizer.py | 11 +- .../visualizers/base_pipeline_visualizer.py | 7 +- .../visualizers/base_step_visualizer.py | 3 +- ...neage_visualizer.py => base_visualizer.py} | 12 +- .../post_execution/visualizers/lineage.py | 195 ------------------ .../visualizers/lineage/__init__.py | 0 .../lineage/pipeline_lineage_visualizer.py | 71 +++++++ .../pipeline_run_lineage_visualizer.py | 5 +- .../visualizers/statistics/__init__.py | 0 .../facet_statistics_visualizer.py | 2 +- 13 files changed, 105 insertions(+), 217 deletions(-) rename src/zenml/post_execution/visualizers/{pipeline_lineage_visualizer.py => base_visualizer.py} (65%) delete mode 100644 src/zenml/post_execution/visualizers/lineage.py create mode 100644 src/zenml/post_execution/visualizers/lineage/__init__.py create mode 100644 src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py rename src/zenml/post_execution/visualizers/{ => lineage}/pipeline_run_lineage_visualizer.py (84%) create mode 100644 src/zenml/post_execution/visualizers/statistics/__init__.py rename src/zenml/post_execution/visualizers/{ => statistics}/facet_statistics_visualizer.py (98%) diff --git a/examples/visualizers/statistics/README.md b/examples/visualizers/statistics/README.md index 56fbc0e9bd3..1d88a6176cb 100644 --- a/examples/visualizers/statistics/README.md +++ b/examples/visualizers/statistics/README.md @@ -15,7 +15,7 @@ This visualization is produced with the following code: ```python from zenml.core.repo import Repository -from zenml.post_execution.visualizers.facet_statistics_visualizer import ( +from zenml.post_execution.visualizers.statistics.facet_statistics_visualizer import ( FacetStatisticsVisualizer, ) diff --git a/examples/visualizers/statistics/run.py b/examples/visualizers/statistics/run.py index 74e36fada68..9583402603f 100644 --- a/examples/visualizers/statistics/run.py +++ b/examples/visualizers/statistics/run.py @@ -18,7 +18,7 @@ from zenml.core.repo import Repository from zenml.pipelines import pipeline -from zenml.post_execution.visualizers.facet_statistics_visualizer import ( +from zenml.post_execution.visualizers.statistics.facet_statistics_visualizer import ( FacetStatisticsVisualizer, ) from zenml.steps import step diff --git a/src/zenml/post_execution/visualizers/__init__.py b/src/zenml/post_execution/visualizers/__init__.py index 559cfb95472..6f4d2c06652 100644 --- a/src/zenml/post_execution/visualizers/__init__.py +++ b/src/zenml/post_execution/visualizers/__init__.py @@ -18,8 +18,18 @@ try: - from zenml.post_execution.visualizers.facet_statistics_visualizer import ( # noqa + from zenml.post_execution.visualizers.statistics.facet_statistics_visualizer import ( # noqa FacetStatisticsVisualizer, ) except ImportError: logger.debug("`facets_overview` not installed.") + +try: + from zenml.post_execution.visualizers.lineage.pipeline_lineage_visualizer import ( # noqa + PipelineLineageVisualizer, + ) + from zenml.post_execution.visualizers.lineage.pipeline_run_lineage_visualizer import ( # noqa + PipelineRunLineageVisualizer, + ) +except ImportError: + logger.debug("`plotly` not installed.") diff --git a/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py index f1415d44f9b..69f8f029dea 100644 --- a/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py @@ -15,14 +15,15 @@ from abc import abstractmethod from zenml.logger import get_logger -from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.pipeline_run import PipelineRunView +from zenml.post_execution.visualizers.base_visualizer import BaseVisualizer logger = get_logger(__name__) -class BasePipelineVisualizer: - """The base implementation of a ZenML Step Visualizer.""" +class BasePipelineRunVisualizer(BaseVisualizer): + """The base implementation of a ZenML Pipeline Run Visualizer.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: - """Method to visualize pipelines""" + def visualize(self, pipeline: PipelineRunView, *args, **kwargs) -> None: + """Method to visualize pipeline runs.""" diff --git a/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py index fa2bbf0cc33..1592f82cd78 100644 --- a/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py @@ -16,13 +16,14 @@ from zenml.logger import get_logger from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.visualizers.base_visualizer import BaseVisualizer logger = get_logger(__name__) -class BaseStepVisualizer: - """The base implementation of a ZenML Step Visualizer.""" +class BasePipelineVisualizer(BaseVisualizer): + """The base implementation of a ZenML Pipeline Visualizer.""" @abstractmethod def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: - """Method to visualize pipelines""" + """Method to visualize pipelines.""" diff --git a/src/zenml/post_execution/visualizers/base_step_visualizer.py b/src/zenml/post_execution/visualizers/base_step_visualizer.py index 1ee37b54b89..c8ce86d5ee4 100644 --- a/src/zenml/post_execution/visualizers/base_step_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_step_visualizer.py @@ -16,11 +16,12 @@ from zenml.logger import get_logger from zenml.post_execution.step import StepView +from zenml.post_execution.visualizers.base_visualizer import BaseVisualizer logger = get_logger(__name__) -class BaseStepVisualizer: +class BaseStepVisualizer(BaseVisualizer): """The base implementation of a ZenML Step Visualizer.""" @abstractmethod diff --git a/src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py b/src/zenml/post_execution/visualizers/base_visualizer.py similarity index 65% rename from src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py rename to src/zenml/post_execution/visualizers/base_visualizer.py index a9cd8eb604f..bd341ef8ff3 100644 --- a/src/zenml/post_execution/visualizers/pipeline_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_visualizer.py @@ -15,17 +15,13 @@ from abc import abstractmethod from zenml.logger import get_logger -from zenml.post_execution.pipeline import PipelineView -from zenml.post_execution.visualizers.base_pipeline_visualizer import ( - BasePipelineVisualizer, -) logger = get_logger(__name__) -class PipelineLineageVisualizer(BasePipelineVisualizer): - """The base implementation of a ZenML Step Visualizer.""" +class BaseVisualizer: + """Base class for all ZenML Visualizers.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: - """Method to visualize pipelines""" + def visualize(self, *args, **kwargs) -> None: + """Method to visualize objects.""" diff --git a/src/zenml/post_execution/visualizers/lineage.py b/src/zenml/post_execution/visualizers/lineage.py deleted file mode 100644 index ae5dae61188..00000000000 --- a/src/zenml/post_execution/visualizers/lineage.py +++ /dev/null @@ -1,195 +0,0 @@ -import json - -import pandas as pd -import plotly.express as px - -from zenml.core.repo import Repository - -steps_to_artifacts = { - "datasource": ["DataGen", "DataSchema", "DataStatistics"], - "preprocesser": ["Transform"], - "sequencer": ["Sequencer", "SequencerStatistics", "SequencerSchema"], - "split": ["SplitGen", "SplitStatistics", "SplitSchema"], - "ImageGen": ["ImageGen"], -} - -artifact_types = ["output"] - - -def read_lineage_info(repo: Repository): - store = repo.get_active_stack().metadata_store - pipelines = repo.get_pipelines() - - result = [] - - for p in pipelines: - # Get the config and status of the pipeline - steps_config = p.get_steps_config()["steps"] - components_status = store.get_components_status(p) - - # Handle datasource - for a_type in artifact_types: - s = "datasource" - for component in steps_to_artifacts[s]: - artifacts = store.get_artifacts_by_component_and_type( - p, component, a_type - ) - e = store.get_execution_by_component(p, component)[0] - for a in artifacts: - result.append( - { - "pipeline": p.name, - "step": s, - "status": "committed", - "source": p.datasource._source, - "args": p.datasource._source_args, - "artifact": store.store.get_artifact_types_by_id( - [a.type_id] - )[0].name, - "artifact_type": a_type, - "artifact_uri": a.uri, - "artifact_id": a.id, - "execution_id": e.id, - } - ) - - # Handle steps - for s, config in steps_config.items(): - for component in steps_to_artifacts[s]: - artifacts = store.get_artifacts_by_component_and_type( - p, component, a_type - ) - e = store.get_execution_by_component(p, component)[0] - for a in artifacts: - result.append( - { - "pipeline": p.name, - "step": s, - "status": components_status[component], - "source": config["source"], - "args": config["args"], - "artifact": store.store.get_artifact_types_by_id( - [a.type_id] - )[ - 0 - ].name, - "artifact_type": a_type, - "artifact_uri": a.uri, - "artifact_id": a.id, - "execution_id": e.id, - } - ) - - # Handle additional components - extra_components = ["ImageGen"] - for s in extra_components: - for component in steps_to_artifacts[s]: - artifacts = store.get_artifacts_by_component_and_type( - p, component, a_type - ) - e = store.get_execution_by_component(p, component)[0] - for a in artifacts: - result.append( - { - "pipeline": p.name, - "step": s, - "status": components_status[s], - "source": f"zenml@{component}", - "args": "", - "artifact": store.store.get_artifact_types_by_id( - [a.type_id] - )[ - 0 - ].name, - "artifact_type": a_type, - "artifact_uri": a.uri, - "artifact_id": a.id, - "execution_id": e.id, - } - ) - - return result - - -def artifact_lineage_graph(repo: Repository): - result_df = pd.DataFrame(read_lineage_info(repo)) - - fig = px.treemap( - result_df, - path=["pipeline", "step", "artifact"], - hover_data={ - "source": True, - "args": True, - }, - color="status", - color_discrete_map={ - "(?)": "purple", - "committed": "orange", - "complete": "green", - "cached": "lightgreen", - }, - ) - fig.update_annotations(hoverinfo="skip") - - return fig - - -def pipeline_lineage_graph(repo: Repository): - category_df = {} - - for step_artifact in read_lineage_info(repo): - pipeline = step_artifact["pipeline"] - step = step_artifact["step"] - - if pipeline not in category_df: - category_df[pipeline] = {"pipeline": pipeline} - - category_df[pipeline].update( - { - f"{step}_status": step_artifact["status"], - f"{step}_source": step_artifact["source"] - .split("@")[0] - .split(".")[-1], - f"{step}_args": step_artifact["args"], - } - ) - - if f"{step}_artifact_ids" not in category_df[pipeline]: - category_df[pipeline][f"{step}_artifact_ids"] = set() - - category_df[pipeline][f"{step}_artifact_ids"].add( - step_artifact["artifact_id"] - ) - - for pipeline in category_df: - for step in steps_to_artifacts: - source = category_df[pipeline][f"{step}_source"] - output_artifacts = json.dumps( - sorted(category_df[pipeline][f"{step}_artifact_ids"]) - ) - - category_df[pipeline][step] = f"{source}-{output_artifacts}" - - category_df = pd.DataFrame.from_dict(category_df, "index") - - category_df = category_df.reset_index() - - fig = px.parallel_categories( - category_df, - dimensions=[ - "pipeline", - "datasource", - "sequencer", - "split", - "preprocesser", - "ImageGen", - ], - color=category_df.index, - labels="status", - ) - - return fig - - -repo = Repository() -artifact_lineage_graph(repo) diff --git a/src/zenml/post_execution/visualizers/lineage/__init__.py b/src/zenml/post_execution/visualizers/lineage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py new file mode 100644 index 00000000000..33cb76577b8 --- /dev/null +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py @@ -0,0 +1,71 @@ +# Copyright (c) ZenML GmbH 2021. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing +# permissions and limitations under the License. + +from abc import abstractmethod + +import pandas as pd +import plotly.express as px +from plotly.graph_objs import Figure + +from zenml.logger import get_logger +from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.visualizers.base_pipeline_visualizer import ( + BasePipelineVisualizer, +) + +logger = get_logger(__name__) + + +class PipelineLineageVisualizer(BasePipelineVisualizer): + """Visualize the lineage of runs in a pipeline.""" + + @abstractmethod + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> Figure: + """Creates a pipeline lineage diagram using plotly. + + Args: + pipeline: + *args: + **kwargs: + + Returns: + + """ + category_df = {} + dimensions = ["run"] + for run in pipeline.runs: + category_df[run.name] = {"run": run.name} + for step in run.steps: + # for artifact_name, artifact in step.outputs.items(): + category_df[run.name].update( + { + step.name: step.id, + } + ) + if step.name not in dimensions: + dimensions.append(f"{step.name}") + + category_df = pd.DataFrame.from_dict(category_df, orient="index") + + category_df = category_df.reset_index() + + fig = px.parallel_categories( + category_df, + dimensions, + color=None, + labels="status", + ) + + fig.show() + return fig diff --git a/src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py similarity index 84% rename from src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py rename to src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py index b447332afc8..7d91ad6c382 100644 --- a/src/zenml/post_execution/visualizers/pipeline_run_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py @@ -16,11 +16,14 @@ from zenml.logger import get_logger from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.visualizers.base_pipeline_run_visualizer import ( + BasePipelineRunVisualizer, +) logger = get_logger(__name__) -class PipelineLineageVisualizer: +class PipelineRunLineageVisualizer(BasePipelineRunVisualizer): """The base implementation of a ZenML Step Visualizer.""" @abstractmethod diff --git a/src/zenml/post_execution/visualizers/statistics/__init__.py b/src/zenml/post_execution/visualizers/statistics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py similarity index 98% rename from src/zenml/post_execution/visualizers/facet_statistics_visualizer.py rename to src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py index 98e9ba44946..01b3661d0ee 100644 --- a/src/zenml/post_execution/visualizers/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py @@ -78,7 +78,7 @@ def generate_html(self, datasets: List[Dict[Text, pd.DataFrame]]) -> str: protostr = base64.b64encode(proto.SerializeToString()).decode("utf-8") template = os.path.join( - os.path.abspath(os.path.dirname(__file__)), "stats.html" + os.path.abspath(os.path.dirname(__file__)), "../stats.html" ) html_template = path_utils.read_file_contents_as_string(template) From 442db20a6400e73ab8e449d5e5a391a30b78037a Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 14:47:54 +0100 Subject: [PATCH 24/29] Added visualizers to post execution docs --- docs/book/core-concepts.md | 4 +++ docs/book/guides/post-execution-workflow.md | 32 +++++++++++-------- src/zenml/post_execution/lineage.md | 34 --------------------- 3 files changed, 23 insertions(+), 47 deletions(-) delete mode 100644 src/zenml/post_execution/lineage.md diff --git a/docs/book/core-concepts.md b/docs/book/core-concepts.md index d1b4983f5c8..e46ad437814 100644 --- a/docs/book/core-concepts.md +++ b/docs/book/core-concepts.md @@ -158,6 +158,10 @@ zenml stack register STACK_NAME \ Backends are the infrastructure and environments on which your steps run. There are different kinds of backends depending on the particular use case. COMING SOON +**Visualizers** + +Visualizers contain logic to create visualizations within the ZenML ecosystem. + **Tying Things All Together** ZenML's core abstractions are either close to or replicate completely the commonly-found abstractions found in the industry for pipeline-style workflows. As a data scientist, it perhaps isn't natural to think of your work from within this 'pipeline' abstraction, but we think you'll see the benefits if you try it out with some examples. Check out our Get Started guide to see an example of what ZenML will add to your current workflow! diff --git a/docs/book/guides/post-execution-workflow.md b/docs/book/guides/post-execution-workflow.md index 022890b5fce..68af681fb39 100644 --- a/docs/book/guides/post-execution-workflow.md +++ b/docs/book/guides/post-execution-workflow.md @@ -65,30 +65,36 @@ output = step.output output.read() ``` -## Visuals - ### Materializing outputs (or inputs) -Once an output artifact is acquired from history, one can visualize it with any chosen `Materializer`. +Once an output artifact is acquired from history, one can visualize it with any chosen `Visualizer`. ```python -df = output.read(materializer=PandasMaterializer) +df = output.read(materializer_class=PandasMaterializer) df.head() ``` -### Seeing statistics and schema -```python -stats = output.read(materializer=StatisticsMaterializer) -stats # visualize stats +### Retrieving Model -schema = output.read(materializer=SchemaMaterializer) -schema # visualize schema +```python +model = output.read(materializer_class=KerasModelMaterializer) +model # read keras.Model ``` -### Retrieving Model +## Visuals + +### Seeing statistics ```python -model = output.read(materializer=KerasModelMaterializer) -model # visualize model +from zenml.post_execution.visualizers.statistics.facet_statistics_visualizer import ( + FacetStatisticsVisualizer, +) + +FacetStatisticsVisualizer().visualize(output) ``` + +It produces the following visualization: + +![Statistics for boston housing dataset](../.gitbook/assets/statistics_boston_housing.png) + diff --git a/src/zenml/post_execution/lineage.md b/src/zenml/post_execution/lineage.md deleted file mode 100644 index 8ee131742eb..00000000000 --- a/src/zenml/post_execution/lineage.md +++ /dev/null @@ -1,34 +0,0 @@ -# Pipeline Lineage - -```shell -pipeline = repo.get_pipelines()[-1] -run = pipeline.get_runs()[-1] - -run.get_lineage() - -``` - -# Lineage Object -```shell -class LineageObject: - parent = None - children = List - is_root: bool = False - - def get_children(): - pass - -class Lineage: - lineage_objects: List[LineageObject] - root_node: LineageObject -``` - - -# ArtifactViews -* Create a relation that identifies which step an artifact was produced by (`artifact.producer_step`) -* Create a relation that identifies which steps used an artifact (`artifact.consumer_steps`) - -``` - -def get_ -``` \ No newline at end of file From e28e2fdbe06ff4324b4d760273b60c41c4d27446 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 14:50:55 +0100 Subject: [PATCH 25/29] Commented out visualizer for lineage --- .../lineage/pipeline_lineage_visualizer.py | 16 +++++++++++----- .../lineage/pipeline_run_lineage_visualizer.py | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py index 33cb76577b8..980e35b6f41 100644 --- a/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py @@ -14,16 +14,17 @@ from abc import abstractmethod -import pandas as pd -import plotly.express as px -from plotly.graph_objs import Figure - from zenml.logger import get_logger from zenml.post_execution.pipeline import PipelineView from zenml.post_execution.visualizers.base_pipeline_visualizer import ( BasePipelineVisualizer, ) +# import pandas as pd +# import plotly.express as px +# from plotly.graph_objs import Figure + + logger = get_logger(__name__) @@ -31,7 +32,7 @@ class PipelineLineageVisualizer(BasePipelineVisualizer): """Visualize the lineage of runs in a pipeline.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> Figure: + def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: """Creates a pipeline lineage diagram using plotly. Args: @@ -41,6 +42,10 @@ def visualize(self, pipeline: PipelineView, *args, **kwargs) -> Figure: Returns: + """ + raise NotImplementedError + + # WIP: """ category_df = {} dimensions = ["run"] @@ -69,3 +74,4 @@ def visualize(self, pipeline: PipelineView, *args, **kwargs) -> Figure: fig.show() return fig + """ diff --git a/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py index 7d91ad6c382..3285840bd76 100644 --- a/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py @@ -29,3 +29,4 @@ class PipelineRunLineageVisualizer(BasePipelineRunVisualizer): @abstractmethod def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: """Method to visualize pipelines""" + raise NotImplementedError From 303125a222f29675174544d270ed61bee0b8930b Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 15:28:12 +0100 Subject: [PATCH 26/29] Fixed mypy issues --- pyproject.toml | 2 ++ src/zenml/post_execution/artifact.py | 6 +----- .../visualizers/base_pipeline_run_visualizer.py | 5 ++++- .../visualizers/base_pipeline_visualizer.py | 3 ++- .../post_execution/visualizers/base_step_visualizer.py | 3 ++- src/zenml/post_execution/visualizers/base_visualizer.py | 3 ++- .../visualizers/lineage/pipeline_lineage_visualizer.py | 5 ++++- .../lineage/pipeline_run_lineage_visualizer.py | 7 +++++-- .../visualizers/statistics/facet_statistics_visualizer.py | 8 ++++---- 9 files changed, 26 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5bc32b8342..cc0861ccf2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,8 @@ module = [ "pytorch_lightning.*", "sklearn.*", "numpy.*", + "facets_overview.*", + "IPython.core.*" ] ignore_missing_imports = true diff --git a/src/zenml/post_execution/artifact.py b/src/zenml/post_execution/artifact.py index f3efffa9e6a..322b9d757b3 100644 --- a/src/zenml/post_execution/artifact.py +++ b/src/zenml/post_execution/artifact.py @@ -63,7 +63,6 @@ def __init__( self._uri = uri self._materializer = materializer self._data_type = data_type - self._producer_step = None self._metadata_store = metadata_store self._parent_step_id = parent_step_id @@ -93,10 +92,7 @@ def producer_step(self) -> "StepView": """Returns the original StepView that produced the artifact.""" # TODO [LOW]: Replace with artifact.id instead of passing self if # required. - self._producer_step = ( - self._metadata_store.get_producer_step_from_artifact(self) - ) - return self._producer_step + return self._metadata_store.get_producer_step_from_artifact(self) @property def is_cached(self) -> bool: diff --git a/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py index 69f8f029dea..755af309e6b 100644 --- a/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_pipeline_run_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger from zenml.post_execution.pipeline_run import PipelineRunView @@ -25,5 +26,7 @@ class BasePipelineRunVisualizer(BaseVisualizer): """The base implementation of a ZenML Pipeline Run Visualizer.""" @abstractmethod - def visualize(self, pipeline: PipelineRunView, *args, **kwargs) -> None: + def visualize( + self, object: PipelineRunView, *args: Any, **kwargs: Any + ) -> None: """Method to visualize pipeline runs.""" diff --git a/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py index 1592f82cd78..70f77444f36 100644 --- a/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_pipeline_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger from zenml.post_execution.pipeline import PipelineView @@ -25,5 +26,5 @@ class BasePipelineVisualizer(BaseVisualizer): """The base implementation of a ZenML Pipeline Visualizer.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + def visualize(self, object: PipelineView, *args: Any, **kwargs: Any) -> Any: """Method to visualize pipelines.""" diff --git a/src/zenml/post_execution/visualizers/base_step_visualizer.py b/src/zenml/post_execution/visualizers/base_step_visualizer.py index c8ce86d5ee4..bfce7852a0b 100644 --- a/src/zenml/post_execution/visualizers/base_step_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_step_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger from zenml.post_execution.step import StepView @@ -25,5 +26,5 @@ class BaseStepVisualizer(BaseVisualizer): """The base implementation of a ZenML Step Visualizer.""" @abstractmethod - def visualize(self, step: StepView, *args, **kwargs) -> None: + def visualize(self, object: StepView, *args: Any, **kwargs: Any) -> Any: """Method to visualize steps.""" diff --git a/src/zenml/post_execution/visualizers/base_visualizer.py b/src/zenml/post_execution/visualizers/base_visualizer.py index bd341ef8ff3..6d3368a23a6 100644 --- a/src/zenml/post_execution/visualizers/base_visualizer.py +++ b/src/zenml/post_execution/visualizers/base_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger @@ -23,5 +24,5 @@ class BaseVisualizer: """Base class for all ZenML Visualizers.""" @abstractmethod - def visualize(self, *args, **kwargs) -> None: + def visualize(self, object: Any, *args: Any, **kwargs: Any) -> None: """Method to visualize objects.""" diff --git a/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py index 980e35b6f41..b2e9713cbb9 100644 --- a/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_lineage_visualizer.py @@ -13,6 +13,7 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger from zenml.post_execution.pipeline import PipelineView @@ -32,7 +33,9 @@ class PipelineLineageVisualizer(BasePipelineVisualizer): """Visualize the lineage of runs in a pipeline.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + def visualize( + self, object: PipelineView, *args: Any, **kwargs: Any + ) -> None: """Creates a pipeline lineage diagram using plotly. Args: diff --git a/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py index 3285840bd76..1766a172f82 100644 --- a/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py +++ b/src/zenml/post_execution/visualizers/lineage/pipeline_run_lineage_visualizer.py @@ -13,9 +13,10 @@ # permissions and limitations under the License. from abc import abstractmethod +from typing import Any from zenml.logger import get_logger -from zenml.post_execution.pipeline import PipelineView +from zenml.post_execution.pipeline_run import PipelineRunView from zenml.post_execution.visualizers.base_pipeline_run_visualizer import ( BasePipelineRunVisualizer, ) @@ -27,6 +28,8 @@ class PipelineRunLineageVisualizer(BasePipelineRunVisualizer): """The base implementation of a ZenML Step Visualizer.""" @abstractmethod - def visualize(self, pipeline: PipelineView, *args, **kwargs) -> None: + def visualize( + self, object: PipelineRunView, *args: Any, **kwargs: Any + ) -> Any: """Method to visualize pipelines""" raise NotImplementedError diff --git a/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py index 01b3661d0ee..9a8a4d5caea 100644 --- a/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py @@ -18,7 +18,7 @@ import tempfile import webbrowser from abc import abstractmethod -from typing import Dict, List, Text +from typing import Any, Dict, List, Text import pandas as pd from facets_overview.generic_feature_statistics_generator import ( @@ -41,16 +41,16 @@ class FacetStatisticsVisualizer(BaseStepVisualizer): @abstractmethod def visualize( - self, step: StepView, magic: bool = False, *args, **kwargs + self, object: StepView, magic: bool = False, *args: Any, **kwargs: Any ) -> None: """Method to visualize components Args: - step: StepView fetched from run.get_step(). + object: StepView fetched from run.get_step(). magic: Whether to render in a Jupyter notebook or not. """ datasets = [] - for output_name, artifact_view in step.outputs.items(): + for output_name, artifact_view in object.outputs.items(): df = artifact_view.read() if type(df) is not pd.DataFrame: logger.warning( From 311ea6d33eddce6c9d98fe0cf60266b885fb0175 Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 15:46:11 +0100 Subject: [PATCH 27/29] Updated README --- README.md | 144 ++++++++++++++------------- docs/book/.gitbook/assets/stacks.png | Bin 0 -> 83215 bytes 2 files changed, 77 insertions(+), 67 deletions(-) create mode 100644 docs/book/.gitbook/assets/stacks.png diff --git a/README.md b/README.md index 70a714cc92a..05f972d5cd9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Before: Sam struggles to productionalize ML | After: Sam finds Zen in her MLOps with ZenML :-------------------------:|:-------------------------: -![](docs/readme/sam_frustrated.jpg) | ![](docs/readme/sam_zen_mode.jpg) +![Sam is frustrated](docs/readme/sam_frustrated.jpg) | ![Sam is happy](docs/readme/sam_zen_mode.jpg) @@ -78,24 +78,8 @@ It is created for data science / machine learning teams that are engaged in not - If you are using models as a software service to serve predictions and are consistently improving the model over time. - If you are trying to understand patterns using machine learning for any business process. -- In all of the above, there will be team that is engaged with creating, deploying, managing and improving the entire process. You always want the best results, the best models, and the most robust and reliable results. This is where ZenML can help. - In terms of user persona, ZenML is created for producers of the models. This role is classically known as 'data scientist' in the industry and can range from research-minded individuals to more engineering-driven people. The goal of ZenML is to enable these practitioners to own their models until deployment and beyond. - -## Release 0.5.0 and what lies ahead - -The current release is bare bones (as it is a complete rewrite). -We are missing some basic features which used to be part of ZenML 0.3.8 (the previous release): - -- Standard interfaces for `TrainingPipeline`. -- Individual step interfaces like `PreprocessorStep`, `TrainerStep`, `DeployerStep` etc. need to be rewritten from within the new paradigm. They should - be included in the non-RC version of this release. -- A proper production setup with an orchestrator like Airflow. -- A post-execution workflow to analyze and inspect pipeline runs. -- The concept of `Backends` will evolve into a simple mechanism of transitioning individual steps into different runners. -- Support for `KubernetesOrchestrator`, `KubeflowOrchestrator`, `GCPOrchestrator` and `AWSOrchestrator` are also planned. -- Dependency management including Docker support is planned. - -However, bare with us: Adding those features back in should be relatively faster as we now have a solid foundation to build on. Look out for the next email! +In all of the above, there will be team that is engaged with creating, deploying, managing and improving the entire process. You always want the best results, the best models, and the most robust and reliable results. This is where ZenML can help. +In terms of user persona, ZenML is created for producers of the models. This role is classically known as 'data scientist' in the industry and can range from research-minded individuals to more engineering-driven people. The goal of ZenML is to enable these practitioners to own their models until deployment and beyond. ## Roadmap and Community @@ -107,30 +91,6 @@ ZenML is managed by a [core team](https://zenml.io/team) of developers that are - Create a [Feature Request](https://github.com/zenml-io/zenml/issues/new/choose) in the [GitHub board](https://github.com/zenml-io/zenml/issues). - Start a thread in the [Slack channel](https://zenml.io/slack-invite). -## Contributing - -We would love to receive your contributions! Check our [Contributing Guide](CONTRIBUTING.md) for more details on how best to contribute. - -
- -![Repobeats analytics image](https://repobeats.axiom.co/api/embed/635c57b743efe649cadceba6a2e6a956663f96dd.svg "Repobeats analytics image") - -## Copyright - -ZenML is distributed under the terms of the Apache License Version 2.0. A complete version of the license is available in the [LICENSE.md](LICENSE.md) in this repository. - -Any contribution made to this project will be licensed under the Apache License Version 2.0. - -## Credit - -ZenML is built on the shoulders of giants: we leverage, and would like to give credit to, existing open-source libraries like [TFX](https://github.com/tensorflow/tfx/). The goal of our framework is neither to replace these libraries, nor to diminish their usage. ZenML is simply an opinionated, higher-level interface with the focus being purely on easy-of-use and coherent intuitive design. -You can read more about why we actually started building ZenML at our [blog](https://blog.zenml.io/why-zenml/). - -## Legacy [Updated Soon Q4 2021] - -From this point onwards, the README is intended to give a glimpse as to what lies ahead. We have redesigned our [public roadmap](https://zenml.io/roadmap) -to showcase better the timeline in which these features will be complete. - ## Quickstart The quickest way to get started is to create a simple pipeline. @@ -230,60 +190,74 @@ pipeline.run() ``` ## Leverage powerful integrations - Once code is organized into a ZenML pipeline, you can supercharge your ML development with powerful integrations and on multiple [MLOps stacks](https://docs.zenml.io/core-concepts). -### Work locally but switch seamlessly to the cloud +### View statistics -Switching from local experiments to cloud-based pipelines doesn't need to be complex. +```python +# See statistics of train and eval [COMING SOON] +from zenml.core.repo import Repository +from zenml.post_execution.visualizers.statistics.facet_statistics_visualizer import ( + FacetStatisticsVisualizer, +) +repo = Repository() +pipe = repo.get_pipelines()[-1] +importer_outputs = pipe.runs[-1].get_step(name="importer") +FacetStatisticsVisualizer().visualize(importer_outputs) ``` -pipeline.run('airflow_gcp_stack') -``` -### Versioning galore: Use caching across experiments +![Boston Housing Dataset Statistics Visualization](docs/book/.gitbook/assets/statistics_boston_housing.png) + +### Use Caching across (Pipelines As) Experiments ZenML makes sure for every pipeline you can trust that: -✅ Code is versioned +✅ Code is versioned ✅ Data is versioned ✅ Models are versioned ✅ Configurations are versioned -Use caching to help iterate quickly through ML experiments. +You can utilize caching to help iterate quickly through ML experiments. -### Automatically detect schema -```python -# See the schema of your data [COMING SOON] -pipeline.view_schema() +### Work locally but switch seamlessly to the cloud + +Switching from local experiments to cloud-based pipelines doesn't need to be complex. + +``` +[COMING SOON] ``` -![Automatic schema dection](docs/schema.png) +![Development and production stack](docs/book/.gitbook/assets/stacks.png) + + -### View statistics + +### Automatically detect schema ```python -# See statistics of train and eval [COMING SOON] -pipeline.view_statistics() +[COMING SOON] ``` -ZenML statistics visualization +![Automatic schema dection](docs/schema.png) + ### Evaluate the model using built-in evaluators ```python -# Creates a notebook for evaluation [COMING SOON] -training_pipeline.evaluate() +[COMING SOON] ``` -Tensorboard built-in + +![ZenML built-in pipeline comparison](docs/tensorboard_inline.png) + ### Compare training pipelines ```python -# COMING SOON +[COMING SOON] ``` ![ZenML built-in pipeline comparison](docs/compare.png) @@ -293,17 +267,53 @@ training_pipeline.evaluate() Leverage distributed compute powered by [Apache Beam](https://beam.apache.org/): ```python -# COMING SOON +[COMING SOON] ``` -ZenML distributed processing +![ZenML distributed processing](docs/zenml_distribute.png) + ### Deploy models automatically Automatically deploy each model with powerful Deployment integrations like [Ray](https://docs.ray.io/en/latest/serve/index.html). ```python -# COMING SOON +[COMING SOON] ``` The best part is that ZenML is extensible easily, and can be molded to your use-case. You can create your own custom logic or create a PR and contribute to the ZenML community, so that everyone can benefit. + +## Release 0.5.0 and what lies ahead + +The current release is bare bones (as it is a complete rewrite). +We are missing some basic features which used to be part of ZenML 0.3.8 (the previous release): + +- Standard interfaces for `TrainingPipeline`. +- Individual step interfaces like `PreprocessorStep`, `TrainerStep`, `DeployerStep` etc. need to be rewritten from within the new paradigm. They should + be included in the non-RC version of this release. +- A proper production setup with an orchestrator like Airflow. +- A post-execution workflow to analyze and inspect pipeline runs. +- The concept of `Backends` will evolve into a simple mechanism of transitioning individual steps into different runners. +- Support for `KubernetesOrchestrator`, `KubeflowOrchestrator`, `GCPOrchestrator` and `AWSOrchestrator` are also planned. +- Dependency management including Docker support is planned. + +However, bare with us: Adding those features back in should be relatively faster as we now have a solid foundation to build on. Look out for the next email! + +## Contributing + +We would love to receive your contributions! Check our [Contributing Guide](CONTRIBUTING.md) for more details on how best to contribute. + +
+ +![Repobeats analytics image](https://repobeats.axiom.co/api/embed/635c57b743efe649cadceba6a2e6a956663f96dd.svg "Repobeats analytics image") + +## Copyright + +ZenML is distributed under the terms of the Apache License Version 2.0. A complete version of the license is available in the [LICENSE.md](LICENSE.md) in this repository. + +Any contribution made to this project will be licensed under the Apache License Version 2.0. + +## Credit + +ZenML is built on the shoulders of giants: we leverage, and would like to give credit to, existing open-source libraries like [TFX](https://github.com/tensorflow/tfx/). The goal of our framework is neither to replace these libraries, nor to diminish their usage. ZenML is simply an opinionated, higher-level interface with the focus being purely on easy-of-use and coherent intuitive design. +You can read more about why we actually started building ZenML at our [blog](https://blog.zenml.io/why-zenml/). diff --git a/docs/book/.gitbook/assets/stacks.png b/docs/book/.gitbook/assets/stacks.png new file mode 100644 index 0000000000000000000000000000000000000000..70a4cc5c28778cb323f49a38aa176af741971414 GIT binary patch literal 83215 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 literal 0 HcmV?d00001 From 4bf5d1ebee863ba42062918789a345707f8abdde Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 15:48:04 +0100 Subject: [PATCH 28/29] Inccorporated michael feedback --- examples/visualizers/statistics/run.py | 1 - .../statistics/facet_statistics_visualizer.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/visualizers/statistics/run.py b/examples/visualizers/statistics/run.py index 9583402603f..89b01fe0550 100644 --- a/examples/visualizers/statistics/run.py +++ b/examples/visualizers/statistics/run.py @@ -38,7 +38,6 @@ "PTRATIO", "B", "STAT", - # "MEDV", ] TARGET_COL_NAME = "target" diff --git a/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py index 9a8a4d5caea..b38c6751218 100644 --- a/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py +++ b/src/zenml/post_execution/visualizers/statistics/facet_statistics_visualizer.py @@ -82,10 +82,10 @@ def generate_html(self, datasets: List[Dict[Text, pd.DataFrame]]) -> str: ) html_template = path_utils.read_file_contents_as_string(template) - h = html_template.replace("protostr", protostr) - return h + html_ = html_template.replace("protostr", protostr) + return html_ - def generate_facet(self, h: str, magic: bool = False) -> None: + def generate_facet(self, html_: str, magic: bool = False) -> None: """Generate a Facet Overview Args: @@ -97,10 +97,10 @@ def generate_facet(self, h: str, magic: bool = False) -> None: raise EnvironmentError( "The magic functions are only usable in a Jupyter notebook." ) - display(HTML(h)) + display(HTML(html_)) else: with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f: - path_utils.write_file_contents_as_string(f.name, h) + path_utils.write_file_contents_as_string(f.name, html_) url = f"file:///{f.name}" logger.info("Opening %s in a new browser.." % f.name) webbrowser.open(url, new=2) From 755fd99257e07e3de5e6cb4df877aa115d7b2bcd Mon Sep 17 00:00:00 2001 From: Hamza Tahir Date: Fri, 19 Nov 2021 15:48:17 +0100 Subject: [PATCH 29/29] Lineage run --- run.py | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/run.py b/run.py index 4dccfcf9df6..103398e7c83 100644 --- a/run.py +++ b/run.py @@ -66,26 +66,34 @@ def my_pipeline(importer, preprocesser, trainer, evaluator, deployer): deployer=deployer(), ) +lineage_pipeline.run() +lineage_pipeline.run() +lineage_pipeline.run() lineage_pipeline.run() pipeline = Repository().get_pipelines()[-1] -for run in pipeline.runs: - try: - deployer_step = run.get_step(name="deployer") - trainer_step = run.get_step(name="trainer") - deployed_model_artifact = deployer_step.inputs["model"] - trained_model_artifact = trainer_step.output - - # lets do the lineage - print( - f"trained_model_artifact was produced by: " - f"{trained_model_artifact.producer_step.id} and is_cached: " - f"{trained_model_artifact.is_cached} step cached: " - f"{trainer_step.status}" - ) - except Exception as e: - if "No step found for name `deployer`" in str(e): - pass - else: - raise e +# for run in pipeline.runs: +# try: +# deployer_step = run.get_step(name="deployer") +# trainer_step = run.get_step(name="trainer") +# deployed_model_artifact = deployer_step.inputs["model"] +# trained_model_artifact = trainer_step.output +# +# # lets do the lineage +# print( +# f"trained_model_artifact was produced by: " +# f"{trained_model_artifact.producer_step.id} and is_cached: " +# f"{trained_model_artifact.is_cached} step cached: " +# f"{trainer_step.status}" +# ) +# except Exception as e: +# if "No step found for name `deployer`" in str(e): +# pass +# else: +# raise e + + +from zenml.post_execution.visualizers.lineage.pipeline_lineage_visualizer import PipelineLineageVisualizer + +PipelineLineageVisualizer().visualize(pipeline) \ No newline at end of file