Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

data-pipeline

Maven BuildMaven Central VersionjavadoccodecovGitHub

This library is a toolkit for describing data transformation pipelines by compositing simple reusable components.

A typical data-pipeline use-case can be:

  • a system aggregating results from several external services: pipelines are modular, easily rearranged and each individual step can be padded with safety nets and error handling without affecting business logic
  • a system performing iterative analysis on an input: the data-pipeline data model retains intermediate results from all steps, and each result is tagged with lineage metadata

On top of its core feature-set, complying to the data-pipeline model comes with rather nice benefits:

I. Installation

The library requires Java 17+, in order to use it, add the following in your pom.xml:

<dependency>
<groupId>tech.illuin</groupId>
<artifactId>data-pipeline</artifactId>
<version>0.32</version>
</dependency>

Additionally, some optional extension libraries can be added, at the time of this writing this includes:

II. Core Principles

The main goals behind its design were:

  1. having a fairly straightforward API and overall design: pipelines are made from compositing user-defined functions executed in a linear fashion
  2. leveraging this design to introduce useful features: data-model with lineage features, parallel execution, resilience features (retry, time-limiter, etc.), systematic performance metrics, error tracking, etc.

A simplified high-level view of a data-pipeline pipeline looks like the following diagram:

  • the main phase is composed of Step functions, they return a Result and are expected to have no side effect (think of them as almost-pure functions)
  • the end phase is composed of Sink functions, they return void and are expected to induce side effects (e.g. database persistence, message queue push, etc.)
flowchart LR
INPUT((input))
OUTPUT((output))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
STEP_1(Step 1):::step
STEP_2(Step 2):::step
STEP_N(...):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
SINK_1(Sink 1):::sink
SINK_N(...):::sink
end
end
INPUT --> PIPELINE --> OUTPUT
STEP_PHASE --> SINK_PHASE STEP_1 --> STEP_2 --> STEP_N
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

III. Basic Usage

We'll go through a quick example in order to demonstrate what data-pipeline looks like in action.

The goal of this example is to have a simple pipeline for:

  • performing a basic tokenization of a sentence
  • performing a basic analysis of said tokens
  • recovering results and logging them out
flowchart LR
INPUT((sentence))
subgraph PIPELINE[Pipeline]
direction LR
subgraph STEP_PHASE[Steps]
direction LR
TOKENIZER(Tokenizer):::step
MATCHER(Matcher):::step
end
subgraph SINK_PHASE[Sinks]
direction LR
MATCH_LOGGER(Match Logger):::sink
end
end
INPUT --> PIPELINE
STEP_PHASE --> SINK_PHASE
TOKENIZER --> MATCHER
classDef optional stroke-dasharray: 6 6;
classDef step stroke:#0f0;
classDef sink stroke:#f00;
Loading

Defining steps and sinks

First, we'll design the Tokenizer step, with a basic regex split.

Three things to note here, which will remain true for the following pieces:

  • The step's entrypoint is annotated with @StepConfig, it will be identified at the pipeline build time
  • Some component inputs have to be annotated in order to narrow down their identity, the pipeline input can be supplied with the @Input annotation
  • The step output are expected to be a Result subtype, here we chose to go with a dedicated record
publicclassTokenizer
{
@StepConfig(id = "tokenizer")
publicTokenizedSentencetokenize(@InputStringsentence)
{
returnnewTokenizedSentence(Stream.of(sentence.split("[^\\p{L}]+"))
.map(String::toLowerCase)
.toList()
);
}
publicrecordTokenizedSentence(
List<String> tokens
) implementsResult {}
}

Next up, the Matcher step, with a blacklist specified upon instantiation.

It will recover the tokenizer's output, and produce a Matches record of its findings.

Note the @Current annotation for requesting the currently known value for tokenized sentence. There is more to be said about the semantics of this annotation, which we'll cover in details in the documentation section.

