data-processing-pipeline 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-processing-pipeline
3
+ Version: 0.1.0
4
+ Summary: Data processing pipeline
5
+ Author: Mikolaj Kawaler
@@ -0,0 +1 @@
1
+ # Data-Processing-Pipeline-Test
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: data-processing-pipeline
3
+ Version: 0.1.0
4
+ Summary: Data processing pipeline
5
+ Author: Mikolaj Kawaler
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ data_processing_pipeline.egg-info/PKG-INFO
4
+ data_processing_pipeline.egg-info/SOURCES.txt
5
+ data_processing_pipeline.egg-info/dependency_links.txt
6
+ data_processing_pipeline.egg-info/top_level.txt
7
+ pipeline/__init__.py
8
+ pipeline/config.py
9
+ pipeline/db.py
10
+ pipeline/main.py
11
+ pipeline/monitoring.py
12
+ pipeline/processing.py
13
+ tests/test_monitoring.py
14
+ tests/test_processing.py
File without changes
@@ -0,0 +1,8 @@
1
+ import logging
2
+
3
+ def setup_logging():
4
+ logging.basicConfig(
5
+ level=logging.INFO,
6
+ format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
7
+ )
8
+ return logging.getLogger("data_pipeline")
@@ -0,0 +1,39 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+
4
+ DB_PATH = Path(__file__).parent / "example.db"
5
+
6
+
7
+ def init_db():
8
+ conn = sqlite3.connect(DB_PATH)
9
+ cur = conn.cursor()
10
+
11
+ # Tabela zamówień
12
+ cur.execute(
13
+ """
14
+ CREATE TABLE IF NOT EXISTS orders (
15
+ id INTEGER PRIMARY KEY,
16
+ customer TEXT NOT NULL,
17
+ quantity INTEGER NOT NULL,
18
+ price_per_unit REAL NOT NULL
19
+ )
20
+ """
21
+ )
22
+
23
+ # Proste dane przykładowe
24
+ cur.execute("DELETE FROM orders")
25
+ cur.executemany(
26
+ "INSERT INTO orders (customer, quantity, price_per_unit) VALUES (?, ?, ?)",
27
+ [
28
+ ("Alice", 10, 5.0),
29
+ ("Bob", 3, 12.5),
30
+ ("Charlie", 0, 7.0), # quantity 0 – przyda się do walidacji
31
+ ],
32
+ )
33
+
34
+ conn.commit()
35
+ conn.close()
36
+
37
+
38
+ def get_connection():
39
+ return sqlite3.connect(DB_PATH)
@@ -0,0 +1,180 @@
1
+ from pathlib import Path
2
+ import argparse
3
+ import json
4
+ from .config import setup_logging
5
+ from .db import init_db, get_connection
6
+ from .processing import (
7
+ load_orders,
8
+ validate_orders,
9
+ deduplicate_orders,
10
+ transform_orders,
11
+ save_to_csv,
12
+ save_to_json,
13
+ save_to_parquet,
14
+ )
15
+ from .monitoring import start_monitoring, end_monitoring
16
+
17
+ OUTPUT_PATH = Path(__file__).parent / "orders_processed.csv"
18
+
19
+
20
+ def run_pipeline(
21
+ output_path: str = None,
22
+ output_format: str = "csv",
23
+ dry_run: bool = False,
24
+ enable_dedup: bool = True,
25
+ ):
26
+ """Run the data processing pipeline."""
27
+ logger = setup_logging()
28
+ logger.info("Starting pipeline (dry_run=%s)", dry_run)
29
+
30
+ # Inicjalizacja bazy i danych
31
+ init_db()
32
+ conn = get_connection()
33
+
34
+ # Monitoring start
35
+ start_time = start_monitoring()
36
+
37
+ try:
38
+ # ETL
39
+ df_raw = load_orders(conn)
40
+ df_valid = validate_orders(df_raw)
41
+
42
+ # Deduplikacja (opcjonalna)
43
+ if enable_dedup:
44
+ df_valid = deduplicate_orders(df_valid)
45
+
46
+ df_transformed = transform_orders(df_valid)
47
+
48
+ # Zapis (jeśli nie dry-run)
49
+ if not dry_run:
50
+ output_file = output_path or str(OUTPUT_PATH)
51
+
52
+ if output_format == "csv":
53
+ save_to_csv(df_transformed, output_file)
54
+ elif output_format == "json":
55
+ save_to_json(df_transformed, output_file)
56
+ elif output_format == "parquet":
57
+ save_to_parquet(df_transformed, output_file)
58
+ else:
59
+ logger.error("Unknown format: %s", output_format)
60
+ raise ValueError(f"Unsupported format: {output_format}")
61
+ else:
62
+ logger.info("[DRY RUN] Would save %d records to %s", len(df_transformed), output_path or OUTPUT_PATH)
63
+
64
+ # Monitoring end
65
+ metrics = end_monitoring(
66
+ start_time=start_time,
67
+ records_in=len(df_raw),
68
+ records_out=len(df_transformed),
69
+ )
70
+
71
+ logger.info(
72
+ "Pipeline finished in %.3f s (records in: %d, records out: %d)",
73
+ metrics.duration,
74
+ metrics.records_in,
75
+ metrics.records_out,
76
+ )
77
+
78
+ return metrics
79
+
80
+ finally:
81
+ conn.close()
82
+
83
+
84
+ def run_batch_pipeline(config_file: str, dry_run: bool = False):
85
+ """Run pipeline on multiple configs from JSON file."""
86
+ logger = setup_logging()
87
+ logger.info("Starting batch pipeline from config: %s", config_file)
88
+
89
+ with open(config_file, "r") as f:
90
+ configs = json.load(f)
91
+
92
+ results = []
93
+ for idx, config in enumerate(configs, 1):
94
+ logger.info("Processing batch %d/%d", idx, len(configs))
95
+ output_format = config.get("format", "csv")
96
+ output_path = config.get("output")
97
+ enable_dedup = config.get("deduplicate", True)
98
+
99
+ try:
100
+ metrics = run_pipeline(
101
+ output_path=output_path,
102
+ output_format=output_format,
103
+ dry_run=dry_run,
104
+ enable_dedup=enable_dedup,
105
+ )
106
+ results.append({"status": "success", "config": config, "metrics": metrics.__dict__})
107
+ except Exception as e:
108
+ logger.error("Batch %d failed: %s", idx, str(e))
109
+ results.append({"status": "error", "config": config, "error": str(e)})
110
+
111
+ return results
112
+
113
+
114
+ def main():
115
+ """CLI entry point for the pipeline."""
116
+ parser = argparse.ArgumentParser(
117
+ description="Data processing pipeline with advanced transformations"
118
+ )
119
+
120
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
121
+
122
+ # Single pipeline command
123
+ single_parser = subparsers.add_parser("run", help="Run single pipeline")
124
+ single_parser.add_argument(
125
+ "-o", "--output",
126
+ type=str,
127
+ default=None,
128
+ help="Output file path (default: pipeline/orders_processed.csv)",
129
+ )
130
+ single_parser.add_argument(
131
+ "-f", "--format",
132
+ type=str,
133
+ choices=["csv", "json", "parquet"],
134
+ default="csv",
135
+ help="Output format (default: csv)",
136
+ )
137
+ single_parser.add_argument(
138
+ "--dry-run",
139
+ action="store_true",
140
+ help="Preview without saving",
141
+ )
142
+ single_parser.add_argument(
143
+ "--no-dedup",
144
+ action="store_true",
145
+ help="Disable deduplication",
146
+ )
147
+
148
+ # Batch pipeline command
149
+ batch_parser = subparsers.add_parser("batch", help="Run batch pipeline from config")
150
+ batch_parser.add_argument(
151
+ "config",
152
+ type=str,
153
+ help="JSON config file with batch configurations",
154
+ )
155
+ batch_parser.add_argument(
156
+ "--dry-run",
157
+ action="store_true",
158
+ help="Preview without saving",
159
+ )
160
+
161
+ args = parser.parse_args()
162
+
163
+ if args.command == "run" or args.command is None:
164
+ run_pipeline(
165
+ output_path=args.output if hasattr(args, 'output') else None,
166
+ output_format=args.format if hasattr(args, 'format') else "csv",
167
+ dry_run=args.dry_run if hasattr(args, 'dry_run') else False,
168
+ enable_dedup=not (args.no_dedup if hasattr(args, 'no_dedup') else False),
169
+ )
170
+ elif args.command == "batch":
171
+ run_batch_pipeline(
172
+ config_file=args.config,
173
+ dry_run=args.dry_run,
174
+ )
175
+ else:
176
+ parser.print_help()
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,27 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+
4
+ @dataclass
5
+ class RunMetrics:
6
+ start_time: float
7
+ end_time: float
8
+ records_in: int
9
+ records_out: int
10
+
11
+ @property
12
+ def duration(self) -> float:
13
+ return self.end_time - self.start_time
14
+
15
+
16
+ def start_monitoring():
17
+ return time.time()
18
+
19
+
20
+ def end_monitoring(start_time: float, records_in: int, records_out: int) -> RunMetrics:
21
+ end_time = time.time()
22
+ return RunMetrics(
23
+ start_time=start_time,
24
+ end_time=end_time,
25
+ records_in=records_in,
26
+ records_out=records_out,
27
+ )
@@ -0,0 +1,144 @@
1
+ import pandas as pd
2
+ import logging
3
+ from functools import wraps
4
+ from time import sleep
5
+ from typing import Callable
6
+
7
+ logger = logging.getLogger("data_pipeline")
8
+
9
+
10
+ def retry_on_error(max_retries: int = 3, delay: float = 1.0):
11
+ """Decorator to retry a function on exception."""
12
+ def decorator(func: Callable):
13
+ @wraps(func)
14
+ def wrapper(*args, **kwargs):
15
+ for attempt in range(max_retries):
16
+ try:
17
+ return func(*args, **kwargs)
18
+ except Exception as e:
19
+ if attempt < max_retries - 1:
20
+ logger.warning(
21
+ "Attempt %d/%d failed: %s. Retrying in %.1fs...",
22
+ attempt + 1, max_retries, str(e), delay
23
+ )
24
+ sleep(delay)
25
+ else:
26
+ logger.error("All %d attempts failed for %s", max_retries, func.__name__)
27
+ raise
28
+ return wrapper
29
+ return decorator
30
+
31
+
32
+ def load_orders(conn) -> pd.DataFrame:
33
+ logger.info("Loading orders from database")
34
+ df = pd.read_sql_query("SELECT * FROM orders", conn)
35
+ logger.info("Loaded %d records", len(df))
36
+ return df
37
+
38
+
39
+ def validate_orders(df: pd.DataFrame) -> pd.DataFrame:
40
+ logger.info("Validating orders")
41
+
42
+ # Sprawdzenie braków
43
+ missing = df.isna().sum().sum()
44
+ if missing > 0:
45
+ logger.warning("Found %d missing values", missing)
46
+
47
+ # Sprawdzenie typów (proste)
48
+ assert "quantity" in df.columns
49
+ assert "price_per_unit" in df.columns
50
+
51
+ # Zakresy – np. quantity > 0
52
+ invalid_quantity = (df["quantity"] <= 0).sum()
53
+ if invalid_quantity > 0:
54
+ logger.warning("Found %d orders with non-positive quantity", invalid_quantity)
55
+
56
+ # Filtrowanie niepoprawnych rekordów
57
+ df_valid = df[df["quantity"] > 0].copy()
58
+ logger.info("After validation: %d valid records", len(df_valid))
59
+ return df_valid
60
+
61
+
62
+ def deduplicate_orders(df: pd.DataFrame) -> pd.DataFrame:
63
+ """Remove duplicate orders based on all columns."""
64
+ logger.info("Deduplicating orders")
65
+ initial_count = len(df)
66
+
67
+ # Usuń dokładne duplikaty
68
+ df_dedup = df.drop_duplicates().copy()
69
+
70
+ # Opcjonalnie: usuń duplikaty bazując na customer i quantity
71
+ # df_dedup = df.drop_duplicates(subset=['customer', 'quantity'], keep='first')
72
+
73
+ removed = initial_count - len(df_dedup)
74
+ if removed > 0:
75
+ logger.warning("Removed %d duplicate records", removed)
76
+
77
+ logger.info("After deduplication: %d records", len(df_dedup))
78
+ return df_dedup
79
+
80
+
81
+ def transform_orders(df: pd.DataFrame) -> pd.DataFrame:
82
+ logger.info("Transforming orders")
83
+
84
+ # Feature engineering: total_value
85
+ df["total_value"] = df["quantity"] * df["price_per_unit"]
86
+
87
+ # Normalizacja – standaryzacja total_value
88
+ total_mean = df["total_value"].mean()
89
+ total_std = df["total_value"].std()
90
+ if total_std > 0:
91
+ df["total_value_norm"] = (df["total_value"] - total_mean) / total_std
92
+ else:
93
+ df["total_value_norm"] = 0
94
+
95
+ # Zaawansowane transformacje
96
+ # 1. Percentile bucketing
97
+ df["value_percentile"] = pd.qcut(df["total_value"], q=4, labels=["Q1", "Q2", "Q3", "Q4"], duplicates="drop")
98
+
99
+ # 2. Kwantyle dla ilości
100
+ df["quantity_category"] = pd.cut(df["quantity"], bins=[0, 5, 15, float('inf')], labels=["small", "medium", "large"], include_lowest=True)
101
+
102
+ # 3. Average price normalizacja
103
+ if "price_per_unit" in df.columns:
104
+ price_mean = df["price_per_unit"].mean()
105
+ df["price_norm"] = df["price_per_unit"] / price_mean if price_mean > 0 else df["price_per_unit"]
106
+
107
+ # 4. Log transform dla total_value (aby uniknąć log(0), dodajemy 1)
108
+ df["total_value_log"] = df["total_value"].apply(lambda x: __import__('math').log(x + 1) if x >= 0 else 0)
109
+
110
+ # 5. Customer-level aggregations (jeśli mamy customer info)
111
+ if "customer" in df.columns:
112
+ customer_stats = df.groupby("customer").agg({
113
+ "total_value": ["count", "sum", "mean"]
114
+ }).reset_index()
115
+ customer_stats.columns = ["customer", "customer_order_count", "customer_total_spend", "customer_avg_spend"]
116
+ df = df.merge(customer_stats, on="customer", how="left")
117
+
118
+ logger.info("Transformation complete with %d features", len(df.columns))
119
+ return df
120
+
121
+
122
+ @retry_on_error(max_retries=3, delay=1.0)
123
+ def save_to_csv(df: pd.DataFrame, path: str):
124
+ logger.info("Saving result to %s", path)
125
+ df.to_csv(path, index=False)
126
+ logger.info("Saved %d records", len(df))
127
+
128
+
129
+ def save_to_json(df: pd.DataFrame, path: str):
130
+ """Save dataframe to JSON format."""
131
+ logger.info("Saving result to JSON: %s", path)
132
+ df.to_json(path, orient="records", indent=2)
133
+ logger.info("Saved %d records to JSON", len(df))
134
+
135
+
136
+ def save_to_parquet(df: pd.DataFrame, path: str):
137
+ """Save dataframe to Parquet format."""
138
+ logger.info("Saving result to Parquet: %s", path)
139
+ try:
140
+ df.to_parquet(path, index=False)
141
+ logger.info("Saved %d records to Parquet", len(df))
142
+ except ImportError:
143
+ logger.error("Parquet format requires 'pyarrow' or 'fastparquet' library")
144
+ raise
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "data-processing-pipeline"
7
+ version = "0.1.0"
8
+ description = "Data processing pipeline"
9
+ authors = [
10
+ { name="Mikolaj Kawaler" }
11
+ ]
12
+ dependencies = []
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from pipeline.monitoring import start_monitoring, end_monitoring
2
+
3
+
4
+ def test_monitoring_metrics():
5
+ start = start_monitoring()
6
+ metrics = end_monitoring(start, records_in=10, records_out=8)
7
+
8
+ assert metrics.records_in == 10
9
+ assert metrics.records_out == 8
10
+ assert metrics.duration >= 0
@@ -0,0 +1,32 @@
1
+ import pandas as pd
2
+ from pipeline.processing import validate_orders, transform_orders
3
+
4
+
5
+ def test_validate_orders_filters_non_positive_quantity():
6
+ df = pd.DataFrame(
7
+ {
8
+ "id": [1, 2],
9
+ "customer": ["A", "B"],
10
+ "quantity": [10, 0],
11
+ "price_per_unit": [5.0, 7.0],
12
+ }
13
+ )
14
+
15
+ df_valid = validate_orders(df)
16
+ assert len(df_valid) == 1
17
+ assert (df_valid["quantity"] > 0).all()
18
+
19
+
20
+ def test_transform_orders_creates_features():
21
+ df = pd.DataFrame(
22
+ {
23
+ "id": [1, 2],
24
+ "customer": ["A", "B"],
25
+ "quantity": [10, 3],
26
+ "price_per_unit": [5.0, 12.5],
27
+ }
28
+ )
29
+
30
+ df_transformed = transform_orders(df)
31
+ assert "total_value" in df_transformed.columns
32
+ assert "total_value_norm" in df_transformed.columns