A Python-based ETL pipeline for extracting data from CSV files, transforming and validating the data and loading clean records into a MySQL database.
The project is designed as a small-scale data engineering project that demonstrates fundamental ETL concepts, data quality checks, error handling, logging and SQL-based data storage.
The pipeline processes three datasets:
- Customers - customer information
- Products - product information
- Orders - customer orders and purchased products
Each dataset goes through its own ETL pipeline. The goal is to ensure that only valid and cleaned data is loaded into the database, while invalid records are removed and preserved separately.
- Python
- Pandas
- MySQL
- SQLAlchemy
- PyMySQL
- Python-dotenv
- Git / GitHub
- Logging
- pathlib
ETL-Python-pipeline/
|
|-- .github/
| |-- workflows/
| | |-- allow-merge-to-main.yml
| | |-- auto-pr.yml
|
|-- data/
| |-- customers.csv
| |-- orders.csv
| |-- products.csv
|
|-- output/
| |-- invalid/
| | |-- customers_invalid.csv
| | |-- orders_invalid.csv
| | |-- products_invalid.csv
| |-- reports/
| | |-- customers_report.json
| | |-- orders_report.json
| | |-- products_report.json
|
|-- sql_analysis/
| |-- tables/
| | |-- customers.sql
| | |-- orders.sql
| | |-- products.sql
| |-- database.sql
| |-- queries.sql
|
|-- src/
| |-- functions/
| | |-- extract.py
| | |-- load.py
| | |-- report.py
| | |-- transform.py
| | |-- validate.py
| |-- pipelines/
| | |-- customers.py
| | |-- orders.py
| | |-- products.py
| |-- country_codes.py
| |-- main.py
| |-- paths.py
|
|-- .env.example
|-- .gitignore
|-- README.md
|-- requirements.txt
The extraction step reads the source CSV files using Pandas.
The files from where we are extracting the data are in the data/ folder
The transformation stage cleans and standardizes the raw data after validation. Examples of transformations include:
- removing leading and trailing whitespace
- normalizing multiple spaces
- standardizing names using 'Title Case'
- converting emails to lowercase
- converting country names to country codes
- converting date strings into proper date values
- converting numeric columns into appropriate numeric types
Example transformations:
- Removing leading and trailing whitespace, normalizing multiple spaces, standardizing names using Title Case
"JOhN SmiTh "
|
v
"John Smith"
- converting date strings into proper date values
"27/07/2026"
|
v
2026-07-27
Dates are converted into proper date values, so that they can be stored in the database using an appropriate DATE column.
After transformation, the data is validated against rules, specific to each dataset.
Customers:
customer_idmust not be NULLcustomer_idmust me uniquenamemust not be NULLemailmust not be NULLemailmust have a valid formatcountrymust not be NULLcreated_atmust not be NULLcreated_atmust be a valid date
Products:
product_idmust not be NULLproduct_idmust be a numeric valueproduct_idmust not be duplicatednamemust not be NULLcategorymust not be NULLpricemust be a numeric valuepricemust not be NULLpricemust be greater than 0
Orders:
order_idmust not be NULLorder_idmust be a numeric valuecustomer_idmust not be nullcustomer_idmust be a numeric valuecustomer_idmust exist in the 'customer' tableproduct_idmust not be nullproduct_idmust be a numeric valueproduct_idmust exist in the 'products' tablequantitymust not be NULLquantitymust be greater than 0quantitymust be a numeric valueorder_datemust not be NULLorder_datemust be valid
Validation is performed independently for each rule so that a single record can have multiple validation errors (this is done for more extensive reporting)
Invalid records are not just deleted.
Each dataset has it's own invalid output .csv file, that is generated in the output/invalid folder and each record contains a rejection_reason column, where it is described why the row was rejected
The pipeline also generates validation statistic for each dataset.
The report distinguishes between:
- input rows - total number of records read from the source file
- valid rows - records that passed all validation rules
- rejected rows - unique records that failed at least one validation rule
- validation failures - number of times individual rules were violated
Example of validation statistics:
{
"input rows": 1014,
"valid rows": 652,
"rejected rows": 362,
"validation failures": {
"missing_customer_id": 22,
"duplicate_customer_id": 48,
"missing_customer_name": 35,
"missing_email": 15,
"invalid_email": 25,
"missing_country_": 53,
"missing_date": 30,
"invalid_date": 246
}
}
A row can fail multiple validation rules. For instance one record can have a missing name, invalid email and invalid date. A record like that is counted once as rejected, but contributes to all three individual validation failure counters.
This prevents rejected records from being counted more than once.
Before loading the data into MySQL, I created the tables in MySQL Workbench using these statements:
CREATE TABLE customers (
customer_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
country VARCHAR(3) NOT NULL,
created_at DATE NOT NULL,
PRIMARY KEY (customer_id)
);
CREATE TABLE products (
product_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
category VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (product_id)
);
CREATE TABLE orders (
order_id INT NOT NULL,
customer_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
order_date DATE NOT NULL,
PRIMARY KEY (order_id, customer_id, product_id),
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id),
FOREIGN KEY (product_id)
REFERENCES products(product_id)
);The same statements can be found in the sql/tables folder, each in the corresponding .sql file.
After the transformation, validation and database / table creation, only valid records are loaded into a MySQL database.
Each dataset is loaded into a separate table, with a name corresponding to the dataset name.
When adding data to the database I chose to use the if_exists ='append' flag, which appends data to the table instead of dropping and recreating the table every time the pipeline is run.
We can do some analysis using the cleaned and transformed data from the dataset. Here are some of the questions that we can explore, using different queries:
- Top 10 most ordered products of all time.
- Which customers have placed the most orders?
- What is the total quantity sold for each product?
- What is the total revenue by product category?
- Which customers generated the highest revenue?
- How many orders were placed each day?
- Which products have never been ordered?
- Which month were the most customers 'created'?
- Which countries are most customers from?
Example queries are written in the sql/queries.sql file
Database credentials are stored in environment variables rather than directly in the source code.
This is an example of the .env file:
USER='your_username'
PASSWORD='your_password'
HOST='localhost'
PORT=3306
DATABASE='your_database'Personal .env file is excluded from Git, using .gitignore.
An .env.example file is included in the repository to show the required configuration without exposing credentials.
Prerequisites:
- MySQL installed
- Python installed
- Git installed
Clone the repository and open it with your code editor of choice.
https://github.com/TjasaZil/ETL-Python-pipeline.git
Create a virtual environment
python -m venv venv
Activate it
- Windows
venv\Scripts\activate
- Linux / macOS
source venv/bin/activate
Install dependencies
pip install -r requirements.txt
Rename .env.example file into .env and write in your MySQL credentials.
Run the pipelines
python -m src.main
There are three individual pipelines, one for each dataset. The pipeline extracts data from the .csv file, validates it against specific rules and removes invalid rows into it's own .csv files.
Valid data is then transformed and loaded into MySQL database.
After the execution of the pipelines the database should be populated with tables and there should be an output/ folder in the repository.
Invalid data are stored in output/invalid/ folder and simple reports are generated in the output/reports/ folder.
- containerizing the application with Docker
- adding a Docker Compose setup for MySQL
- adding a CI/CD pipeline with GitHub Actions - [added on 11.09.2026 - pipeline for auto-pr to
developmentbranch] - adding tests using pytest
- improving pipeline configuration
- adding database schema migrations
- adding pipeline execution metrics
- scheduling the pipeline
- adding monitoring and alerting
- restrict
masterbranch - [added on 11.09.2026 -masterbranch requires PR before merge]
This project was built to demonstrate practical understanding of several fundamental data engineering concepts:
- Building an ETL pipeline in python
- Working with structured data using pandas
- Data cleaning and transformation
- Data validation and simple quality checks
- Handling invalid data
- Designing reusable pipeline components
- Working with relational databases
- Loading data into MySQL
- Using Git and GitHub
- Documenting a data pipeline