Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions task-1/output/clean_sales.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
transaction_id,product_name,category,price,quantity,customer_email,date,revenue,vat
1,Laptop Pro,Electronics,999.99,2,alice@example.com,2024-03-15,1999.98,420.0
2,Wireless Mouse,Electronics,29.99,5,bob@company.com,2024-03-15,149.95,31.49
3,Usb Cable,Electronics,4.99,10,,2024-03-16,49.9,10.48
4,Office Chair,Furniture,349.5,1,charlie@work.org,2024-03-16,349.5,73.39
5,Standing Desk,Furniture,599.0,1,charlie@work.org,not_a_date,599.0,125.79
9,Webcam Hd,Electronics,54.99,1,,2024-03-18,54.99,11.55
10,Desk Lamp,Furniture,34.99,4,grace@university.edu,2024-03-19,139.96,29.39
11,Noise Cancelling Headphones,Electronics,199.99,1,alice@example.com,2024-03-19,199.99,42.0
12,Cable Management Kit,Furniture,15.99,6,henry@business.com,2024-03-20,95.94,20.15
13,Ergonomic Mouse Pad,Furniture,24.99,3,ivan@email.com,2024-03-20,74.97,15.74
14,Laptop Stand,Furniture,45.99,2,jenny@work.org,2024-03-21,91.98,19.32
15,Bluetooth Speaker,Unknown,39.99,1,karl@startup.io,2024-03-21,39.99,8.4
Binary file added task-1/src/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file added task-1/src/__pycache__/config.cpython-311.pyc
Binary file not shown.
Binary file added task-1/src/__pycache__/models.cpython-311.pyc
Binary file not shown.
Binary file added task-1/src/__pycache__/pipeline.cpython-311.pyc
Binary file not shown.
Binary file added task-1/src/__pycache__/transforms.cpython-311.pyc
Binary file not shown.
14 changes: 9 additions & 5 deletions task-1/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@

def _required(name: str) -> str:
"""Read an env var; fail loudly if missing."""
# TODO 2: Read os.environ[name]; if not set, raise ValueError with a
# message that names the missing variable AND points at .env.example.
raise NotImplementedError("Implement _required: see TODO 2 in config.py")
try:
return os.environ[name]
except KeyError as error:
raise ValueError(
f"Missing required environment variable: {name}. "
"See .env.example for expected variable names."
) from error


# TODO 3: Replace the placeholder lines below by calling _required(...) for
# each variable. INPUT_PATH and OUTPUT_PATH must be importable from this
# module by the rest of the pipeline as a relative import
# (`from .config import INPUT_PATH, ...`), since the pipeline runs as
# `python -m src.pipeline`.
INPUT_PATH: str = "" # TODO: _required("INPUT_PATH")
OUTPUT_PATH: str = "" # TODO: _required("OUTPUT_PATH")
INPUT_PATH: str = _required("INPUT_PATH")
OUTPUT_PATH: str = _required("OUTPUT_PATH")
35 changes: 16 additions & 19 deletions task-1/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,21 @@
"""
from dataclasses import dataclass


# TODO 1: Define a @dataclass called Transaction with these fields:
# transaction_id: int
# product_name: str
# category: str
# price: float
# quantity: int
# customer_email: str
# date: str
# revenue: float = 0.0
# vat: float = 0.0
#
# TODO 2: Add __post_init__ that raises ValueError when:
# - self.price < 0 (with a message naming the bad value)
# - not self.product_name.strip() (empty / whitespace-only product name)


# Replace this stub with your dataclass:
@dataclass
class Transaction:
transaction_id: int # TODO: replace this stub with the full field list above
transaction_id: int
product_name: str
category: str
price: float
quantity: int
customer_email: str
date: str
revenue: float = 0.0
vat: float = 0.0

def __post_init__(self) -> None:
if self.price < 0:
raise ValueError(f"price must be >= 0, got {self.price}")

if not self.product_name.strip():
raise ValueError("product_name must not be empty")
14 changes: 10 additions & 4 deletions task-1/src/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@

def read_csv(path: str) -> list[dict]:
"""Read a CSV file into a list of dicts. I/O only — no business rules."""
# TODO: implement using csv.DictReader.
raise NotImplementedError
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
return list(reader)


def write_csv(rows: list[dict], path: str) -> None:
"""Write a list of dicts to CSV. I/O only — no business rules."""
# TODO: implement using csv.DictWriter.
raise NotImplementedError
if not rows:
return

with open(path, "w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(file, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)


def run() -> None:
Expand Down
38 changes: 30 additions & 8 deletions task-1/src/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ def remove_invalid(rows: list[dict]) -> list[dict]:

Empty here means missing, "", or whitespace-only.
"""
# TODO: implement. Return a new list, do not mutate `rows`.
raise NotImplementedError
return [
{**row}
for row in rows
if row.get("product_name", "").strip() != ""
and float(row.get("price", 0)) >= 0
]


