Composable ETL Logic Layer
Project description
PETaL - Python Extract Transform and Load
A framework to compose custom ETLs on top of a logical floor.
Follow along in the /examples directory to build up arbitrary pipelines.
(This is modeled on Airflow's DAG Operator model for intuitive composition in Python.)
Getting Started
01. Trivial Case
# Let's start with the most trivial pipeline possible:
# - a Source that produces nothing
# - s Sink that does nothing with its input
# This is just to demonstrate the structure of a Petal pipeline
# First, we define the pipeline context manager.
# All the code in the inner block will get scoped to this object, automagically registering itself to the ETL!
with Pipeline("01_noop") as dag:
# Inside the Pipeline context, we define operators
# They can be Petal's built-in operators
# Or we can extend any of Petal's built-ins to support custom use cases
source = EmptySource("empty_source")
sink = NoOpSink("no_op_sink")
# Once the operators are defined, we compose them into a DAG
# The >> and << operators signify the direction of flow
source >> sink
# (an equivalent statement would have been: sink << source)
# Finally we execute the pipeline
dag.run()
02. Simple Case - Single ETL Step
# Now let's look at a non-trivial case
# We will implement 1 step from each of the ETL (Extract, Transform, Load)
with Pipeline("02_copy_file_to_file") as dag:
# Extract the data from a file - in this case, INFO and WARN logs
read_logs = FileReader("read_logs", file_path="../data/example_input.txt")
# Transform the data - filter it to only match lines that start with INFO
pattern_filter = RegexFilter("filter_info", pattern="^INFO")
# Load the data into a destination file
write_to_file = FileWriter("write_file", file_path="../data/example_output.txt")
# Specify the order of the ETL
read_logs >> pattern_filter >> write_to_file
dag.run()
03. One Source, Multiple Sinks
# Until now, we've used linear DAGs but Petal is able to handle any arbitrary DAG
# Let's demonstrate a DAG that branches out into multiple prongs
# There are multiple ways to implement this in Petal, we'll just show one here
with Pipeline("03_splitting_streams_to_multiple_destinations") as dag:
# We'll read from the same file as before
read_logs = FileReader("read_logs", file_path="../data/example_input.txt")
# This is a Splitter operator
# It will receive a single input stream and will copy it to N output streams
splitter = Splitter("split")
# Under the hood, it uses a deepcopy of the stream - but if you have many output streams
# and know that the consumers of the streams won't be mutating the data, you can implement
# a custom Splitter that just propagates the input stream without copying it.
# That's the beauty of Petal - easy extensibility!
# To demonstrate the 2 branches of the ETL, we'll apply different transformations to each branch
# For the first branch, we'll filter for INFO lines just like in example_02
pattern_filter = RegexFilter("filter_info", pattern="^INFO")
# Then write it to a file whose name indicates it's filtered
write_to_file_filtered = FileWriter("write_filtered", file_path="../data/example_output_filtered.txt")
# The other branch will just be a direct copy of the input file - no transformations applied
write_to_file_unfiltered = FileWriter("write_unfiltered", file_path="../data/example_output_unfiltered.txt")
# Here's how we define the DAG:
# The reader will extract the source data into the Splitter
read_logs >> splitter
# And then the Splitter will act like a source itself
# So we can write two branches of the ETL from the Splitter
# First branch passes through the filter, then to the filtered file
splitter >> pattern_filter >> write_to_file_filtered
# And the second branch just copies directly from the Splitter to the unfiltered file
splitter >> write_to_file_unfiltered
dag.run()
04. Multiple Sources, One Sink
# In example_03 we split streams up - now we want the ability to join streams together as well
# However, joining streams is not trivial - we need to make decisions on how the data is combined
with Pipeline("example_04_joining_multiple_streams_into_one") as dag:
# Let's start with 3 distinct sources
source_1 = FileReader("source_1", file_path="../data/source_1.txt")
source_2 = FileReader("source_2", file_path="../data/source_2.txt")
source_3 = FileReader("source_3", file_path="../data/source_3.txt")
# This is a subclass of the Joiner operator
# A generic regular Joiner accepts two inputs - the operator name and a reducer function
# To implement a StreamJoiner, we can subclass the Joiner and define its reducer function
# In this case, the reducer function concatenates the data from all the streams
joiner = StreamJoiner("joiner")
# As always, we need a Sink
write_to_file = FileWriter("write_to_file", file_path="../data/all_concatenated.txt")
# We read all the Sources into the Joiner
source_1 >> joiner
source_2 >> joiner
source_3 >> joiner
# Then route the Joiner to the Sink. As the data passes through, it's `process()` method
# will execute the reducer function on the inputs and reduce them to a single output stream.
joiner >> write_to_file
dag.run()
05. Putting It All Together
Consider the following case scenario: you're a software engineer managing the logs for your application running in a VM. Your app is producing logs, and your VM also logs events, such as connection attempts, pings, app restarts, etc.
We want to analyze the IP addresses of everyone that interacts with either the app, or the VM directly. We want to stream all of the IPs we find to a queue for our data science team to deal with. Separately, the OPS team asked us to send them all the PING attempts to the VM so they can cross-reference known threat actors.
Here's what the diagram looks like - let's use Petal to achieve this!
# We're now ready to compose a complex multi-stage ETL.
with Pipeline("example_05_multi_step_etl") as dag:
# Let's start with our Sources...
log_file = FileReader("log_file", file_path="../data/app_logs.txt")
events_file = FileReader("events_file", file_path="../data/vm_events.txt")
# ...then the non-terminal operators...
event_file_splitter = Splitter("event_file_splitter")
filter_for_helo_lines = RegexFilter("filter_for_helo_lines", "HELO")
filter_for_ping_events = RegexFilter("filter_for_ping_events", "PING")
map_to_ip_addr = RegexMapper("map_to_ip_addr", r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}")
ping_splitter = Splitter("ping_splitter")
joiner = StreamJoiner("joiner")
# ...and finally our Sinks
write_to_ping_file = FileWriter("write_to_ping_file", file_path="../data/detected_pings.txt")
write_to_sqs = MockSqsWriter("write_to_sqs", "<sqs_queue_name>", "us-east-1")
# Now let's piece together this DAG
log_file >> joiner >> write_to_sqs
events_file >> event_file_splitter
event_file_splitter >> filter_for_helo_lines >> map_to_ip_addr >> joiner
event_file_splitter >> filter_for_ping_events >> ping_splitter
ping_splitter >> joiner
ping_splitter >> write_to_ping_file
dag.run()
Theory
There are 3 types of Operators - Sources, Sinks, and Non-Terminal Operators. Pipelines in Petal are wrappers around arbitrary Directed Acyclic Graphs (DAGs). A Pipeline is constructed from a DAG and has the following invariants:
- It must have at least 1 Operator satisfying each of the terminal operator types (ie. at least 1 Source and 1 Sink).
- Operators are directional (data flows in a particular direction within the Operator).
- Each Operator has a unique ID and a reference to its upstream and downstream Operators.
- There are no cycles in the graph.
Contributing
After cloning this repo, make sure to run the following:
pip install -r requirements
pre-commit install # This will set up the pre-commit hooks
If you have suggestions
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file petal_etl_composer-0.1.10.tar.gz.
File metadata
- Download URL: petal_etl_composer-0.1.10.tar.gz
- Upload date:
- Size: 14.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67595f54a86d84d21a83c2f520a2258377695e0399d62776cbe0fd8c9e8f3cc4
|
|
| MD5 |
92a6664aa8e69f87fc25dd4f74308cbe
|
|
| BLAKE2b-256 |
29d41cae1635a2fd7bca2e6ef853d24ad644df8c79b1689fe7ad583a447b203b
|
File details
Details for the file petal_etl_composer-0.1.10-py3-none-any.whl.
File metadata
- Download URL: petal_etl_composer-0.1.10-py3-none-any.whl
- Upload date:
- Size: 22.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
debf856430f82e9114f912d3274eb041dc2cafe11ac35b62eef2a1586ae362a3
|
|
| MD5 |
44d7cefe71a2b60078fc91cad5c27819
|
|
| BLAKE2b-256 |
095271dae47c3d9b97eef6e5c0f9d8826838244a0999607711c0f7ad690d7495
|