diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51fcbfd..f42988c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ name: CI on: push: - branches: ["TODO-replace-with-main"] + branches: ["main"] pull_request: jobs: @@ -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 }} \ No newline at end of file diff --git a/AI_ASSIST.md b/AI_ASSIST.md index 70463b0..9798661 100644 --- a/AI_ASSIST.md +++ b/AI_ASSIST.md @@ -8,18 +8,25 @@ -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 +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 -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. diff --git a/Dockerfile b/Dockerfile index d665b11..fd3af10 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/assets/acr_push_week5.png b/assets/acr_push_week5.png new file mode 100644 index 0000000..3e1254d Binary files /dev/null and b/assets/acr_push_week5.png differ diff --git a/requirements.txt b/requirements.txt index 42299cf..d8b45b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,5 @@ # ruff== # # Add your pinned dependencies below: +pytest==7.4.0 +ruff==0.0.241 \ No newline at end of file diff --git a/src/pipeline.py b/src/pipeline.py index 1cd17a4..90ec20a 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -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__) @@ -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]: @@ -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: """ @@ -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"]) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 73029e3..73ed390 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -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.""" save_results([{"id": 1}, {"id": 2}], tmp_path) content = (tmp_path / "results.txt").read_text() assert len(content.strip().splitlines()) >= 2