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
31 changes: 26 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ name: CI

on:
push:
branches: ["TODO-replace-with-main"]
branches: ["main"]
pull_request:

jobs:
Expand All @@ -29,10 +29,31 @@ jobs:
- name: Install dependencies
run: pip install -r requirements.txt
- name: Lint
run: echo "TODO implement this step"
run: ruff check src tests
- name: Format
run: echo "TODO implement this step"
run: ruff format --check src tests
- name: Test
run: echo "TODO implement this step"
env:
API_KEY: test-key-123
run: pytest tests -q
- name: Build image
run: echo "TODO implement this step"
run: docker build -t halyna1995-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 halyna1995-pipeline:${{ github.sha }} hyfregistry.azurecr.io/halyna1995-pipeline:${{ github.sha }}
docker push hyfregistry.azurecr.io/halyna1995-pipeline:${{ github.sha }}
13 changes: 10 additions & 3 deletions AI_ASSIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@

<!-- Paste the exact prompt you gave to an LLM (ChatGPT, Claude, Copilot, etc.). -->

TODO: paste your prompt here.
Explain what this Docker or GitHub Actions error means and what I should check. Please explain the reasoning step by step.

## The code or suggestion it returned

<!-- Paste the code or key suggestion the LLM returned. -->
The assistant suggested checking the following points:

- whether requirements.txt contains the required pinned dependencies;
- whether the Dockerfile copies requirements.txt before copying src/;
- whether the pipeline is started with python -m src.pipeline;
- whether the GitHub Actions workflow includes linting, formatting, tests, and Docker build;
- whether Azure credentials are stored as GitHub Secrets instead of being committed to the repository.

```python
# TODO: paste the AI-generated code here

```

## What I changed after reviewing it

<!-- Describe what you accepted, rejected, or modified, and why. -->

TODO: describe your review and any changes you made.
I reviewed the suggestions and applied only the parts that matched the assignment requirements. I verified the result locally by running formatting checks, linting, tests, and Docker build commands. The AI assistance was used for explanation and debugging support.
12 changes: 5 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@
#
# 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 --no-cache-dir -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"]
Binary file added assets/acr_push_week5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
# ruff==
#
# Add your pinned dependencies below:
pytest==7.4.0
ruff==0.0.241
27 changes: 24 additions & 3 deletions src/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import logging
from pathlib import Path
import os
import json

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logger = logging.getLogger(__name__)
Expand All @@ -24,7 +26,16 @@ def get_config() -> dict:

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")
api_key = os.getenv("API_KEY")
output_dir = os.getenv("OUTPUT_DIR")

if not api_key:
raise RuntimeError("Missing required environment variable: API_KEY")

if not output_dir:
output_dir = "output"

return {"api_key": api_key, "output_dir": output_dir}


def fetch_data(api_key: str) -> list[dict]:
Expand All @@ -34,8 +45,11 @@ 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")
if not api_key:
raise RuntimeError("API key must not be empty")

return [{"id": 1, "name": "Halyna", "status": "active"},
{"id": 2, "name": "Alice", "status": "inactive"}]

def save_results(records: list[dict], output_dir: Path) -> None:
"""
Expand All @@ -44,10 +58,17 @@ 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)
results_file = output_dir / "results.txt"
with open(results_file, "w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record) + "\n")

logger.info("Written %s records to %s/results.txt", len(records), output_dir)


def run() -> None:
"""Run the data pipeline."""
config = get_config()
logger.info("starting pipeline")
records = fetch_data(config["api_key"])
Expand Down
13 changes: 13 additions & 0 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,69 @@


class TestGetConfig:
"""Tests for get_config()."""
def test_returns_api_key_from_env(self, monkeypatch):
"""Test that get_config() reads API_KEY from the environment."""
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.delenv("OUTPUT_DIR", raising=False)
config = get_config()
assert config["api_key"] == "test-key-123"

def test_uses_default_output_dir(self, monkeypatch):
"""Test that get_config() uses 'output' as the default output directory."""
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.delenv("OUTPUT_DIR", raising=False)
config = get_config()
assert config["output_dir"] == "output"

def test_reads_custom_output_dir(self, monkeypatch):
"""Test that get_config() reads OUTPUT_DIR from the environment."""
monkeypatch.setenv("API_KEY", "test-key-123")
monkeypatch.setenv("OUTPUT_DIR", "/tmp/myout")
config = get_config()
assert config["output_dir"] == "/tmp/myout"

def test_raises_when_api_key_missing(self, monkeypatch):
"""Test that get_config() raises RuntimeError when API_KEY is missing."""
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises((RuntimeError, KeyError, SystemExit)):
get_config()


class TestFetchData:
"""Tests for fetch_data()."""
def test_returns_list(self):
"""Test that fetch_data() returns a list."""
records = fetch_data("any-key")
assert isinstance(records, list)

def test_returns_at_least_one_record(self):
"""Test that fetch_data() returns at least one record."""
records = fetch_data("any-key")
assert len(records) >= 1

def test_records_are_dicts(self):
"""Test that each record returned by fetch_data() is a dict."""
records = fetch_data("any-key")
assert all(isinstance(r, dict) for r in records)


class TestSaveResults:
"""Tests for save_results()."""
def test_creates_output_dir(self, tmp_path):
"""Test that save_results() creates the output directory if it doesn't exist."""
output_dir = tmp_path / "new_dir"
save_results([{"id": 1}], output_dir)
assert output_dir.exists()

def test_writes_results_file(self, tmp_path):
"""Test that save_results() writes results.txt in the output directory."""
save_results([{"id": 1}, {"id": 2}], tmp_path)
results_file = tmp_path / "results.txt"
assert results_file.exists()

def test_file_contains_records(self, tmp_path):
"""Test that save_results() writes the correct number of records to results.txt."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is causing CI to fail with E501 because it is longer than 88 characters. Wrap the docstring onto multiple lines or shorten it so ruff check src tests passes.

save_results([{"id": 1}, {"id": 2}], tmp_path)
content = (tmp_path / "results.txt").read_text()
assert len(content.strip().splitlines()) >= 2
Loading