From 42fdb567528c935429f6140349e50e705a83cec0 Mon Sep 17 00:00:00 2001 From: Mareh Date: Wed, 3 Jun 2026 23:31:32 +0200 Subject: [PATCH 1/7] implement full ci pipeline with acr container registry push --- .github/workflows/ci.yml | 20 ++++-- .gitignore | 3 + Dockerfile | 10 +-- data/messy_customers.csv | 38 +++++++++++ data/messy_sales.csv | 123 +++++++++++++++++++++++++++++++++++ requirements.txt | 21 +++--- src/clean.py | 42 ++++++++++++ src/data/messy_customers.csv | 38 +++++++++++ src/data/messy_sales.csv | 123 +++++++++++++++++++++++++++++++++++ src/ingest.py | 66 +++++++++++++++++++ src/pipeline.py | 53 +++++++++++---- src/report.py | 57 ++++++++++++++++ src/transform.py | 14 ++++ tests/test_pipeline.py | 35 ++++++++-- 14 files changed, 603 insertions(+), 40 deletions(-) create mode 100644 data/messy_customers.csv create mode 100644 data/messy_sales.csv create mode 100644 src/clean.py create mode 100644 src/data/messy_customers.csv create mode 100644 src/data/messy_sales.csv create mode 100644 src/ingest.py create mode 100644 src/report.py create mode 100644 src/transform.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e91465..143fc8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ name: CI on: push: - branches: ["TODO-replace-with-main"] + branches: ["main"] pull_request: jobs: @@ -21,10 +21,20 @@ jobs: - name: Install dependencies run: pip install -r requirements.txt - name: Lint - run: echo "TODO implement this step" + run: ruff check src - name: Format - run: echo "TODO implement this step" + run: ruff format --check src - name: Test - run: echo "TODO implement this step" + run: pytest -q - name: Build image - run: echo "TODO implement this step" + run: docker build -t mareh-aboghanem-pipeline:${{ github.sha }} . + - name: Azure login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: ACR login + run: az acr login --name hyfregistry + - name: Push image + run: | + docker tag mareh-aboghanem-pipeline:${{ github.sha }} hyfregistry.azurecr.io/mareh-aboghanem-pipeline:${{ github.sha }} + docker push hyfregistry.azurecr.io/mareh-aboghanem-pipeline:${{ github.sha }} diff --git a/.gitignore b/.gitignore index ec8f344..e320bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ Thumbs.db # hyf .hyf/score.json +#My files +.my_side/ + # Editor and IDE settings .vscode/ .idea/ diff --git a/Dockerfile b/Dockerfile index d665b11..3a30da9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,15 +10,15 @@ # Replace each TODO comment with the correct Dockerfile instruction. # TODO: set the base image -FROM TODO +FROM python:3.11-slim WORKDIR /app # TODO: copy requirements.txt (before source — this keeps the install layer cached) - +COPY requirements.txt . # TODO: install dependencies - +RUN pip install -r requirements.txt # TODO: copy source code - +COPY src/ src # TODO: set the command that runs when the container starts -CMD ["TODO"] +CMD ["python", "-m", "src.pipeline"] diff --git a/data/messy_customers.csv b/data/messy_customers.csv new file mode 100644 index 0000000..31b2e1f --- /dev/null +++ b/data/messy_customers.csv @@ -0,0 +1,38 @@ +customer_email,customer_name,region,signup_date,loyalty_tier +alice@example.com,Alice van den Berg,NL,2023-11-15,Gold +Bob@Company.COM,Bob De Smet,BE,2023-12-01,Silver +charlie@work.org,Charlie Müller,DE,2024-01-10,Bronze +dave@email.com,,NL,2024-01-15,Silver +eve@startup.io,Eve Jansen,NL,2024-02-01,Gold +frank@corp.com,Frank Dubois,FR,2024-02-14,Bronze +Grace@University.EDU,Grace van Dijk,NL,2024-03-01,Silver +henry@business.com,Henry Peeters,BE,2024-03-10,Bronze +ivan@email.com,Ivan Schneider,DE,2024-03-15,Silver +jenny@work.org,Jenny Laurent,FR,2024-01-20,Gold +karl@startup.io,Karl Bakker,NL,2024-02-28,Bronze +lena@mail.nl,Lena de Vries,NL,2024-04-01,Silver +Marco@Business.DE,Marco Weber,DE,2024-04-10,Gold +nina@university.edu,,NL,2024-04-15,Bronze +oliver@corp.com,Oliver Martin,FR,2024-05-01,Silver +Paula@Startup.IO,Paula Visser,NL,2024-05-10,Bronze +quinn@work.org,Quinn Claes,BE,2024-05-15,Silver +rachel@email.com,Rachel Schmitt,DE,not_a_date,Gold +simon@company.com,Simon Leroy,FR,2024-06-01,Bronze +tina@mail.nl,Tina Meijer,NL,2024-06-05,Silver +uwe@business.de,Uwe Fischer,DE,2024-06-10,Gold +vera@mail.nl,Vera Willems,BE,2024-06-15,Bronze +wendy@startup.io,Wendy van Leeuwen,NL,2024-01-05,Silver +xander@corp.com,Xander Moreau,FR,2024-02-20,Gold +Yara@University.EDU,Yara Hendriks,NL,2024-03-25,Bronze +zach@email.com,Zach Bauer,DE,2024-04-05,Silver +orphan_customer1@mail.nl,Dirk Janssen,NL,2024-01-01,Gold +orphan_customer2@business.de,Petra Hofmann,DE,2024-02-15,Bronze +orphan_customer3@work.fr,Louis Petit,FR,2024-03-20,Silver +orphan_customer4@mail.be,,BE,invalid_date,Gold +sam@company.com,Sam de Groot,NL,2024-05-20,Silver +lisa@email.com,Lisa Maes,BE,2024-06-01,Bronze +tom@corp.com,Tom Bernard,FR,2024-04-18,Gold +emma@startup.io,Emma van Houten,NL,2024-03-12,Silver +jan@mail.nl,Jan de Boer,NL,2024-02-08,Bronze +sophie@business.de,Sophie Klein,DE,2024-05-25,Gold +orphan_customer5@university.edu,Maria Garcia,FR,2024-06-20,Silver diff --git a/data/messy_sales.csv b/data/messy_sales.csv new file mode 100644 index 0000000..77ce766 --- /dev/null +++ b/data/messy_sales.csv @@ -0,0 +1,123 @@ +transaction_id,product_name,category,price,quantity,customer_email,date +1, laptop PRO ,Electronics,999.99,2,alice@example.com,2024-03-15 +2,WIRELESS MOUSE,Electronics,29.99,5, BOB@Company.COM ,2024-03-15 +3, usb cable,Electronics,4.99,10,,2024-03-16 +4, Office Chair ,Furniture,349.50,1,charlie@work.org,2024-03-16 +5,standing DESK,Furniture,599.00,1,charlie@work.org,not_a_date +6,,Electronics,19.99,3,dave@email.com,2024-03-17 +7, Mechanical Keyboard ,Electronics,-89.99,2,eve@startup.io,2024-03-17 +8,monitor ARM,Furniture,79.99,0,frank@corp.com,2024-03-18 +9, WEBCAM hd ,Electronics,54.99,1, ,2024-03-18 +10,desk lamp,Furniture,34.99,4,grace@university.edu,2024-03-19 +11, NOISE CANCELLING headphones,Electronics,199.99,1,alice@example.com,2024-03-19 +12,cable management KIT,Furniture,15.99,6,henry@business.com,2024-03-20 +13, ergonomic MOUSE PAD ,Furniture,24.99,3,ivan@email.com,2024-03-20 +14,laptop STAND,Furniture,45.99,2,jenny@work.org,2024-03-21 +15, BLUETOOTH speaker,,39.99,1,karl@startup.io,2024-03-21 +16,Docking Station,Electronics,129.99,2,alice@example.com,2024-03-22 +17, USB HUB ,Electronics,24.99,3,bob@company.com,2024-03-23 +18,WIRELESS CHARGER,Electronics,35.99,1,lena@mail.nl,2024-03-24 +19, desk organizer,Furniture,22.50,2,marco@business.de,2024-03-25 +20,Monitor 27 Inch,Electronics,449.00,1,nina@university.edu,2024-03-26 +21, STANDING desk MAT ,Furniture,67.99,1,oliver@corp.com,2024-03-27 +22,webcam RING LIGHT,Electronics,19.99,4,paula@startup.io,2024-03-28 +23,Laptop Bag,Accessories,49.99,2,quinn@work.org,2024-03-29 +24, mouse pad XL,Furniture,18.99,3,rachel@email.com,2024-03-30 +25,power STRIP,Electronics,14.99,5,simon@company.com,2024-03-31 +26, Ergonomic Footrest ,Furniture,45.00,1,tina@mail.nl,2024-04-01 +27,HDMI Cable,Electronics,9.99,8,alice@example.com,2024-04-02 +28, wireless KEYBOARD ,Electronics,69.99,2,bob@company.com,2024-04-03 +29,Desk Shelf,Furniture,89.99,1,charlie@work.org,2024-04-04 +30, SCREEN PROTECTOR ,Accessories,12.99,6,dave@email.com,2024-04-05 +31,,Furniture,199.99,1,eve@startup.io,2024-04-06 +32,Adjustable Monitor Stand,Furniture,159.00,1,frank@corp.com,2024-04-07 +33, usb c ADAPTER ,Electronics,15.99,3,grace@university.edu,2024-04-08 +34,Cable Clips,Accessories,7.99,10,unknown_buyer@gmail.com,2024-04-09 +35,LAPTOP COOLING pad,Electronics,32.99,2,ivan@email.com,2024-04-10 +36, desk PLANT pot ,Furniture,11.99,4,jenny@work.org,2024-04-11 +37,Webcam Cover,Accessories,4.99,15,karl@startup.io,2024-04-12 +38, MECHANICAL keyboard ,Electronics,-149.99,1,lena@mail.nl,2024-04-13 +39,Office Lamp LED,Furniture,54.99,2,marco@business.de,2024-04-14 +40, ethernet cable ,Electronics,8.99,7,nina@university.edu,2024-04-15 +41,Standing Desk Frame,Furniture,399.00,1,oliver@corp.com,2024-04-16 +42, wireless PRESENTER ,Electronics,29.99,2,paula@startup.io,2024-04-17 +43,Drawer Organizer,Furniture,16.99,3,quinn@work.org,2024-04-18 +44,,Electronics,44.99,2,rachel@email.com,2024-04-19 +45, PORTABLE monitor ,Electronics,279.99,1,simon@company.com,2024-04-20 +46,Desk Cable Tray,Furniture,24.99,2,tina@mail.nl,2024-04-21 +47, noise machine ,,39.99,1,uwe@business.de,2024-04-22 +48,LAPTOP RISER,Furniture,55.99,1,vera@mail.nl,2024-04-23 +49, usb MICROPHONE ,Electronics,79.99,1,wendy@startup.io,2024-04-24 +50,Whiteboard Small,Furniture,29.99,2,xander@corp.com,2024-04-25 +51, POWER bank ,Electronics,49.99,3,yara@university.edu,2024-04-26 +52,Desk Mat Large,Furniture,34.99,1,zach@email.com,2024-04-27 +53,Monitor Light Bar,Electronics,59.99,1,alice@example.com,2024-04-28 +54, FILE organizer ,Furniture,13.99,4,bob@company.com,2024-04-29 +55,Webcam Tripod,Accessories,21.99,2,charlie@work.org,2024-04-30 +56, SURGE protector ,Electronics,-19.99,3,dave@email.com,2024-05-01 +57,Keyboard Wrist Rest,Furniture,17.99,2,eve@startup.io,2024-05-02 +58, hdmi SPLITTER ,Electronics,22.99,2,frank@corp.com,2024-05-03 +59,Desk Fan USB,Electronics,15.99,3,temp_worker@messycorp.com,2024-05-04 +60, bookend SET ,Furniture,19.99,2,henry@business.com,2024-05-05 +61,Wireless Mouse Pad,Electronics,42.99,1,ivan@email.com,2024-05-06 +62,,,29.99,2,jenny@work.org,2024-05-07 +63, LAPTOP sleeve ,Accessories,27.99,2,karl@startup.io,2024-05-08 +64,Smart Power Strip,Electronics,39.99,1,lena@mail.nl,2024-05-09 +65, monitor CLEANING kit ,Accessories,11.99,5,marco@business.de,2024-05-10 +66,Desk Drawer Unit,Furniture,129.99,1,nina@university.edu,2024-05-11 +67, PHONE stand ,Accessories,14.99,3,oliver@corp.com,2024-05-12 +68,USB Docking Hub,Electronics,89.99,1,paula@startup.io,2024-05-13 +69, cable MANAGEMENT box ,Furniture,21.99,2,quinn@work.org,2024-05-14 +70,LED Desk Lamp,Furniture,44.99,2,rachel@email.com,2024-05-15 +71,Portable Charger,Electronics,34.99,0,simon@company.com,2024-05-16 +72, OFFICE whiteboard ,Furniture,79.99,1,tina@mail.nl,2024-05-17 +73,Mouse Bungee,,16.99,2,uwe@business.de,2024-05-18 +74, laptop DOCKING station ,Electronics,189.99,1,vera@mail.nl,2024-05-19 +75,Paper Tray,Furniture,9.99,4,,2024-05-20 +76, BLUETOOTH adapter ,Electronics,12.99,3,wendy@startup.io,2024-05-21 +77,Monitor Privacy Screen,Accessories,49.99,1,xander@corp.com,2024-05-22 +78, document HOLDER ,Furniture,23.99,2,yara@university.edu,2024-05-23 +79,Portable SSD,Electronics,119.99,1,zach@email.com,2024-05-24 +80, DESK calendar ,Furniture,8.99,5,alice@example.com,2024-05-25 +81,Webcam HD Pro,Electronics,89.99,1,bob@company.com,2024-05-26 +82, pen HOLDER ,Furniture,6.99,6,charlie@work.org,2024-05-27 +83,Gaming Mouse,Electronics,59.99,2,dave@email.com,2024-05-28 +16,Docking Station,Electronics,129.99,2,alice@example.com,2024-03-22 +84, WIRELESS earbuds ,Electronics,79.99,1,eve@startup.io,2024-05-29 +85,Desk Hutch,Furniture,199.99,1,frank@corp.com,2024-05-30 +86, usb FAN ,Electronics,11.99,3,grace@university.edu,2024-05-31 +87,Keyboard Cover,Accessories,9.99,4,freelancer@outlook.com,2024-06-01 +88, MINI projector ,Electronics,299.99,1,ivan@email.com,2024-06-02 +89,Foot Hammock,Furniture,18.99,2,jenny@work.org,2024-06-03 +90, screen CLEANING wipes ,Accessories,5.99,10,karl@startup.io,2024-06-04 +91,Portable Speaker,Electronics,44.99,1,lena@mail.nl,2024-06-05 +92,,Accessories,14.99,3,marco@business.de,2024-06-06 +93, LAPTOP lock ,Accessories,24.99,2,nina@university.edu,2024-06-07 +94,Desk Pad Leather,Furniture,64.99,1,oliver@corp.com,2024-06-08 +95, wireless HEADSET ,Electronics,149.99,1,paula@startup.io,2024-06-09 +96,Clip-on Desk Light,Furniture,27.99,3,quinn@work.org,2024-06-10 +97,USB Wall Charger,Electronics,18.99,4,intern2024@messycorp.com,2024-06-11 +98, DESK storage BOX ,Furniture,32.99,2,simon@company.com,2024-06-12 +99,Portable Monitor Stand,Furniture,44.99,0,tina@mail.nl,2024-06-13 +100, webcam LIGHT ,Electronics,22.99,2,uwe@business.de,2024-06-14 +101,Desk Shelf Riser,Furniture,37.99,1,vera@mail.nl,2024-06-15 +102, CABLE tester ,Electronics,29.99,1,wendy@startup.io,2024-06-16 +103,Under Desk Drawer,Furniture,41.99,1,xander@corp.com,2024-06-17 +53,Monitor Light Bar,Electronics,59.99,1,alice@example.com,2024-04-28 +104, SD card READER ,Electronics,14.99,3,yara@university.edu,2024-06-18 +105,Desktop Organizer Set,Furniture,54.99,1,zach@email.com,2024-06-19 +106, GAMING keyboard ,Electronics,129.99,1,alice@example.com,2024-06-20 +107,Laptop Stand Pro,Furniture,4999.99,1,bob@company.com,2024-06-21 +108, usb LIGHT strip ,,16.99,3,charlie@work.org,2024-06-22 +109,Desk Clock Digital,Furniture,19.99,2,dave@email.com,2024-06-23 +110, WIRELESS trackball ,Electronics,64.99,1,eve@startup.io,2024-06-24 +111,Office Partition,Furniture,2499.00,1,frank@corp.com,2024-06-25 +112, laptop SCREEN cleaner ,Accessories,7.99,5,grace@university.edu,2024-06-26 +113,Smart Plug,Electronics,19.99,4,henry@business.com,2024-06-27 +114, DESK lamp WIRELESS ,Furniture,69.99,1,ivan@email.com,2024-06-28 +115,Monitor Arm Dual,Furniture,179.99,1,jenny@work.org,2024-06-29 +116, PHONE charger ,Electronics,12.99,5, ,2024-06-30 +117,Keyboard Cleaner,Accessories,8.99,3,lena@mail.nl,2024-04-22 +118, desk TIDY caddy ,Furniture,15.99,2,marco@business.de,2024-13-01 +119,USB Hub Powered,Electronics,34.99,2,nina@university.edu, +120, WEBCAM stand ,Accessories,18.99,1,oliver@corp.com,2024-05-15 diff --git a/requirements.txt b/requirements.txt index 42299cf..0a141b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,8 @@ -# Task 2: Pin every dependency your pipeline uses. -# -# Format: package==version -# Example: requests==2.31.0 -# -# Find the current version of any package: -# pip show -# -# Always include pytest and ruff: -# pytest== -# ruff== -# -# Add your pinned dependencies below: +azure-storage-blob +matplotlib==3.10.9 +pandas==3.0.3 +pytest==9.0.3 +python-dotenv==1.2.2 +pyarrow +azure-identity +ruff diff --git a/src/clean.py b/src/clean.py new file mode 100644 index 0000000..f785e3a --- /dev/null +++ b/src/clean.py @@ -0,0 +1,42 @@ +import logging +from pathlib import Path +import pandas as pd + + +def load_and_explore(data_dir: Path) -> tuple[pd.DataFrame, pd.DataFrame]: + data_sales = pd.read_csv(data_dir / "messy_sales.csv") + data_customers = pd.read_csv(data_dir / "messy_customers.csv") + + logging.info("--- Exploring Data ---") + logging.info("\n== Sales Data ==") + data_sales.info() + logging.info(f"\nDescribe:\n{data_sales.describe()}") + logging.info(f"\nRows:\n{data_sales.head(20)}") + logging.info(f"\nMissing Values:\n{data_sales.isna().sum()}") + logging.info("Exploration of Sales Data is complete.") + logging.info("\n== Customers Data ==") + data_customers.info() + logging.info(f"\nDescribe:\n{data_customers.describe()}") + logging.info(f"\nFirst Rows:\n{data_customers.head(20)}") + logging.info(f"\nMissing Values:\n{data_customers.isna().sum()}") + logging.info("Exploration of Customers Data is complete.") + return data_sales, data_customers + + +def clean_sales(sales: pd.DataFrame) -> pd.DataFrame: + product_name = sales["product_name"].str.strip().str.title() + sales["product_name"] = product_name + customer_email = sales["customer_email"].str.lower().str.strip() + sales["customer_email"] = customer_email + price = pd.to_numeric(sales["price"], errors="coerce") + sales["price"] = price + date = pd.to_datetime(sales["date"], errors="coerce") + sales["date"] = date + sales = sales.dropna(subset=["product_name"]) + sales = sales[sales["price"] >= 0] + sales = sales[sales["quantity"] > 0] + sales = sales.dropna(subset=["date"]) + sales = sales.drop_duplicates(subset="transaction_id", keep="first") + logging.info(f"cleaning complete. Rows remaining: {len(sales)}") + # Decision: Leave outlier prices as they are. Why? Because I think they could be valid values and I need to understand the detalis of the data. + return sales diff --git a/src/data/messy_customers.csv b/src/data/messy_customers.csv new file mode 100644 index 0000000..31b2e1f --- /dev/null +++ b/src/data/messy_customers.csv @@ -0,0 +1,38 @@ +customer_email,customer_name,region,signup_date,loyalty_tier +alice@example.com,Alice van den Berg,NL,2023-11-15,Gold +Bob@Company.COM,Bob De Smet,BE,2023-12-01,Silver +charlie@work.org,Charlie Müller,DE,2024-01-10,Bronze +dave@email.com,,NL,2024-01-15,Silver +eve@startup.io,Eve Jansen,NL,2024-02-01,Gold +frank@corp.com,Frank Dubois,FR,2024-02-14,Bronze +Grace@University.EDU,Grace van Dijk,NL,2024-03-01,Silver +henry@business.com,Henry Peeters,BE,2024-03-10,Bronze +ivan@email.com,Ivan Schneider,DE,2024-03-15,Silver +jenny@work.org,Jenny Laurent,FR,2024-01-20,Gold +karl@startup.io,Karl Bakker,NL,2024-02-28,Bronze +lena@mail.nl,Lena de Vries,NL,2024-04-01,Silver +Marco@Business.DE,Marco Weber,DE,2024-04-10,Gold +nina@university.edu,,NL,2024-04-15,Bronze +oliver@corp.com,Oliver Martin,FR,2024-05-01,Silver +Paula@Startup.IO,Paula Visser,NL,2024-05-10,Bronze +quinn@work.org,Quinn Claes,BE,2024-05-15,Silver +rachel@email.com,Rachel Schmitt,DE,not_a_date,Gold +simon@company.com,Simon Leroy,FR,2024-06-01,Bronze +tina@mail.nl,Tina Meijer,NL,2024-06-05,Silver +uwe@business.de,Uwe Fischer,DE,2024-06-10,Gold +vera@mail.nl,Vera Willems,BE,2024-06-15,Bronze +wendy@startup.io,Wendy van Leeuwen,NL,2024-01-05,Silver +xander@corp.com,Xander Moreau,FR,2024-02-20,Gold +Yara@University.EDU,Yara Hendriks,NL,2024-03-25,Bronze +zach@email.com,Zach Bauer,DE,2024-04-05,Silver +orphan_customer1@mail.nl,Dirk Janssen,NL,2024-01-01,Gold +orphan_customer2@business.de,Petra Hofmann,DE,2024-02-15,Bronze +orphan_customer3@work.fr,Louis Petit,FR,2024-03-20,Silver +orphan_customer4@mail.be,,BE,invalid_date,Gold +sam@company.com,Sam de Groot,NL,2024-05-20,Silver +lisa@email.com,Lisa Maes,BE,2024-06-01,Bronze +tom@corp.com,Tom Bernard,FR,2024-04-18,Gold +emma@startup.io,Emma van Houten,NL,2024-03-12,Silver +jan@mail.nl,Jan de Boer,NL,2024-02-08,Bronze +sophie@business.de,Sophie Klein,DE,2024-05-25,Gold +orphan_customer5@university.edu,Maria Garcia,FR,2024-06-20,Silver diff --git a/src/data/messy_sales.csv b/src/data/messy_sales.csv new file mode 100644 index 0000000..77ce766 --- /dev/null +++ b/src/data/messy_sales.csv @@ -0,0 +1,123 @@ +transaction_id,product_name,category,price,quantity,customer_email,date +1, laptop PRO ,Electronics,999.99,2,alice@example.com,2024-03-15 +2,WIRELESS MOUSE,Electronics,29.99,5, BOB@Company.COM ,2024-03-15 +3, usb cable,Electronics,4.99,10,,2024-03-16 +4, Office Chair ,Furniture,349.50,1,charlie@work.org,2024-03-16 +5,standing DESK,Furniture,599.00,1,charlie@work.org,not_a_date +6,,Electronics,19.99,3,dave@email.com,2024-03-17 +7, Mechanical Keyboard ,Electronics,-89.99,2,eve@startup.io,2024-03-17 +8,monitor ARM,Furniture,79.99,0,frank@corp.com,2024-03-18 +9, WEBCAM hd ,Electronics,54.99,1, ,2024-03-18 +10,desk lamp,Furniture,34.99,4,grace@university.edu,2024-03-19 +11, NOISE CANCELLING headphones,Electronics,199.99,1,alice@example.com,2024-03-19 +12,cable management KIT,Furniture,15.99,6,henry@business.com,2024-03-20 +13, ergonomic MOUSE PAD ,Furniture,24.99,3,ivan@email.com,2024-03-20 +14,laptop STAND,Furniture,45.99,2,jenny@work.org,2024-03-21 +15, BLUETOOTH speaker,,39.99,1,karl@startup.io,2024-03-21 +16,Docking Station,Electronics,129.99,2,alice@example.com,2024-03-22 +17, USB HUB ,Electronics,24.99,3,bob@company.com,2024-03-23 +18,WIRELESS CHARGER,Electronics,35.99,1,lena@mail.nl,2024-03-24 +19, desk organizer,Furniture,22.50,2,marco@business.de,2024-03-25 +20,Monitor 27 Inch,Electronics,449.00,1,nina@university.edu,2024-03-26 +21, STANDING desk MAT ,Furniture,67.99,1,oliver@corp.com,2024-03-27 +22,webcam RING LIGHT,Electronics,19.99,4,paula@startup.io,2024-03-28 +23,Laptop Bag,Accessories,49.99,2,quinn@work.org,2024-03-29 +24, mouse pad XL,Furniture,18.99,3,rachel@email.com,2024-03-30 +25,power STRIP,Electronics,14.99,5,simon@company.com,2024-03-31 +26, Ergonomic Footrest ,Furniture,45.00,1,tina@mail.nl,2024-04-01 +27,HDMI Cable,Electronics,9.99,8,alice@example.com,2024-04-02 +28, wireless KEYBOARD ,Electronics,69.99,2,bob@company.com,2024-04-03 +29,Desk Shelf,Furniture,89.99,1,charlie@work.org,2024-04-04 +30, SCREEN PROTECTOR ,Accessories,12.99,6,dave@email.com,2024-04-05 +31,,Furniture,199.99,1,eve@startup.io,2024-04-06 +32,Adjustable Monitor Stand,Furniture,159.00,1,frank@corp.com,2024-04-07 +33, usb c ADAPTER ,Electronics,15.99,3,grace@university.edu,2024-04-08 +34,Cable Clips,Accessories,7.99,10,unknown_buyer@gmail.com,2024-04-09 +35,LAPTOP COOLING pad,Electronics,32.99,2,ivan@email.com,2024-04-10 +36, desk PLANT pot ,Furniture,11.99,4,jenny@work.org,2024-04-11 +37,Webcam Cover,Accessories,4.99,15,karl@startup.io,2024-04-12 +38, MECHANICAL keyboard ,Electronics,-149.99,1,lena@mail.nl,2024-04-13 +39,Office Lamp LED,Furniture,54.99,2,marco@business.de,2024-04-14 +40, ethernet cable ,Electronics,8.99,7,nina@university.edu,2024-04-15 +41,Standing Desk Frame,Furniture,399.00,1,oliver@corp.com,2024-04-16 +42, wireless PRESENTER ,Electronics,29.99,2,paula@startup.io,2024-04-17 +43,Drawer Organizer,Furniture,16.99,3,quinn@work.org,2024-04-18 +44,,Electronics,44.99,2,rachel@email.com,2024-04-19 +45, PORTABLE monitor ,Electronics,279.99,1,simon@company.com,2024-04-20 +46,Desk Cable Tray,Furniture,24.99,2,tina@mail.nl,2024-04-21 +47, noise machine ,,39.99,1,uwe@business.de,2024-04-22 +48,LAPTOP RISER,Furniture,55.99,1,vera@mail.nl,2024-04-23 +49, usb MICROPHONE ,Electronics,79.99,1,wendy@startup.io,2024-04-24 +50,Whiteboard Small,Furniture,29.99,2,xander@corp.com,2024-04-25 +51, POWER bank ,Electronics,49.99,3,yara@university.edu,2024-04-26 +52,Desk Mat Large,Furniture,34.99,1,zach@email.com,2024-04-27 +53,Monitor Light Bar,Electronics,59.99,1,alice@example.com,2024-04-28 +54, FILE organizer ,Furniture,13.99,4,bob@company.com,2024-04-29 +55,Webcam Tripod,Accessories,21.99,2,charlie@work.org,2024-04-30 +56, SURGE protector ,Electronics,-19.99,3,dave@email.com,2024-05-01 +57,Keyboard Wrist Rest,Furniture,17.99,2,eve@startup.io,2024-05-02 +58, hdmi SPLITTER ,Electronics,22.99,2,frank@corp.com,2024-05-03 +59,Desk Fan USB,Electronics,15.99,3,temp_worker@messycorp.com,2024-05-04 +60, bookend SET ,Furniture,19.99,2,henry@business.com,2024-05-05 +61,Wireless Mouse Pad,Electronics,42.99,1,ivan@email.com,2024-05-06 +62,,,29.99,2,jenny@work.org,2024-05-07 +63, LAPTOP sleeve ,Accessories,27.99,2,karl@startup.io,2024-05-08 +64,Smart Power Strip,Electronics,39.99,1,lena@mail.nl,2024-05-09 +65, monitor CLEANING kit ,Accessories,11.99,5,marco@business.de,2024-05-10 +66,Desk Drawer Unit,Furniture,129.99,1,nina@university.edu,2024-05-11 +67, PHONE stand ,Accessories,14.99,3,oliver@corp.com,2024-05-12 +68,USB Docking Hub,Electronics,89.99,1,paula@startup.io,2024-05-13 +69, cable MANAGEMENT box ,Furniture,21.99,2,quinn@work.org,2024-05-14 +70,LED Desk Lamp,Furniture,44.99,2,rachel@email.com,2024-05-15 +71,Portable Charger,Electronics,34.99,0,simon@company.com,2024-05-16 +72, OFFICE whiteboard ,Furniture,79.99,1,tina@mail.nl,2024-05-17 +73,Mouse Bungee,,16.99,2,uwe@business.de,2024-05-18 +74, laptop DOCKING station ,Electronics,189.99,1,vera@mail.nl,2024-05-19 +75,Paper Tray,Furniture,9.99,4,,2024-05-20 +76, BLUETOOTH adapter ,Electronics,12.99,3,wendy@startup.io,2024-05-21 +77,Monitor Privacy Screen,Accessories,49.99,1,xander@corp.com,2024-05-22 +78, document HOLDER ,Furniture,23.99,2,yara@university.edu,2024-05-23 +79,Portable SSD,Electronics,119.99,1,zach@email.com,2024-05-24 +80, DESK calendar ,Furniture,8.99,5,alice@example.com,2024-05-25 +81,Webcam HD Pro,Electronics,89.99,1,bob@company.com,2024-05-26 +82, pen HOLDER ,Furniture,6.99,6,charlie@work.org,2024-05-27 +83,Gaming Mouse,Electronics,59.99,2,dave@email.com,2024-05-28 +16,Docking Station,Electronics,129.99,2,alice@example.com,2024-03-22 +84, WIRELESS earbuds ,Electronics,79.99,1,eve@startup.io,2024-05-29 +85,Desk Hutch,Furniture,199.99,1,frank@corp.com,2024-05-30 +86, usb FAN ,Electronics,11.99,3,grace@university.edu,2024-05-31 +87,Keyboard Cover,Accessories,9.99,4,freelancer@outlook.com,2024-06-01 +88, MINI projector ,Electronics,299.99,1,ivan@email.com,2024-06-02 +89,Foot Hammock,Furniture,18.99,2,jenny@work.org,2024-06-03 +90, screen CLEANING wipes ,Accessories,5.99,10,karl@startup.io,2024-06-04 +91,Portable Speaker,Electronics,44.99,1,lena@mail.nl,2024-06-05 +92,,Accessories,14.99,3,marco@business.de,2024-06-06 +93, LAPTOP lock ,Accessories,24.99,2,nina@university.edu,2024-06-07 +94,Desk Pad Leather,Furniture,64.99,1,oliver@corp.com,2024-06-08 +95, wireless HEADSET ,Electronics,149.99,1,paula@startup.io,2024-06-09 +96,Clip-on Desk Light,Furniture,27.99,3,quinn@work.org,2024-06-10 +97,USB Wall Charger,Electronics,18.99,4,intern2024@messycorp.com,2024-06-11 +98, DESK storage BOX ,Furniture,32.99,2,simon@company.com,2024-06-12 +99,Portable Monitor Stand,Furniture,44.99,0,tina@mail.nl,2024-06-13 +100, webcam LIGHT ,Electronics,22.99,2,uwe@business.de,2024-06-14 +101,Desk Shelf Riser,Furniture,37.99,1,vera@mail.nl,2024-06-15 +102, CABLE tester ,Electronics,29.99,1,wendy@startup.io,2024-06-16 +103,Under Desk Drawer,Furniture,41.99,1,xander@corp.com,2024-06-17 +53,Monitor Light Bar,Electronics,59.99,1,alice@example.com,2024-04-28 +104, SD card READER ,Electronics,14.99,3,yara@university.edu,2024-06-18 +105,Desktop Organizer Set,Furniture,54.99,1,zach@email.com,2024-06-19 +106, GAMING keyboard ,Electronics,129.99,1,alice@example.com,2024-06-20 +107,Laptop Stand Pro,Furniture,4999.99,1,bob@company.com,2024-06-21 +108, usb LIGHT strip ,,16.99,3,charlie@work.org,2024-06-22 +109,Desk Clock Digital,Furniture,19.99,2,dave@email.com,2024-06-23 +110, WIRELESS trackball ,Electronics,64.99,1,eve@startup.io,2024-06-24 +111,Office Partition,Furniture,2499.00,1,frank@corp.com,2024-06-25 +112, laptop SCREEN cleaner ,Accessories,7.99,5,grace@university.edu,2024-06-26 +113,Smart Plug,Electronics,19.99,4,henry@business.com,2024-06-27 +114, DESK lamp WIRELESS ,Furniture,69.99,1,ivan@email.com,2024-06-28 +115,Monitor Arm Dual,Furniture,179.99,1,jenny@work.org,2024-06-29 +116, PHONE charger ,Electronics,12.99,5, ,2024-06-30 +117,Keyboard Cleaner,Accessories,8.99,3,lena@mail.nl,2024-04-22 +118, desk TIDY caddy ,Furniture,15.99,2,marco@business.de,2024-13-01 +119,USB Hub Powered,Electronics,34.99,2,nina@university.edu, +120, WEBCAM stand ,Accessories,18.99,1,oliver@corp.com,2024-05-15 diff --git a/src/ingest.py b/src/ingest.py new file mode 100644 index 0000000..50aad21 --- /dev/null +++ b/src/ingest.py @@ -0,0 +1,66 @@ +import logging +from pathlib import Path +import os +import pandas as pd +from dotenv import load_dotenv +from azure.identity import DefaultAzureCredential +from azure.storage.blob import BlobServiceClient + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +load_dotenv(PROJECT_ROOT / ".env") +load_dotenv() + +FILES = ["messy_sales.csv", "messy_customers.csv"] +def download_inputs(data_dir: Path) -> None: + account_url = os.getenv("ACCOUNT_URL") + source_container = os.getenv("SOURCE_CONTAINER") + if not account_url or not source_container: + raise RuntimeError( + "Please set ACCOUNT_URL and SOURCE_CONTAINER environment variables before running." + ) + credential = DefaultAzureCredential() + service = BlobServiceClient(account_url=account_url, credential=credential) + container = service.get_container_client(source_container) + if not container.exists(): + logging.info(f"Container '{source_container}' not found.") + return + data_dir.mkdir(parents=True, exist_ok=True) + for name in FILES: + blob = container.get_blob_client(name) + with open(data_dir / name, "wb") as f: + f.write(blob.download_blob().readall()) + logging.info("Downloaded %s", name) + + +"""def load_inputs_local(data_dir: Path) -> None: + #Because of the Fallback i implented this function to load data locally + data_dir.mkdir(exist_ok=True) + sample_data_dir = Path(__file__).resolve().parent.parent / "sample_data" + + for name in FILES: + source_file = sample_data_dir / name + destination_file = data_dir / name + if source_file.exists(): + shutil.copy(source_file, destination_file) + logging.info("Successfully loaded %s from local sample_data", name) + else: + logging.error("File %s not found in sample_data!", name)""" + + +def upload_outputs(output_dir: Path, github_username: str) -> None: + """Task 7 (extra credit): Upload Parquet outputs to Azure and verify the round-trip.""" + container_name = f"week4-{github_username}" + + # EXTRA CREDIT — implement this after Tasks 2–6 are working. + # TODO: Create a BlobServiceClient using DefaultAzureCredential and ACCOUNT_URL. + # TODO: Get (or create) the container named container_name. + # TODO: Upload every .parquet file in output_dir to the container. + # TODO: Download customer_summary.parquet back and assert its row count matches the local file. + # TODO: Log the container name and number of files uploaded. + pass + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + LOCAL_DATA_DIR = Path(__file__).resolve().parent / "data" + download_inputs(LOCAL_DATA_DIR) diff --git a/src/pipeline.py b/src/pipeline.py index 1cd17a4..fe204cc 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -7,24 +7,31 @@ Replace every `raise NotImplementedError` below with a real implementation. """ - +# python -m src.pipeline +import os import logging from pathlib import Path +from src.ingest import download_inputs, upload_outputs +from src.clean import load_and_explore, clean_sales +from src.transform import join_customers +from src.report import build_reports, write_outputs + +GITHUB_USERNAME = "mareh-aboghanem" +DATA_DIR = Path("data") +# OUTPUT_DIR = Path("output") logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") logger = logging.getLogger(__name__) def get_config() -> dict: - """ - Return configuration read from environment variables. + api_key = os.getenv("API_KEY") + if not api_key: + raise RuntimeError("API_KEY environment is required but its value is missing.") + + output_dirctroy = os.getenv("OUTPUT_DIR", "output") - Required variable: API_KEY - Optional variable: OUTPUT_DIR (default "output") - - Raise RuntimeError with a clear message if a required variable is missing. - """ - raise NotImplementedError("Task 5: read API_KEY and OUTPUT_DIR from the environment") + return {"api_key": api_key, "output_dir": output_dirctroy} def fetch_data(api_key: str) -> list[dict]: @@ -34,7 +41,16 @@ def fetch_data(api_key: str) -> list[dict]: Return a list of at least one dict representing a record. In a real pipeline you would call requests.get(...) here. """ - raise NotImplementedError("Task 1: return at least one sample record") + mock_record = { + "transaction_id": 999, + "product_name": "Mock Test Item", + "category": "Testing", + "price": 99.99, + "quantity": 1, + "customer_email": "test_user@example.com", + "date": "2026-06-03", + } + return [mock_record] def save_results(records: list[dict], output_dir: Path) -> None: @@ -44,16 +60,27 @@ def save_results(records: list[dict], output_dir: Path) -> None: Create output_dir if it does not exist. Log the number of records written. """ - raise NotImplementedError("Task 1: write records to output_dir/results.txt") - + output_dir.mkdir(parents=True, exist_ok=True) + with open(output_dir / "results.txt", "w") as f: + for record in records: + f.write(f"{record}\n") + logging.info("Saved %d records to %s", len(records), output_dir / "results.txt") +# 5 def run() -> None: config = get_config() logger.info("starting pipeline") records = fetch_data(config["api_key"]) output_dir = Path(config["output_dir"]) save_results(records, output_dir) - logger.info("pipeline complete") + download_inputs(DATA_DIR) + sales_raw, customers_raw = load_and_explore(DATA_DIR) + sales_clean = clean_sales(sales_raw) + enriched = join_customers(sales_clean, customers_raw) + reports = build_reports(enriched) + write_outputs(reports, output_dir) + # upload_outputs(OUTPUT_DIR, GITHUB_USERNAME) + logging.info("Pipeline complete.") if __name__ == "__main__": diff --git a/src/report.py b/src/report.py new file mode 100644 index 0000000..29effc9 --- /dev/null +++ b/src/report.py @@ -0,0 +1,57 @@ +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import logging +from pathlib import Path +import pandas as pd + + +def build_reports(enriched: pd.DataFrame) -> dict[str, pd.DataFrame]: + week = enriched["date"].dt.isocalendar().week + enriched["week"] = week + weekly_revenue = enriched.groupby(["week", "region"], as_index=False).agg( + total_revenue=("revenue", "sum"), order_count=("transaction_id", "count") + ) + customer_summary = enriched.groupby("customer_email", as_index=False).agg( + customer_name=("customer_name", "first"), + region=("region", "first"), + loyalty_tier=("loyalty_tier", "first"), + total_spent=("revenue", "sum"), + avg_order=("revenue", "mean"), + order_count=("transaction_id", "count"), + ) + category_performance = enriched.groupby("category", as_index=False).agg( + total_revenue=("revenue", "sum"), order_count=("transaction_id", "count") + ) + loyalty_analysis = enriched.groupby("loyalty_tier", as_index=False).agg( + avg_spent=("revenue", "mean"), customer_count=("customer_email", "nunique") + ) + return { + "weekly_revenue": weekly_revenue, + "customer_summary": customer_summary, + "category_performance": category_performance, + "loyalty_analysis": loyalty_analysis, + } + + +def write_outputs(reports: dict[str, pd.DataFrame], output_dir: Path) -> None: + """Task 6: Write report tables to CSV/Parquet and save a bar chart.""" + output_dir.mkdir(exist_ok=True) + reports["weekly_revenue"].to_csv(output_dir / "weekly_revenue.csv", index=False) + reports["customer_summary"].to_parquet( + output_dir / "customer_summary.parquet", index=False + ) + cat_df = reports["category_performance"].sort_values( + by="total_revenue", ascending=False + ) + cat_df.to_csv(output_dir / "category_performance.csv", index=False) + plt.figure(figsize=(10, 6)) + plt.bar( + cat_df["category"], cat_df["total_revenue"], color="skyblue", edgecolor="black" + ) + plt.title("Total Revenue by Category") + plt.xlabel("Category") + plt.ylabel("Total Revenue") + plt.xticks(rotation=45) + plt.savefig(output_dir / "category_revenue.png", bbox_inches="tight") + plt.close() diff --git a/src/transform.py b/src/transform.py new file mode 100644 index 0000000..abdb53b --- /dev/null +++ b/src/transform.py @@ -0,0 +1,14 @@ +import logging +import pandas as pd + + +def join_customers(sales: pd.DataFrame, customers: pd.DataFrame) -> pd.DataFrame: + """Task 4: Normalize join keys, merge, and add a derived boolean flag.""" + customers["customer_email"] = customers["customer_email"].str.lower().str.strip() + sales["customer_email"] = sales["customer_email"].str.lower().str.strip() + merged = sales.merge(customers, on="customer_email", how="inner") + merged["revenue"] = merged["price"] * merged["quantity"] + merged["is_high_value"] = merged["revenue"] >= 150 + # TODO: (Optional hands-on) Try a left join instead and inspect rows where customer_name is NaN. + logging.info("Joining complete. Rows in merged DataFrame: %d", len(merged)) + return merged diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 73029e3..0e2e74e 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1,9 +1,8 @@ """Tests for the Week 5 pipeline.""" - +# python -m pytest tests/ -v import pytest - -from src.pipeline import fetch_data, get_config, save_results - +import pandas as pd +from src.pipeline import fetch_data, get_config, save_results, clean_sales class TestGetConfig: def test_returns_api_key_from_env(self, monkeypatch): @@ -59,3 +58,31 @@ def test_file_contains_records(self, tmp_path): save_results([{"id": 1}, {"id": 2}], tmp_path) content = (tmp_path / "results.txt").read_text() assert len(content.strip().splitlines()) >= 2 + +def test_clean_sales_strips_whitespace(): + mock_sales = pd.DataFrame( + { + "transaction_id": [1], + "product_name": [" Iphone green "], + "customer_email": ["TEST@example.com"], + "price": [999.99], + "quantity": [1], + "date": ["2026-06-03"], + } + ) + cleaned = clean_sales(mock_sales) + assert cleaned["product_name"].iloc[0] == "Iphone Green" + +def test_clean_sales_handles_empty(): + mock_sales = pd.DataFrame( + { + "transaction_id": [1], + "product_name": [""], + "customer_email": ["TEST@example.com"], + "price": [999.99], + "quantity": [1], + "date": ["2026-06-03"], + } + ) + cleaned = clean_sales(mock_sales) + assert cleaned["product_name"].iloc[0] == "" From 9333afd44b59de7b96c1c40c1d2458aac2dd7ebb Mon Sep 17 00:00:00 2001 From: Mareh Date: Wed, 3 Jun 2026 23:42:28 +0200 Subject: [PATCH 2/7] chore: trigger github actions workflow --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 143fc8e..0d5e3e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,5 @@ # Task 6: Build a CI workflow that runs on pull requests and pushes to main. -# -# See the assignment chapter for the required steps and commands. -# Fill in the TODO values below. - +# Task 6: Build a CI workflow (Triggering pipeline run) name: CI on: From ec8f3e5275e0cae5a38e2f5e2820d5b8bd5a0f12 Mon Sep 17 00:00:00 2001 From: Mareh Date: Wed, 3 Jun 2026 23:45:49 +0200 Subject: [PATCH 3/7] style: remove unused logging import from report --- src/report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/report.py b/src/report.py index 29effc9..9a1733c 100644 --- a/src/report.py +++ b/src/report.py @@ -1,7 +1,6 @@ import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt -import logging from pathlib import Path import pandas as pd From 9db4f54ff8bec4daa2d45bef2a2e60decae60bf3 Mon Sep 17 00:00:00 2001 From: Mareh Date: Wed, 3 Jun 2026 23:49:03 +0200 Subject: [PATCH 4/7] style: clear all unused imports and variables for ruff --- src/ingest.py | 3 +-- src/pipeline.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ingest.py b/src/ingest.py index 50aad21..9e60a84 100644 --- a/src/ingest.py +++ b/src/ingest.py @@ -1,7 +1,6 @@ import logging from pathlib import Path import os -import pandas as pd from dotenv import load_dotenv from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient @@ -50,7 +49,7 @@ def download_inputs(data_dir: Path) -> None: def upload_outputs(output_dir: Path, github_username: str) -> None: """Task 7 (extra credit): Upload Parquet outputs to Azure and verify the round-trip.""" - container_name = f"week4-{github_username}" + #container_name = f"week4-{github_username}" # EXTRA CREDIT — implement this after Tasks 2–6 are working. # TODO: Create a BlobServiceClient using DefaultAzureCredential and ACCOUNT_URL. diff --git a/src/pipeline.py b/src/pipeline.py index fe204cc..a9fff15 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -11,7 +11,7 @@ import os import logging from pathlib import Path -from src.ingest import download_inputs, upload_outputs +from src.ingest import download_inputs from src.clean import load_and_explore, clean_sales from src.transform import join_customers from src.report import build_reports, write_outputs From 83559d32f8b5f5bd81db26913273adee97beadc9 Mon Sep 17 00:00:00 2001 From: Mareh Date: Wed, 3 Jun 2026 23:56:14 +0200 Subject: [PATCH 5/7] style: fix ruff formatting --- src/ingest.py | 7 +++++-- src/pipeline.py | 4 +++- src/report.py | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ingest.py b/src/ingest.py index 9e60a84..2b250b5 100644 --- a/src/ingest.py +++ b/src/ingest.py @@ -11,6 +11,8 @@ load_dotenv() FILES = ["messy_sales.csv", "messy_customers.csv"] + + def download_inputs(data_dir: Path) -> None: account_url = os.getenv("ACCOUNT_URL") source_container = os.getenv("SOURCE_CONTAINER") @@ -23,7 +25,7 @@ def download_inputs(data_dir: Path) -> None: container = service.get_container_client(source_container) if not container.exists(): logging.info(f"Container '{source_container}' not found.") - return + return data_dir.mkdir(parents=True, exist_ok=True) for name in FILES: blob = container.get_blob_client(name) @@ -49,7 +51,7 @@ def download_inputs(data_dir: Path) -> None: def upload_outputs(output_dir: Path, github_username: str) -> None: """Task 7 (extra credit): Upload Parquet outputs to Azure and verify the round-trip.""" - #container_name = f"week4-{github_username}" + # container_name = f"week4-{github_username}" # EXTRA CREDIT — implement this after Tasks 2–6 are working. # TODO: Create a BlobServiceClient using DefaultAzureCredential and ACCOUNT_URL. @@ -59,6 +61,7 @@ def upload_outputs(output_dir: Path, github_username: str) -> None: # TODO: Log the container name and number of files uploaded. pass + if __name__ == "__main__": logging.basicConfig(level=logging.INFO) LOCAL_DATA_DIR = Path(__file__).resolve().parent / "data" diff --git a/src/pipeline.py b/src/pipeline.py index a9fff15..2d62cff 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -7,6 +7,7 @@ Replace every `raise NotImplementedError` below with a real implementation. """ + # python -m src.pipeline import os import logging @@ -28,7 +29,7 @@ def get_config() -> dict: api_key = os.getenv("API_KEY") if not api_key: raise RuntimeError("API_KEY environment is required but its value is missing.") - + output_dirctroy = os.getenv("OUTPUT_DIR", "output") return {"api_key": api_key, "output_dir": output_dirctroy} @@ -66,6 +67,7 @@ def save_results(records: list[dict], output_dir: Path) -> None: f.write(f"{record}\n") logging.info("Saved %d records to %s", len(records), output_dir / "results.txt") + # 5 def run() -> None: config = get_config() diff --git a/src/report.py b/src/report.py index 9a1733c..646260c 100644 --- a/src/report.py +++ b/src/report.py @@ -1,4 +1,5 @@ import matplotlib + matplotlib.use("Agg") import matplotlib.pyplot as plt from pathlib import Path From cd3ddba2795aedc5641c2e2331f1e71bcf496963 Mon Sep 17 00:00:00 2001 From: Mareh Date: Thu, 4 Jun 2026 00:10:00 +0200 Subject: [PATCH 6/7] docs: add azure acr push verification screenshot --- assets/acr_push_week5.png | Bin 0 -> 37751 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/acr_push_week5.png diff --git a/assets/acr_push_week5.png b/assets/acr_push_week5.png new file mode 100644 index 0000000000000000000000000000000000000000..dd04e4f3da0214d988be1666d0e747f184d97940 GIT binary patch literal 37751 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 Date: Thu, 4 Jun 2026 09:58:27 +0200 Subject: [PATCH 7/7] ci: skip Azure/ACR steps on fork PRs GitHub withholds AZURE_CREDENTIALS from pull_request runs from forks, causing azure/login to fail. The if-guard skips the Azure/ACR steps on fork PRs so CI stays green; the central Grade ACR push workflow on main handles the actual push. --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9982e33..acd54a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,13 +37,20 @@ jobs: run: pytest -q - name: Build image run: docker build -t mareh-aboghanem-pipeline:${{ github.sha }} . + # The next three steps need the AZURE_CREDENTIALS secret, which GitHub + # withholds on pull_request runs from forks for security. The `if:` guard + # skips them on fork PRs so CI stays green; the central `Grade ACR push` + # workflow on main handles the actual push from base-repo context. - name: Azure login + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: ACR login + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false run: az acr login --name hyfregistry - name: Push image + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false run: | docker tag mareh-aboghanem-pipeline:${{ github.sha }} hyfregistry.azurecr.io/mareh-aboghanem-pipeline:${{ github.sha }} docker push hyfregistry.azurecr.io/mareh-aboghanem-pipeline:${{ github.sha }}