publicclassMatcher
{
privatefinalSet<String> blacklist;
publicMatcher(String... blacklist)
{
this.blacklist = Set.of(blacklist);
}
@StepConfig(id = "matcher")
publicMatchesmatch(@CurrentTokenizedSentencetokenized)
{
longwordCount = tokenized.tokens().stream().distinct().count();
Set<String> matches = tokenized.tokens().stream()
.filter(this.blacklist::contains)
.collect(Collectors.toSet())
;
returnnewMatches(wordCount, matches);
}
publicrecordMatches(
longwordCount,
Set<String> blacklistMatches
) implementsResult {}
}

Finally, our MatchLogger sink works very similarly, except we need the @SinkConfig annotation.

publicclassMatchLogger
{
privatestaticfinalLoggerlogger = LoggerFactory.getLogger(MatchLogger.class);
@SinkConfig(id = "logger")
publicvoidlog(@CurrentTokenizedSentencetokenized, @CurrentMatchesmatches)
{
logger.info("Found {} unique tokens in {}, with {} blacklisted {}", matches.wordCount(), tokenized.tokens(), matches.blacklistMatches().size(), matches.blacklistMatches());
}
}

Setting-up the pipeline

Now that we have all our building blocks, creating a Pipeline is simply a matter of combining them.

The Pipeline interface offers a builder initialization method, we'll start from there.

Pipeline<String> pipeline = Pipeline.<String>of("string-processor")
.registerStep(newTokenizer())
.registerStep(newMatcher("mostly", "relatively"))
.registerSink(newMatchLogger())
.build()
;

Now, calling the pipeline with some sentences:

pipeline.run("This is a relatively short and mostly meaningless sentence.");
pipeline.run("This is a much longer sentence that should go through the blacklist unscathed.");
pipeline.run("Relatively cool objects (temperatures less than several thousand degrees) emit their radiation primarily in the infrared, as described by Planck's law.");
pipeline.run("The principles were deliberately non dogmatic, since the brotherhood wished to emphasise the personal responsibility of individual artists to determine their own ideas and methods of depiction.");
pipeline.run("The Mystical Nativity, a relatively small and very personal painting, perhaps for his own use, appears to be dated to the end of 1500.");

We should get the following output (given a simplelogger or somesuch properly configured):

[main] INFO MatchLogger - Found 9 unique tokens in [this, is, a, relatively, short, and, mostly, meaningless, sentence], with 2 blacklisted [mostly, relatively]
[main] INFO MatchLogger - Found 13 unique tokens in [this, is, a, much, longer, sentence, that, should, go, through, the, blacklist, unscathed], with 0 blacklisted []
[main] INFO MatchLogger - Found 22 unique tokens in [relatively, cool, objects, temperatures, less, than, several, thousand, degrees, emit, their, radiation, primarily, in, the, infrared, as, described, by, planck, s, law], with 1 blacklisted [relatively]
[main] INFO MatchLogger - Found 23 unique tokens in [the, principles, were, deliberately, non, dogmatic, since, the, brotherhood, wished, to, emphasise, the, personal, responsibility, of, individual, artists, to, determine, their, own, ideas, and, methods, of, depiction], with 0 blacklisted []
[main] INFO MatchLogger - Found 21 unique tokens in [the, mystical, nativity, a, relatively, small, and, very, personal, painting, perhaps, for, his, own, use, appears, to, be, dated, to, the, end, of], with 1 blacklisted [relatively]

As pipelines may use resources (notably a ServiceExecutor for the —optional— async sink execution), it is best to close it down when you are done using it (or consider using a try-with pattern):

pipeline.close();

IV. Documentation

V. Dev Installation

This project will require you to have the following:

  • Java 17+
  • Git (versioning)
  • Maven (dependency resolving, publishing and packaging)

About

Library for describing data transformation pipelines by compositing simple reusable components.

Topics

Resources

Stars

6 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages