From 2bd762105396b11d70bd155e12f6c61824c66c9d Mon Sep 17 00:00:00 2001
From: Danny Meijer
Date: Mon, 10 Jun 2024 15:38:46 +0200
Subject: [PATCH 1/6] Create codeql.yml (#47)
Adding codeql as workflow
---
.github/workflows/codeql.yml | 93 ++++++++++++++++++++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 .github/workflows/codeql.yml
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..fa9f5f1
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,93 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: '36 17 * * 6'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: python
+ build-mode: none
+ # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # βΉοΈ Command-line programs to run using the OS shell.
+ # π See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
From 4c4d06287cc33caf318295539a44d4d9646d685f Mon Sep 17 00:00:00 2001
From: riccamini
Date: Mon, 10 Jun 2024 15:56:07 +0200
Subject: [PATCH 2/6] Add missing merge clauses in DeltaTableWriter (#46)
DeltaTableWriter provides the option to configure the writer in MERGE
mode using the output_mode_params field by providing in it a list of
merge clauses in the merge_builder key. This is especially useful in
case the Delta table or the DataFrame to be merged are not available
upfront. Not all merge clauses were supported: the merge clause provided
was being validated against a list of valid clauses which did not
include whenMatchedUpdateAll and whenNotMatchedInsertAll clauses. As
part of this PR:
- validation of the clauses now happens in the output_mode_params field
validator (currently this is happening only when the merge is being
executed)
- the list of available merge clauses is sourced directly from available
methods in DeltaMergeBuilder class and now includes all of them
## Related Issue
https://github.com/Nike-Inc/koheesio/issues/43
## Motivation and Context
Now it's possible to use all merge clauses when configuring the
DeltaTableWriter
---------
Co-authored-by: riacom_nike
---
.github/workflows/test.yml | 3 +-
src/koheesio/spark/writers/delta/batch.py | 32 ++++++++++++-------
.../spark/writers/delta/test_delta_writer.py | 25 ++++++++-------
3 files changed, 35 insertions(+), 25 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5aae361..1f72446 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,7 +38,8 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- ref: ${{ github.head_ref }}
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Fetch main branch
run: git fetch origin main:main
- name: Check changes
diff --git a/src/koheesio/spark/writers/delta/batch.py b/src/koheesio/spark/writers/delta/batch.py
index dc04d7c..7334f27 100644
--- a/src/koheesio/spark/writers/delta/batch.py
+++ b/src/koheesio/spark/writers/delta/batch.py
@@ -233,7 +233,7 @@ def __merge_all(self) -> Union[DeltaMergeBuilder, DataFrameWriter]:
return self.__merge(merge_builder=builder)
- def _get_merge_builder(self, provided_merge_builder=None):
+ def _get_merge_builder(self, provided_merge_builder=None) -> DeltaMergeBuilder:
"""Resolves the merge builder. If provided, it will be used, otherwise it will be created from the args"""
# A merge builder has been already created - case for merge_all
@@ -274,19 +274,8 @@ def _merge_builder_from_args(self):
.merge(self.df.alias(source_alias), merge_cond)
)
- valid_clauses = [
- "whenMatchedUpdate",
- "whenNotMatchedInsert",
- "whenMatchedDelete",
- "whenNotMatchedBySourceUpdate",
- "whenNotMatchedBySourceDelete",
- ]
-
for merge_clause in merge_clauses:
clause_type = merge_clause.pop("clause", None)
- if clause_type not in valid_clauses:
- raise ValueError(f"Invalid merge clause '{clause_type}' provided")
-
method = getattr(builder, clause_type)
builder = method(**merge_clause)
@@ -314,6 +303,25 @@ def _validate_table(cls, table):
return DeltaTableStep(table=table)
return table
+ @field_validator("params")
+ def _validate_params(cls, params):
+ """Validates params. If an array of merge clauses is provided, they will be validated against the available
+ ones in DeltaMergeBuilder"""
+
+ valid_clauses = {m for m in dir(DeltaMergeBuilder) if m.startswith("when")}
+
+ if "merge_builder" in params:
+ merge_builder = params["merge_builder"]
+ if isinstance(merge_builder, list):
+ for merge_conf in merge_builder:
+ clause = merge_conf.get("clause")
+ if clause not in valid_clauses:
+ raise ValueError(f"Invalid merge clause '{clause}' provided")
+ elif not isinstance(merge_builder, DeltaMergeBuilder):
+ raise ValueError("merge_builder must be a list or merge clauses or a DeltaMergeBuilder instance")
+
+ return params
+
@classmethod
def get_output_mode(cls, choice: str, options: Set[Type]) -> Union[BatchOutputMode, StreamingOutputMode]:
"""Retrieve an OutputMode by validating `choice` against a set of option OutputModes.
diff --git a/tests/spark/writers/delta/test_delta_writer.py b/tests/spark/writers/delta/test_delta_writer.py
index 4a36069..66306de 100644
--- a/tests/spark/writers/delta/test_delta_writer.py
+++ b/tests/spark/writers/delta/test_delta_writer.py
@@ -308,24 +308,25 @@ def test_merge_from_args(spark, dummy_df):
)
-def test_merge_from_args_raise_value_error(spark, dummy_df):
- table_name = "test_table_merge_from_args_value_error"
- dummy_df.write.format("delta").saveAsTable(table_name)
-
- writer = DeltaTableWriter(
- df=dummy_df,
- table=table_name,
- output_mode=BatchOutputMode.MERGE,
- output_mode_params={
+@pytest.mark.parametrize(
+ "output_mode_params",
+ [
+ {
"merge_builder": [
{"clause": "NOT-SUPPORTED-MERGE-CLAUSE", "set": {"id": "source.id"}, "condition": "source.id=target.id"}
],
"merge_cond": "source.id=target.id",
},
- )
-
+ {"merge_builder": MagicMock()},
+ ],
+)
+def test_merge_from_args_raise_value_error(spark, output_mode_params):
with pytest.raises(ValueError):
- writer._merge_builder_from_args()
+ DeltaTableWriter(
+ table="test_table_merge",
+ output_mode=BatchOutputMode.MERGE,
+ output_mode_params=output_mode_params,
+ )
def test_merge_no_table(spark):
From 2f040f061f983444c5e37a3b1bd6967f3af711b7 Mon Sep 17 00:00:00 2001
From: Brend Braeckmans <72923643+BrendBraeckmans@users.noreply.github.com>
Date: Mon, 10 Jun 2024 16:20:22 +0200
Subject: [PATCH 3/6] Make it possible to set explicit schema i.s.o letting
AutoLoader infer the schema (#40)
Make it possible to set explicit schema i.s.o letting AutoLoader infer the schema.
Optional `schema` argument has been added
## Related Issue
[koheesio-39](https://github.com/Nike-Inc/koheesio/issues/39)
## Motivation and Context
The previous implementation of AutoLoader within koheesio would always infer the schema from the files it reads. In a lot of cases this was unnecessary and might even give issues if the input data doesn't contain the required fields.
## How Has This Been Tested?
- Through UTs
- On DBX
---
.../spark/readers/databricks/autoloader.py | 23 ++++++--
tests/spark/readers/test_auto_loader.py | 55 ++++++++++++++++++-
2 files changed, 70 insertions(+), 8 deletions(-)
diff --git a/src/koheesio/spark/readers/databricks/autoloader.py b/src/koheesio/spark/readers/databricks/autoloader.py
index 6b26e20..8444a54 100644
--- a/src/koheesio/spark/readers/databricks/autoloader.py
+++ b/src/koheesio/spark/readers/databricks/autoloader.py
@@ -3,9 +3,12 @@
Autoloader can ingest JSON, CSV, PARQUET, AVRO, ORC, TEXT, and BINARYFILE file formats.
"""
-from typing import Dict, Optional, Union
+from typing import Any, Dict, List, Optional, Tuple, Union
from enum import Enum
+from pyspark.sql.streaming import DataStreamReader
+from pyspark.sql.types import AtomicType, StructType
+
from koheesio.models import Field, field_validator
from koheesio.spark.readers import Reader
@@ -53,7 +56,7 @@ class AutoLoader(Reader):
Example
-------
```python
- from koheesio.spark.readers.databricks import AutoLoader, AutoLoaderFormat
+ from koheesio.steps.readers.databricks import AutoLoader, AutoLoaderFormat
result_df = AutoLoader(
format=AutoLoaderFormat.JSON,
@@ -82,11 +85,16 @@ class AutoLoader(Reader):
description="The location for storing inferred schema and supporting schema evolution, "
"used in `cloudFiles.schemaLocation`.",
)
- options: Optional[Dict[str, str]] = Field(
+ options: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="Extra inputs to provide to the autoloader. For a full list of inputs, "
"see https://docs.databricks.com/ingestion/auto-loader/options.html",
)
+ schema_: Optional[Union[str, StructType, List[str], Tuple[str, ...], AtomicType]] = Field(
+ default=None,
+ description="Explicit schema to apply to the input files.",
+ alias="schema",
+ )
@field_validator("format")
def validate_format(cls, format_specified):
@@ -107,9 +115,12 @@ def get_options(self):
return self.options
# @property
- def reader(self):
- """Return the reader for the autoloader"""
- return self.spark.readStream.format("cloudFiles").options(**self.get_options())
+ def reader(self) -> DataStreamReader:
+ reader = self.spark.readStream.format("cloudFiles")
+ if self.schema_ is not None:
+ reader = reader.schema(self.schema_)
+ reader = reader.options(**self.get_options())
+ return reader
def execute(self):
"""Reads from the given location with the given options using Autoloader"""
diff --git a/tests/spark/readers/test_auto_loader.py b/tests/spark/readers/test_auto_loader.py
index ca07f55..8f2b168 100644
--- a/tests/spark/readers/test_auto_loader.py
+++ b/tests/spark/readers/test_auto_loader.py
@@ -21,10 +21,13 @@ def test_invalid_format(bad_format):
def mock_reader(self):
- return self.spark.read.format("json").options(**self.options)
+ reader = self.spark.read.format("json")
+ if self.schema_ is not None:
+ reader = reader.schema(self.schema_)
+ return reader.options(**self.options)
-def test_read_json(spark, mocker, data_path):
+def test_read_json_infer_schema(spark, mocker, data_path):
mocker.patch("koheesio.spark.readers.databricks.autoloader.AutoLoader.reader", mock_reader)
options = {"multiLine": "true"}
@@ -49,3 +52,51 @@ def test_read_json(spark, mocker, data_path):
]
expected_df = spark.createDataFrame(data_expected, schema_expected)
assert_df_equality(result, expected_df, ignore_column_order=True)
+
+
+def test_read_json_exact_explicit_schema_struct(spark, mocker, data_path):
+ mocker.patch("koheesio.spark.readers.databricks.autoloader.AutoLoader.reader", mock_reader)
+
+ schema = StructType(
+ [
+ StructField("string", StringType(), True),
+ StructField("int", LongType(), True),
+ StructField("array", ArrayType(LongType()), True),
+ ]
+ )
+ options = {"multiLine": "true"}
+ json_file_path_str = f"{data_path}/readers/json_file/dummy.json"
+ auto_loader = AutoLoader(
+ format="json", location=json_file_path_str, schema_location="dummy_value", options=options, schema=schema
+ )
+
+ auto_loader.execute()
+ result = auto_loader.output.df
+
+ data_expected = [
+ {"string": "string1", "int": 1, "array": [1, 11, 111]},
+ {"string": "string2", "int": 2, "array": [2, 22, 222]},
+ ]
+ expected_df = spark.createDataFrame(data_expected, schema)
+ assert_df_equality(result, expected_df, ignore_column_order=True)
+
+
+def test_read_json_different_explicit_schema_string(spark, mocker, data_path):
+ mocker.patch("koheesio.spark.readers.databricks.autoloader.AutoLoader.reader", mock_reader)
+
+ schema = "string STRING,array ARRAY"
+ options = {"multiLine": "true"}
+ json_file_path_str = f"{data_path}/readers/json_file/dummy.json"
+ auto_loader = AutoLoader(
+ format="json", location=json_file_path_str, schema_location="dummy_value", options=options, schema=schema
+ )
+
+ auto_loader.execute()
+ result = auto_loader.output.df
+
+ data_expected = [
+ {"string": "string1", "array": [1, 11, 111]},
+ {"string": "string2", "array": [2, 22, 222]},
+ ]
+ expected_df = spark.createDataFrame(data_expected, schema)
+ assert_df_equality(result, expected_df, ignore_column_order=True)
From 2d1449e13f1148294e275e1970d9fdaaf2ee39a3 Mon Sep 17 00:00:00 2001
From: Brend Braeckmans <72923643+BrendBraeckmans@users.noreply.github.com>
Date: Mon, 17 Jun 2024 08:52:52 +0200
Subject: [PATCH 4/6] Add streaming option to FileLoader. (#51)
## Description
Add streaming option to FileLoader. Default behaviour is batch.
## Related Issue
[Issue 50
](https://github.com/Nike-Inc/koheesio/issues/50)
## Motivation and Context
Having a `streaming` option for the `FileLoader` would be beneficial
during UT's or for people that are not on Databricks and can't make use
of the Databricks proprietary `Autoloader`.
## How Has This Been Tested?
- UT's
- On Databricks
## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
## Checklist:
- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have read the **CONTRIBUTING** document.
- [x] I have added tests to cover my changes.
- [ ] All new and existing tests passed.
---
src/koheesio/spark/readers/file_loader.py | 6 ++++--
tests/spark/readers/test_file_loader.py | 11 +++++++++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/koheesio/spark/readers/file_loader.py b/src/koheesio/spark/readers/file_loader.py
index a497a0a..9d33806 100644
--- a/src/koheesio/spark/readers/file_loader.py
+++ b/src/koheesio/spark/readers/file_loader.py
@@ -97,6 +97,7 @@ class FileLoader(Reader, ExtraParamsMixin):
schema_: Optional[Union[StructType, str]] = Field(
default=None, description="Schema to use when reading the file", validate_default=False, alias="schema"
)
+ streaming: Optional[bool] = Field(default=False, description="Whether to read the files as a Stream or not")
@field_validator("path")
def ensure_path_is_str(cls, v):
@@ -106,8 +107,9 @@ def ensure_path_is_str(cls, v):
return v
def execute(self):
- """Reads the file using the specified format, schema, while applying any extra parameters."""
- reader = self.spark.read.format(self.format)
+ """Reads the file, in batch or as a stream, using the specified format and schema, while applying any extra parameters."""
+ reader = self.spark.readStream if self.streaming else self.spark.read
+ reader = reader.format(self.format)
if self.schema_:
reader.schema(self.schema_)
diff --git a/tests/spark/readers/test_file_loader.py b/tests/spark/readers/test_file_loader.py
index d6c3d13..9605e56 100644
--- a/tests/spark/readers/test_file_loader.py
+++ b/tests/spark/readers/test_file_loader.py
@@ -1,5 +1,7 @@
import pytest
+import pyspark.sql.types as T
+
from koheesio.spark import AnalysisException
from koheesio.spark.readers.file_loader import (
AvroReader,
@@ -106,6 +108,15 @@ def test_json_reader(json_file):
assert actual_data == expected_data
+def test_json_stream_reader(json_file):
+ schema = "string STRING, int INT, float FLOAT"
+ reader = JsonReader(path=json_file, schema=schema, streaming=True)
+ assert reader.path == json_file
+ df = reader.read()
+ assert df.isStreaming
+ assert df.schema == T._parse_datatype_string(schema)
+
+
def test_parquet_reader(parquet_file):
expected_data = [
{"id": 0},
From 4b2e01580d8ffc81e180e3ff8e2879bc491b4382 Mon Sep 17 00:00:00 2001
From: Danny Meijer
Date: Fri, 21 Jun 2024 21:15:41 +0200
Subject: [PATCH 5/6] [DOC] documentation updates June 2024 (#48)
Documentation updates
## Description
- Fixed spelling of and type annotations for Parameters across
docstrings
- Fixed broken links and unrecognized relative links
- Fixed indentation for continuation lines in docstrings
- Refactored README for legibility and clarity with a more defined
structure and better formatting
- General style and formatting improvements
- Minor change to the layout of the pages (to better accommodate the
reader)
- No code changes were made as part of this PR
## Testing
To build the docs:
- check out the branch
- run `make docs`
- docsite will be hosted on your local machine
## Related Issue
#38
#42
---------
Co-authored-by: Danny Meijer <10511979+dannymeijer@users.noreply.github.com>
---
CONTRIBUTING.md | 25 +--
Makefile | 2 +-
README.md | 210 +++++++++++++-----
docs/assets/documentation-system-overview.png | Bin 0 -> 209013 bytes
docs/community/approach-documentation.md | 7 +-
docs/community/contribute.md | 11 +-
docs/css/custom.css | 26 +--
docs/reference/concepts/concepts.md | 33 ++-
docs/reference/concepts/context.md | 4 +-
.../concepts/{logging.md => logger.md} | 0
docs/reference/concepts/{steps.md => step.md} | 0
docs/reference/concepts/tasks.md | 8 -
docs/reference/{concepts => spark}/readers.md | 2 +-
.../{concepts => spark}/transformations.md | 4 +-
docs/reference/{concepts => spark}/writers.md | 0
docs/tutorials/getting-started.md | 112 +++++++---
docs/tutorials/how-to.md | 8 -
docs/tutorials/learn-koheesio.md | 90 ++++++--
docs/tutorials/onboarding.md | 4 -
mkdocs.yml | 43 ++--
pyproject.toml | 3 +-
src/koheesio/integrations/spark/sftp.py | 44 ++--
src/koheesio/logger.py | 2 +-
src/koheesio/models/__init__.py | 12 +-
src/koheesio/models/sql.py | 6 +-
src/koheesio/spark/readers/__init__.py | 2 +-
src/koheesio/spark/readers/delta.py | 2 +-
src/koheesio/spark/readers/hana.py | 2 +-
src/koheesio/spark/readers/snowflake.py | 4 +-
src/koheesio/spark/readers/teradata.py | 2 +-
src/koheesio/spark/snowflake.py | 7 +-
.../spark/transformations/__init__.py | 58 +++--
src/koheesio/spark/transformations/arrays.py | 4 +-
.../spark/transformations/cast_to_datatype.py | 32 +--
.../transformations/date_time/__init__.py | 12 +-
.../transformations/date_time/interval.py | 11 +-
.../spark/transformations/strings/__init__.py | 16 +-
.../transformations/strings/change_case.py | 8 +-
.../spark/transformations/strings/regexp.py | 10 +-
.../spark/transformations/strings/trim.py | 6 +-
src/koheesio/spark/writers/buffer.py | 21 +-
.../dq/test_spark_expectations.py | 6 +
42 files changed, 509 insertions(+), 350 deletions(-)
create mode 100644 docs/assets/documentation-system-overview.png
rename docs/reference/concepts/{logging.md => logger.md} (100%)
rename docs/reference/concepts/{steps.md => step.md} (100%)
delete mode 100644 docs/reference/concepts/tasks.md
rename docs/reference/{concepts => spark}/readers.md (97%)
rename docs/reference/{concepts => spark}/transformations.md (99%)
rename docs/reference/{concepts => spark}/writers.md (100%)
delete mode 100644 docs/tutorials/how-to.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7cbfedb..0145c92 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,26 +2,18 @@
There are a few guidelines that we need contributors to follow so that we are able to process requests as efficiently as possible.
-
-[//]: # (If you have any questions or concerns please feel free to contact us at [opensource@nike.com](mailto:opensource@nike.com).)
+If you have any questions or concerns please feel free to contact us at [opensource@nike.com](mailto:opensource@nike.com).
-[//]: # ()
-[//]: # (## Getting Started)
-[//]: # ()
-[//]: # (* Review our [Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md))
+## Getting Started
-[//]: # (* Submit the [Individual Contributor License Agreement](https://www.clahub.com/agreements/Nike-Inc/fastbreak))
-[//]: # (* Make sure you have a [GitHub account](https://github.com/signup/free))
-
-[//]: # (* Submit a ticket for your issue, assuming one does not already exist.)
-
-[//]: # ( * Clearly describe the issue including steps to reproduce when it is a bug.)
-
-[//]: # ( * Make sure you fill in the earliest version that you know has the issue.)
-
-[//]: # (* Fork the repository on GitHub)
+* Review our [Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md)
+* Make sure you have a [GitHub account](https://github.com/signup/free)
+* Submit a ticket for your issue, assuming one does not already exist.
+ * Clearly describe the issue including steps to reproduce when it is a bug.
+ * Make sure you fill in the earliest version that you know has the issue.
+* Fork the repository on GitHub
## Making Changes
@@ -98,6 +90,5 @@ At the moment, the release process is manual. We try to make frequent releases.
* [GitHub pull request documentation](https://help.github.com/send-pull-requests/)
* [Nike's Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md)
-[//]: # (* [Nike's Individual Contributor License Agreement](https://www.clahub.com/agreements/Nike-Inc/fastbreak))
[//]: # (* [Nike OSS](https://nike-inc.github.io/))
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 163a4fd..54da8d0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: help ## Display this message
help:
- @python koheesio/__about__.py
+ @python src/koheesio/__about__.py
@echo "\nAvailable \033[34m'make'\033[0m commands:"
@echo "\n\033[1mSetup:\033[0m"
@grep -E '^.PHONY: .*?## setup - .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ".PHONY: |## (setup|hatch) - "}; {printf " \033[36m%-22s\033[0m %s\n", $$2, $$3}'
diff --git a/README.md b/README.md
index 23454f8..41e7f0a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
# Koheesio
-
-
+
-
+
| | |
|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -11,10 +10,16 @@
| Package | [![PyPI - Version](https://img.shields.io/pypi/v/koheesio.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/koheesio/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/koheesio.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/koheesio/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/koheesio?color=blue&label=Installs&logo=pypi&logoColor=gold)](https://pypi.org/project/koheesio/) |
| Meta | [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![docstring - numpydoc](https://img.shields.io/badge/docstring-numpydoc-blue)](https://numpydoc.readthedocs.io/en/latest/format.html) [![code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![License - Apache 2.0](https://img.shields.io/github/license/Nike-Inc/koheesio)](LICENSE.txt) |
-Koheesio, named after the Finnish word for cohesion, is a robust Python framework for building efficient data pipelines.
-It promotes modularity and collaboration, enabling the creation of complex pipelines from simple, reusable components.
+[//]: # (suggested edit: )
+# Koheesio: A Python Framework for Efficient Data Pipelines
+
+Koheesio - the Finnish word for cohesion - is a robust Python framework designed to build efficient data pipelines. It
+encourages modularity and collaboration, allowing the creation of complex pipelines from simple, reusable components.
+
-The framework is versatile, aiming to support multiple implementations and working seamlessly with various data
+## What is Koheesio?
+
+Koheesio is a versatile framework that supports multiple implementations and works seamlessly with various data
processing libraries or frameworks. This ensures that Koheesio can handle any data processing task, regardless of the
underlying technology or data scale.
@@ -23,50 +28,127 @@ safety and structured configurations within pipeline components.
[Pydantic]: docs/includes/glossary.md#pydantic
-Koheesio's goal is to ensure predictable pipeline execution through a solid foundation of well-tested code and a rich
-set of features, making it an excellent choice for developers and organizations seeking to build robust and adaptable
-Data Pipelines.
+The goal of Koheesio is to ensure predictable pipeline execution through a solid foundation of well-tested code and a
+rich set of features. This makes it an excellent choice for developers and organizations seeking to build robust and
+adaptable data pipelines.
-## What sets Koheesio apart from other libraries?"
-Koheesio encapsulates years of data engineering expertise, fostering a collaborative and innovative community. While
-similar libraries exist, Koheesio's focus on data pipelines, integration with PySpark, and specific design for tasks
-like data transformation, ETL jobs, data validation, and large-scale data processing sets it apart.
-
-Koheesio aims to provide a rich set of features including readers, writers, and transformations for any type of Data
-processing. Koheesio is not in competition with other libraries. Its aim is to offer wide-ranging support and focus
-on utility in a multitude of scenarios. Our preference is for integration, not competition...
+### What Koheesio is Not
+
+Koheesio is **not** a workflow orchestration tool. It does not serve the same purpose as tools like Luigi,
+Apache Airflow, or Databricks workflows, which are designed to manage complex computational workflows and generate
+DAGs (Directed Acyclic Graphs).
+
+Instead, Koheesio is focused on providing a robust, modular, and testable framework for data tasks. It's designed to
+make it easier to write, maintain, and test data processing code in Python, with a strong emphasis on modularity,
+reusability, and error handling.
+
+If you're looking for a tool to orchestrate complex workflows or manage dependencies between different tasks, you might
+want to consider dedicated workflow orchestration tools.
+
+
+### The Strength of Koheesio
+
+The core strength of Koheesio lies in its **focus on the individual tasks within those workflows**. It's all about
+making these tasks as robust, repeatable, and maintainable as possible. Koheesio aims to break down tasks into small,
+manageable units of work that can be easily tested, reused, and composed into larger workflows orchestrated with other
+tools or frameworks (such as Apache Airflow, Luigi, or Databricks Workflows).
+
+By using Koheesio, you can ensure that your data tasks are resilient, observable, and repeatable, adhering to good
+software engineering practices. This makes your data pipelines more reliable and easier to maintain, ultimately leading
+to more efficient and effective data processing.
+
+
+### Promoting Collaboration and Innovation
+
+Koheesio encapsulates years of software and data engineering expertise. It fosters a collaborative and innovative
+community, setting itself apart with its unique design and focus on data pipelines, data transformation, ETL jobs,
+data validation, and large-scale data processing.
+
+The core components of Koheesio are designed to bring strong software engineering principles to data engineering.
+
+'Steps' break down tasks and workflows into manageable, reusable, and testable units. Each 'Step' comes with built-in
+logging, providing transparency and traceability. The 'Context' component allows for flexible customization of task
+behavior, making it adaptable to various data processing needs.
+
+In essence, Koheesio is a comprehensive solution for data engineering challenges, designed with the principles of
+modularity, reusability, testability, and transparency at its core. It aims to provide a rich set of features including
+utilities, readers, writers, and transformations for any type of data processing. It is not in competition with other
+libraries, but rather aims to offer wide-ranging support and focus on utility in a multitude of scenarios. Our
+preference is for integration, not competition.
We invite contributions from all, promoting collaboration and innovation in the data engineering community.
+
+### Comparison to other libraries
+
+#### ML frameworks
+
+The libraries listed under this section are primarily focused on Machine Learning (ML) workflows. They provide various
+functionalities, from orchestrating ML and data processing workflows, simplifying the deployment of ML workflows on
+Kubernetes, to managing the end-to-end ML lifecycle. While these libraries have a strong emphasis on ML, Koheesio is a
+more general data pipeline framework. It is designed to handle a variety of data processing tasks, not exclusively
+focused on ML. This makes Koheesio a versatile choice for data pipeline construction, regardless of whether the
+pipeline involves ML tasks or not.
+
+- [Flyte](https://flyte.org/): A cloud-native platform for orchestrating ML and data processing workflows. Unlike Koheesio, it requires Kubernetes for deployment and has a strong focus on workflow orchestration.
+- [Kubeflow](https://kubeflow.org/): A project dedicated to simplifying the deployment of ML workflows on Kubernetes. Unlike Koheesio, it is more specialized for ML workflows.
+- [Kedro](https://kedro.readthedocs.io/): A Python framework that applies software engineering best-practice to data and machine-learning pipelines. It is similar to Koheesio but has a stronger emphasis on machine learning pipelines.
+- [Metaflow](https://docs.metaflow.org/): A human-centric framework for data science that addresses the entire data science lifecycle. It is more focused on data science projects compared to Koheesio.
+- [MLflow](https://mlflow.org/docs/latest/index.html): An open source platform for managing the end-to-end machine learning lifecycle. It is more focused on machine learning projects compared to Koheesio.
+- [TFX](https://www.tensorflow.org/tfx/guide): An end-to-end platform for deploying production ML pipelines. It is more focused on TensorFlow-based machine learning pipelines compared to Koheesio.
+- [Seldon Core](https://docs.seldon.io/projects/seldon-core/en/latest/): An open source platform for deploying machine learning models on Kubernetes. Unlike Koheesio, it is more focused on model deployment.
+
+
+#### Orchestration tools
+
+The libraries listed under this section are primarily focused on workflow orchestration. They provide various
+functionalities, from authoring, scheduling, and monitoring workflows, to building complex pipelines of batch jobs, and
+creating and executing Directed Acyclic Graphs (DAGs). Some of these libraries are designed for modern infrastructure
+and powered by open-source workflow engines, while others use a Python-style language for defining workflows. While
+these libraries have a strong emphasis on workflow orchestration, Koheesio is a more general data pipeline framework. It
+is designed to handle a variety of data processing tasks, not limited to workflow orchestration.Ccode written with
+Koheesio is often compatible with these orchestration engines. This makes Koheesio a versatile choice for data pipeline
+construction, regardless of how the pipeline orchestration is set up.
+
+- [Apache Airflow](https://airflow.apache.org/docs/): A platform to programmatically author, schedule and monitor workflows. Unlike Koheesio, it focuses on managing complex computational workflows.
+- [Luigi](https://luigi.readthedocs.io/): A Python module that helps you build complex pipelines of batch jobs. It is more focused on workflow orchestration compared to Koheesio.
+- [Databricks Workflows](https://www.databricks.com/product/workflows): A set of tools for building, debugging, deploying, and running Apache Spark workflows on Databricks.
+- [Prefect](https://docs.prefect.io/): A new workflow management system, designed for modern infrastructure and powered by the open-source Prefect Core workflow engine. It is more focused on workflow orchestration and management compared to Koheesio.
+- [Snakemake](https://snakemake.readthedocs.io/en/stable/): A workflow management system that uses a Python-style language for defining workflows. While it's powerful for creating complex workflows, Koheesio's focus on modularity and reusability might make it easier to build, test, and maintain your data pipelines.
+- [Dagster](https://docs.dagster.io/): A data orchestrator for machine learning, analytics, and ETL. It's more focused on orchestrating and visualizing data workflows compared to Koheesio.
+- [Ploomber](https://ploomber.readthedocs.io/): A Python library for building robust data pipelines. In some ways it is similar to Koheesio, but has a very different API design more focused on workflow orchestration.
+- [Pachyderm](https://docs.pachyderm.com/): A data versioning, data lineage, and workflow system running on Kubernetes. It is more focused on data versioning and lineage compared to Koheesio.
+- [Argo](https://argoproj.github.io/): An open source container-native workflow engine for orchestrating parallel jobs on Kubernetes. Unlike Koheesio, it requires Kubernetes for deployment.
+
+
+#### Others
+ The libraries listed under this section offer a variety of unique functionalities, from parallel and distributed
+ computing, to SQL-first transformation workflows, to data versioning and lineage, to data relation definition and
+ manipulation, and data warehouse management. Some of these libraries are designed for specific tasks such as
+ transforming data in warehouses using SQL, building concurrent, multi-stage data ingestion and processing pipelines,
+ or orchestrating parallel jobs on Kubernetes.
+
+- [Dask](https://dask.org/): A flexible parallel computing library for analytics. Unlike Koheesio, it is more focused on parallel computing and distributed computing. While not currently support, Dask could be a future implementation pattern for Koheesio, just like Pandas and PySpark at the moment.
+- [dbt](https://www.getdbt.com/): A SQL-first transformation workflow that also supports Python. It excels in transforming data in warehouses using SQL. In contrast, Koheesio is a more general data pipeline framework with strong typing, capable of handling a variety of data processing tasks beyond transformations.
+- [Broadway](https://elixir-broadway.org/): An Elixir library for building concurrent, multi-stage data ingestion and processing pipelines. If your team is more comfortable with Python or if you're looking for a framework that emphasizes modularity and collaboration, Koheesio could be a better fit.
+- [Ray](https://docs.ray.io/en/latest/): A general-purpose distributed computing framework. Unlike Koheesio, it is more focused on distributed computing.
+- [DataJoint](https://docs.datajoint.io/): A language for defining data relations and manipulating data. Unlike Koheesio, it is more focused on data relation definition and manipulation.
+
+
## Koheesio Core Components
-Here are the key components included in Koheesio:
+Here are the 3 core components included in Koheesio:
- __Step__: This is the fundamental unit of work in Koheesio. It represents a single operation in a data pipeline,
taking in inputs and producing outputs.
-
- ```text
- βββββββββββ ββββββββββββββββββββ ββββββββββββ
- β Input 1 βββββββββΆβ βββββββββΆβ Output 1 β
- βββββββββββ β β ββββββββββββ
- β β
- βββββββββββ β β ββββββββββββ
- β Input 2 βββββββββΆβ Step βββββββββΆβ Output 2 β
- βββββββββββ β β ββββββββββββ
- β β
- βββββββββββ β β ββββββββββββ
- β Input 3 βββββββββΆβ βββββββββΆβ Output 3 β
- βββββββββββ ββββββββββββββββββββ ββββββββββββ
- ```
-
- __Context__: This is a configuration class used to set up the environment for a Task. It can be used to share
variables across tasks and adapt the behavior of a Task based on its environment.
- __Logger__: This is a class for logging messages at different levels.
## Installation
-You can install Koheesio using either pip or poetry.
+You can install Koheesio using either pip, hatch, or poetry.
### Using Pip
@@ -81,7 +163,7 @@ pip install koheesio
If you're using Hatch for package management, you can add Koheesio to your project by simply adding koheesio to your
`pyproject.toml`.
- ```toml
+ ```toml title="pyproject.toml"
[dependencies]
koheesio = ""
```
@@ -94,34 +176,49 @@ If you're using poetry for package management, you can add Koheesio to your proj
poetry add koheesio
```
-or add the following line to your `pyproject.toml` (under `[tool.poetry.dependencies]`), making sure to replace `...` with the version you want to have installed:
+or add the following line to your `pyproject.toml` (under `[tool.poetry.dependencies]`), making sure to replace
+`...` with the version you want to have installed:
-```toml
+```toml title="pyproject.toml"
koheesio = {version = "..."}
```
-### Features
+## Extras
+
+Koheesio also provides some additional features that can be useful in certain scenarios. We call these 'integrations'.
+With an integration we mean a module that requires additional dependencies to be installed.
-Koheesio also provides some additional features that can be useful in certain scenarios. These include:
+Extras can be added by adding `extras=['name_of_the_extra']` (poetry) or `koheesio[name_of_the_extra]` (pip/hatch) to
+the `pyproject.toml` entry mentioned above or installing through pip.
-- __Spark Expectations__: Available through the `koheesio.steps.integration.spark.dq.spark_expectations` module;
- - Installable through the `se` extra.
- - SE Provides Data Quality checks for Spark DataFrames. For more information, refer to the [Spark Expectations docs](https://engineering.nike.com/spark-expectations).
+### Integrations
+
+- __Spark Expectations:__
+ Available through the `koheesio.steps.integration.spark.dq.spark_expectations` module; installable through the `se` extra.
+ - SE Provides Data Quality checks for Spark DataFrames.
+ - For more information, refer to the [Spark Expectations docs](https://engineering.nike.com/spark-expectations).
[//]: # (- **Brickflow:** Available through the `koheesio.steps.integration.workflow` module; installable through the `bf` extra.)
[//]: # ( - Brickflow is a workflow orchestration tool that allows you to define and execute workflows in a declarative way.)
[//]: # ( - For more information, refer to the [Brickflow docs](https://engineering.nike.com/brickflow))
-- __Box__: Available through the `koheesio.steps.integration.box` module
- - Installable through the `box` extra.
- - Box is a cloud content management and file sharing service for businesses.
+- __Box__:
+ Available through the `koheesio.integration.box` module; installable through the `box` extra.
+ - [Box](https://www.box.com) is a cloud content management and file sharing service for businesses.
+
+- __SFTP__:
+ Available through the `koheesio.integration.spark.sftp` module; installable through the `sftp` extra.
+ - SFTP is a network protocol used for secure file transfer over a secure shell.
+ - The SFTP integration of Koheesio relies on [paramiko](https://www.paramiko.org/)
-- __SFTP__: Available through the `koheesio.steps.integration.spark.sftp` module;
- - Installable through the `sftp` extra.
- - SFTP is a network protocol used for secure file transfer over a secure shell.
+[//]: # (TODO: add implementations)
+[//]: # (## Implementations)
+[//]: # (TODO: add async extra)
+[//]: # (TODO: add spark extra)
+[//]: # (TODO: add pandas extra)
> __Note:__
-> Some of the steps require extra dependencies. See the [Features](#features) section for additional info.
+> Some of the steps require extra dependencies. See the [Extras](#extras) section for additional info.
> Extras can be done by adding `features=['name_of_the_extra']` to the toml entry mentioned above
## Contributing
@@ -130,18 +227,21 @@ Koheesio also provides some additional features that can be useful in certain sc
We welcome contributions to our project! Here's a brief overview of our development process:
-- __Code Standards__: We use `pylint`, `black`, and `mypy` to maintain code standards. Please ensure your code passes these checks by running `make check`. No errors or warnings should be reported by the linter before you submit a pull request.
+- __Code Standards__: We use `pylint`, `black`, and `mypy` to maintain code standards. Please ensure your code passes
+ these checks by running `make check`. No errors or warnings should be reported by the linter before you submit a pull
+ request.
-- __Testing__: We use `pytest` for testing. Run the tests with `make test` and ensure all tests pass before submitting a pull request.
+- __Testing__: We use `pytest` for testing. Run the tests with `make test` and ensure all tests pass before submitting
+ a pull request.
- __Release Process__: We aim for frequent releases. Typically when we have a new feature or bugfix, a developer with
admin rights will create a new release on GitHub and publish the new version to PyPI.
-For more detailed information, please refer to our [contribution guidelines](./docs/contribute.md). We also adhere to
-[Nike's Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md) and [Nike's Individual Contributor License Agreement](https://www.clahub.com/agreements/Nike-Inc/fastbreak).
+For more detailed information, please refer to our [contribution guidelines](https://github.com/Nike-Inc/koheesio/blob/main/CONTRIBUTING.md).
+We also adhere to [Nike's Code of Conduct](https://github.com/Nike-Inc/nike-inc.github.io/blob/master/CONDUCT.md).
### Additional Resources
-- [General GitHub documentation](https://help.github.com/)
-- [GitHub pull request documentation](https://help.github.com/send-pull-requests/)
+- [General GitHub documentation](https://support.github.com/)
+- [GitHub pull request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)
- [Nike OSS](https://nike-inc.github.io/)
diff --git a/docs/assets/documentation-system-overview.png b/docs/assets/documentation-system-overview.png
new file mode 100644
index 0000000000000000000000000000000000000000..88e60165fde2abe5c42bd9cbf2278c79afc136c7
GIT binary patch
literal 209013
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