def clean_fields(rows: list[dict]) -> list[dict]:
Expand All @@ -31,20 +35,38 @@ def clean_fields(rows: list[dict]) -> list[dict]:

Return a new list. Do not mutate the input rows.
"""
# TODO: implement.
raise NotImplementedError
return [
{
**row,
"product_name": row["product_name"].strip().title(),
"customer_email": row["customer_email"].strip().lower(),
"category": row.get("category", "").strip() or "Unknown",
}
for row in rows
]


def calculate_revenue(rows: list[dict], vat_rate: float = 0.21) -> list[dict]:
"""Add 'revenue' (price * quantity) and 'vat' (revenue * vat_rate) fields.

Round both to 2 decimal places. Coerce price/quantity from string if needed.
"""
# TODO: implement.
raise NotImplementedError
return [
{
**row,
"price": float(row["price"]),
"quantity": int(row["quantity"]),
"revenue": round(float(row["price"]) * int(row["quantity"]), 2),
"vat": round(float(row["price"]) * int(row["quantity"]) * vat_rate, 2),
}
for row in rows
]


def filter_zero_quantity(rows: list[dict]) -> list[dict]:
"""Remove rows where quantity is 0."""
# TODO: implement.
raise NotImplementedError
return [
{**row}
for row in rows
if int(row["quantity"]) != 0
]
Binary file added task-1/tests/__pycache__/__init__.cpython-311.pyc
Binary file not shown.
Binary file not shown.
80 changes: 66 additions & 14 deletions task-1/tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,82 @@
from src.transforms import (
calculate_revenue,
clean_fields,
filter_zero_quantity,
remove_invalid,
)


def test_remove_invalid_drops_empty_names():
# TODO: feed in 3 rows (one valid, one empty product_name, one
# whitespace-only product_name); assert only the valid one survives.
raise NotImplementedError
rows = [
{"product_name": "Laptop", "price": 100.0},
{"product_name": "", "price": 50.0},
{"product_name": " ", "price": 25.0},
]

result = remove_invalid(rows)

def test_clean_fields_normalizes_names():
# TODO: feed a row with messy product_name and uppercase email; assert
# the output has stripped + title-cased name and lowercase email.
raise NotImplementedError
assert len(result) == 1
assert result[0]["product_name"] == "Laptop"

@pytest.mark.parametrize(
"raw_name, raw_email, expected_name, expected_email",
[
(" laptop stand ", " USER@EXAMPLE.COM ", "Laptop Stand", "user@example.com"),
("MOUSE", "CUSTOMER@MAIL.COM", "Mouse", "customer@mail.com"),
],
)

def test_clean_fields_normalizes_names(
raw_name: str,
raw_email: str,
expected_name: str,
expected_email: str,
):
rows = [
{
"product_name": raw_name,
"customer_email": raw_email,
"category": "accessories",
}
]

result = clean_fields(rows)

assert result[0]["product_name"] == expected_name
assert result[0]["customer_email"] == expected_email


def test_calculate_revenue_adds_fields():
# TODO: feed a row with price=100, quantity=3; assert output has
# revenue=300.0 and vat=63.0 (default VAT rate is 0.21).
raise NotImplementedError
rows = [
{
"product_name": "Laptop",
"price": 100,
"quantity": 3,
}
]

result = calculate_revenue(rows)

assert result[0]["revenue"] == 300.0
assert result[0]["vat"] == 63.0


def test_no_mutation():
# TODO: feed in a list, run any transform on it, assert the original
# list is unchanged. This is the most important test in the file.
raise NotImplementedError
rows = [
{
"product_name": " laptop stand ",
"customer_email": " USER@EXAMPLE.COM ",
"category": "accessories",
}
]

original = [
{
"product_name": " laptop stand ",
"customer_email": " USER@EXAMPLE.COM ",
"category": "accessories",
}
]

clean_fields(rows)

assert rows == original
100 changes: 96 additions & 4 deletions task-2/AI_DEBUG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,119 @@ Aim for 100-200 words per section. Bullet points are fine.
What went wrong? Paste the traceback or the wrong-output sample. Include the file and the line you were running when it broke.

```
(paste here)
File: task-1/tests/test_transforms.py
Command I ran:
python -m pytest tests/ -v
Traceback / error output:
(.venv) pavel@Pavels-MacBook-Air task-1 % python -m pytest tests/ -v
============================================ test session starts =============================================
platform darwin -- Python 3.11.15, pytest-9.0.3, pluggy-1.6.0 -- /Users/pavel/Desktop/data _learn/c55-data-week-2/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/pavel/Desktop/data _learn/c55-data-week-2/task-1
collected 0 items / 1 error
=================================================== ERRORS ===================================================
_________________________________ ERROR collecting tests/test_transforms.py __________________________________
In test_clean_fields_normalizes_names: function uses no argument 'raw_name'
========================================== short test summary info ===========================================
ERROR tests/test_transforms.py - Failed: In test_clean_fields_normalizes_names: function uses no argument 'raw_name'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
============================================== 1 error in 0.06s ==============================================
I was trying to use @pytest.mark.parametrize for test_clean_fields_normalizes_names, but pytest failed before running any tests. The issue happened during test collection, not inside the transform function itself.
```

## The Prompt

What did you ask the AI? Paste the actual prompt verbatim. (Include the code or stack trace you pasted alongside it; do NOT include any real `.env` values, API keys, or PII — replace those with `<REDACTED>`.)

```
(paste here)
I am writing pytest tests for pure transform functions.
I get this error:
In test_clean_fields_normalizes_names: function uses no argument 'raw_name'
Here is my test code:

import pytest

from src.transforms import (
calculate_revenue,
clean_fields,
remove_invalid,
)

@pytest.mark.parametrize(
"raw_name, raw_email, expected_name, expected_email",
[
(" laptop stand ", " USER@EXAMPLE.COM ", "Laptop Stand", "user@example.com"),
("MOUSE", "CUSTOMER@MAIL.COM", "Mouse", "customer@mail.com"),
],
)
def test_clean_fields_normalizes_names():
rows = [
{
"product_name": " laptop stand ",
"customer_email": " USER@EXAMPLE.COM ",
"category": "accessories",
}
]

result = clean_fields(rows)

assert result[0]["product_name"] == "Laptop Stand"
assert result[0]["customer_email"] == "user@example.com"

Why does pytest say that the function uses no argument 'raw_name' and how should I fix it?
```

## The Solution

What did the AI suggest? Did it work on the first try? Did you have to follow up? Paste the final code change (a small diff is best).

```
(paste here)
The AI explained that `@pytest.mark.parametrize` passes values into the test function as arguments. I had listed four argument names in the decorator:
```python
"raw_name, raw_email, expected_name, expected_email"

but my test function did not accept those arguments:

def test_clean_fields_normalizes_names():

So pytest could not inject the parametrized values. I decided not to remove parametrize, because this chapter specifically introduced it as a useful pytest pattern. Instead, I fixed the test by adding the same four arguments to the function signature and using them inside the test data and assertions.

@pytest.mark.parametrize(
"raw_name, raw_email, expected_name, expected_email",
[
(" laptop stand ", " USER@EXAMPLE.COM ", "Laptop Stand", "user@example.com"),
("MOUSE", "CUSTOMER@MAIL.COM", "Mouse", "customer@mail.com"),
],
)
-def test_clean_fields_normalizes_names():
+def test_clean_fields_normalizes_names(
+ raw_name: str,
+ raw_email: str,
+ expected_name: str,
+ expected_email: str,
+):
rows = [
{
- "product_name": " laptop stand ",
- "customer_email": " USER@EXAMPLE.COM ",
+ "product_name": raw_name,
+ "customer_email": raw_email,
"category": "accessories",
}
]

result = clean_fields(rows)

- assert result[0]["product_name"] == "Laptop Stand"
- assert result[0]["customer_email"] == "user@example.com"
+ assert result[0]["product_name"] == expected_name
+ assert result[0]["customer_email"] == expected_email

This worked after the function signature matched the parameter names from the decorator.
```

## Reflection

Did you understand *why* the original code was broken before the AI told you? If not, what was the gap in your mental model? If you understood it before asking, why did you still ask the AI — speed, second opinion, or something else?

(write here, ~100 words)
I did not fully understand the error before asking the AI. I thought @pytest.mark.parametrize was mostly a way to attach several examples to one test, but I missed the exact mechanism: pytest calls the test function once per example and injects the values by matching the names from the decorator to the function arguments. My decorator declared raw_name, raw_email, expected_name, and expected_email, but the function accepted no arguments, so pytest failed during collection. The important lesson is that decorators are not comments; they change how the function is called. I kept parametrize because it made the test stronger and matched the chapter’s pytest pattern.
Binary file added task-3/assets/azure_blob_week2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions task-3/assets/blob_url.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://sthyfstudentsdemo.blob.core.windows.net/week2-pavel-tisner/clean_sales.csv