From 844abb748224e6e930d201605a744603549e2352 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:32:19 +0800 Subject: [PATCH 1/5] Create a DataFrame from a dictionary and export it to a CSV file Task: MIL-004#1 Task: MIL-004#2 --- src/weather/constants.py | 26 ++++++++++++++++++++++++-- src/weather/export.py | 39 +++++++++++++++++++++++++++++++++++++++ tests/test_constants.py | 15 +++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/weather/export.py diff --git a/src/weather/constants.py b/src/weather/constants.py index f28690a..2ed80ad 100644 --- a/src/weather/constants.py +++ b/src/weather/constants.py @@ -3,8 +3,9 @@ # # Every value that the day-25 lectures fix lives here, so that no other module # contains a magic string or number: the name of the data file, the names of -# its columns, the factors of the Fahrenheit conversion and the name of the -# exported file. +# its columns, the factors of the Fahrenheit conversion, the name of the +# exported file, the table that is created from a dictionary and the message +# for a missing optional dependency. """Constants of the weather data project, as fixed by the day-25 lectures.""" @@ -46,3 +47,24 @@ FAHRENHEIT_DENOMINATOR: Final[int] = 5 ## @brief Offset of the Celsius to Fahrenheit conversion: `temp * 9 / 5 + 32`. FAHRENHEIT_OFFSET: Final[int] = 32 + +## @brief Name of the column that holds the student names of the lecture's table. +COLUMN_STUDENTS: Final[str] = "students" + +## @brief Name of the column that holds the scores of the lecture's table. +COLUMN_SCORES: Final[str] = "scores" + +## @brief Student names of the table that the lecture creates from a dictionary. +STUDENTS: Final[tuple[str, ...]] = ("Amy", "James", "Angela") + +## @brief Scores of the students, in the order of `STUDENTS`. +SCORES: Final[tuple[int, ...]] = (76, 56, 65) + +## @brief Name of the day that the main flow selects and converts. +MONDAY: Final[str] = "Monday" + +## @brief Message printed when `pandas`, an optional extra, is not installed. +PANDAS_MISSING_MESSAGE: Final[str] = ( + "pandas is not installed; the pandas part needs the extra: " + 'pip install "weather[pandas]"' +) diff --git a/src/weather/export.py b/src/weather/export.py new file mode 100644 index 0000000..2b60ed1 --- /dev/null +++ b/src/weather/export.py @@ -0,0 +1,39 @@ +## @file export.py +# @brief Creates a `DataFrame` from a dictionary and exports it to a CSV file. +# +# This is the end of the second lecture: the table of students and scores is +# built from a dictionary and written with `DataFrame.to_csv`. Nothing is +# written unless `export_frame` is called with a path or its default name. + +"""Creates a DataFrame from a dictionary and exports it to a CSV file.""" + +from pathlib import Path + +import pandas + +from weather.constants import ( + COLUMN_SCORES, + COLUMN_STUDENTS, + EXPORT_FILE_NAME, + SCORES, + STUDENTS, +) + + +def create_scores_frame() -> pandas.DataFrame: + """! @brief Create the table of students and scores from a dictionary. + + @return A table with the columns `students` and `scores`, one row per student. + """ + data_dict = {COLUMN_STUDENTS: list(STUDENTS), COLUMN_SCORES: list(SCORES)} + return pandas.DataFrame(data_dict) + + +def export_frame(frame: pandas.DataFrame, path: Path | str = EXPORT_FILE_NAME) -> None: + """! @brief Write a table to a CSV file, the row index included. + + @param frame The table to write. + @param path Path of the file to write; `new_data.csv` in the working folder + by default. + """ + frame.to_csv(path) diff --git a/tests/test_constants.py b/tests/test_constants.py index c9872de..c8cff38 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -34,3 +34,18 @@ def test_fahrenheit_factors_give_the_lecture_formula() -> None: def test_exported_file_has_the_name_of_the_lecture() -> None: assert constants.EXPORT_FILE_NAME == "new_data.csv" + + +def test_scores_table_has_the_columns_and_values_of_the_lecture() -> None: + assert constants.COLUMN_STUDENTS == "students" + assert constants.COLUMN_SCORES == "scores" + assert constants.STUDENTS == ("Amy", "James", "Angela") + assert constants.SCORES == (76, 56, 65) + + +def test_main_flow_uses_monday() -> None: + assert constants.MONDAY == "Monday" + + +def test_missing_pandas_message_names_the_extra() -> None: + assert 'pip install "weather[pandas]"' in constants.PANDAS_MISSING_MESSAGE From d823e246169d1f88669f4f0b3c6a0d13bfd9be6c Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:32:21 +0800 Subject: [PATCH 2/5] Add the main flow that prints the results of the lectures Task: MIL-004#3 --- src/weather/__main__.py | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/weather/__main__.py diff --git a/src/weather/__main__.py b/src/weather/__main__.py new file mode 100644 index 0000000..37aaf72 --- /dev/null +++ b/src/weather/__main__.py @@ -0,0 +1,108 @@ +## @file __main__.py +# @brief Command line of the weather data project: `python -m weather`. +# +# Prints the results of both lectures in the order of the lectures. The csv +# part needs no third-party package; the pandas part needs the `pandas` extra. +# A file is written only when `--export PATH` is given. + +"""Prints the results of the weather lectures; run with `python -m weather`.""" + +import argparse +import sys +from collections.abc import Sequence +from pathlib import Path + +from weather.constants import MONDAY, PANDAS_MISSING_MESSAGE +from weather.csv_reader import read_rows, read_temperatures + +## @brief Value that `main` returns when the run succeeded. +EXIT_OK = 0 + +## @brief Value that `main` returns when the optional pandas extra is missing. +EXIT_PANDAS_MISSING = 1 + + +def _build_parser() -> argparse.ArgumentParser: + """! @brief Build the command line parser with its one optional flag. + + @return The parser of `python -m weather`. + """ + parser = argparse.ArgumentParser( + prog="weather", + description="Print the results of the weather data lectures.", + ) + parser.add_argument( + "--export", + metavar="PATH", + type=Path, + default=None, + help="also write the scores table to this CSV file", + ) + return parser + + +def _print_csv_part() -> None: + """! @brief Print the rows and temperatures read with the csv library.""" + print("Rows (csv library):") + for row in read_rows(): + print(row) + print(f"Temperatures (csv library): {read_temperatures()}") + + +def _print_pandas_part(export_path: Path | None) -> None: + """! @brief Print the pandas results and export the scores when asked. + + @param export_path File to write the scores table to; `None` writes nothing. + @throws ImportError When `pandas` is not installed. + """ + # Imported here so that the csv part runs without the optional extra. + from weather.analysis import ( + average_temp, + average_temp_mean, + day_in_fahrenheit, + hottest_row, + max_temp, + read_data, + row_for_day, + to_data_dict, + to_temp_list, + ) + from weather.export import create_scores_frame, export_frame + + data = read_data() + print(f"Data frame (pandas):\n{data}") + print(f"Data dict: {to_data_dict(data)}") + print(f"Temp list: {to_temp_list(data)}") + print(f"Average temperature (sum / len): {average_temp(data)}") + print(f"Average temperature (mean): {average_temp_mean(data)}") + print(f"Maximum temperature: {max_temp(data)}") + print(f"Row of {MONDAY}:\n{row_for_day(data, MONDAY)}") + print(f"Hottest row:\n{hottest_row(data)}") + print(f"{MONDAY} in Fahrenheit: {day_in_fahrenheit(data, MONDAY)}") + scores = create_scores_frame() + print(f"Scores frame (from a dictionary):\n{scores}") + if export_path is not None: + export_frame(scores, export_path) + print(f"Scores frame exported to: {export_path}") + + +def main(argv: Sequence[str] | None = None) -> int: + """! @brief Print the results of the lectures and optionally export a table. + + @param argv The command line arguments; `sys.argv[1:]` when `None`. + @return `0` on success, `1` when the pandas extra is not installed. + """ + args = _build_parser().parse_args(argv) + _print_csv_part() + try: + _print_pandas_part(args.export) + except ImportError as error: + if error.name != "pandas": + raise + print(PANDAS_MISSING_MESSAGE, file=sys.stderr) + return EXIT_PANDAS_MISSING + return EXIT_OK + + +if __name__ == "__main__": + raise SystemExit(main()) From 8a059681acfcb6c1d259b0f6de631db6fa9a9ff8 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:32:22 +0800 Subject: [PATCH 3/5] Add the tests for create, export and the main flow Task: MIL-004#4 --- tests/test_export.py | 52 +++++++++++++++++++++++++++++++++ tests/test_main.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/test_export.py create mode 100644 tests/test_main.py diff --git a/tests/test_export.py b/tests/test_export.py new file mode 100644 index 0000000..ef69eaa --- /dev/null +++ b/tests/test_export.py @@ -0,0 +1,52 @@ +"""Tests of the creation and the export of the scores table.""" + +from pathlib import Path + +import pytest + +pandas = pytest.importorskip("pandas") + +from weather import export # noqa: E402 + + +def test_create_scores_frame_has_the_columns_and_values_of_the_lecture() -> None: + frame = export.create_scores_frame() + + assert isinstance(frame, pandas.DataFrame) + assert list(frame.columns) == ["students", "scores"] + assert frame["students"].to_list() == ["Amy", "James", "Angela"] + assert frame["scores"].to_list() == [76, 56, 65] + + +def test_export_frame_round_trips_through_a_file(tmp_path: Path) -> None: + frame = export.create_scores_frame() + target = tmp_path / "scores.csv" + + export.export_frame(frame, target) + + assert target.is_file() + read_back = pandas.read_csv(target, index_col=0) + pandas.testing.assert_frame_equal(read_back, frame) + + +def test_export_frame_accepts_a_string_path(tmp_path: Path) -> None: + target = tmp_path / "as_text.csv" + + export.export_frame(export.create_scores_frame(), str(target)) + + assert target.is_file() + + +def test_export_frame_default_name_writes_only_new_data_csv_in_the_working_dir( + tmp_path: Path, +) -> None: + # The autouse fixture in conftest.py made tmp_path the working directory. + export.export_frame(export.create_scores_frame()) + + assert [item.name for item in tmp_path.iterdir()] == ["new_data.csv"] + + +def test_create_scores_frame_writes_nothing(tmp_path: Path) -> None: + export.create_scores_frame() + + assert list(tmp_path.iterdir()) == [] diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..ed505ee --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,68 @@ +"""Tests of the command line: the printed results and what is written.""" + +import sys +from pathlib import Path + +import pytest + +from weather.__main__ import main + + +def test_main_without_arguments_prints_the_results_of_the_lectures( + capsys: pytest.CaptureFixture[str], +) -> None: + pytest.importorskip("pandas") + + assert main([]) == 0 + + output = capsys.readouterr().out + assert "17.428" in output + assert "24" in output + assert "Sunny" in output + assert "53.6" in output + assert "Sunday" in output + assert "Angela" in output + + +def test_main_without_the_flag_writes_no_file(tmp_path: Path) -> None: + pytest.importorskip("pandas") + + assert main([]) == 0 + + assert list(tmp_path.iterdir()) == [] + + +def test_main_with_the_export_flag_writes_the_scores_frame( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + pandas = pytest.importorskip("pandas") + from weather.export import create_scores_frame + + target = tmp_path / "out.csv" + + assert main(["--export", str(target)]) == 0 + + assert [item.name for item in tmp_path.iterdir()] == ["out.csv"] + read_back = pandas.read_csv(target, index_col=0) + pandas.testing.assert_frame_equal(read_back, create_scores_frame()) + assert str(target) in capsys.readouterr().out + + +def test_main_reports_the_missing_pandas_extra_and_writes_nothing( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # None in sys.modules makes "import pandas" raise ImportError; the modules + # that import it are removed so that they are imported again. + monkeypatch.setitem(sys.modules, "pandas", None) + monkeypatch.delitem(sys.modules, "weather.analysis", raising=False) + monkeypatch.delitem(sys.modules, "weather.export", raising=False) + + assert main(["--export", str(tmp_path / "out.csv")]) == 1 + + captured = capsys.readouterr() + assert 'pip install "weather[pandas]"' in captured.err + assert "Sunny" in captured.out + assert "17.428" not in captured.out + assert list(tmp_path.iterdir()) == [] From ace3e66841729c62fd9773f1a2975b9237261f09 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:32:24 +0800 Subject: [PATCH 4/5] Record the review of the MIL-004 code (RC-012) --- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-012-mil-004-code.md | 60 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 docs/sqa/reviews/rc-012-mil-004-code.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index eeb680f..e1c53ba 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g. | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 006 | | DICT | Domain Dictionary | docs/dictionary.md | 002 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 012 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 013 | ## Languages diff --git a/docs/sqa/reviews/rc-012-mil-004-code.md b/docs/sqa/reviews/rc-012-mil-004-code.md new file mode 100644 index 0000000..2adf936 --- /dev/null +++ b/docs/sqa/reviews/rc-012-mil-004-code.md @@ -0,0 +1,60 @@ +# RC-012: Code review of MIL-004 (create and export a DataFrame, and the main flow) + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-012 | +| CrossReference | [MIL-004], [QC-PY-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +## Artifact Under Review + +- Instance reviewed: the source and tests that [MIL-004] delivers (create and export a DataFrame, and the main flow) +- Checklist used: [QC-PY-001] +- Scope: full review of the files changed on the branch of the milestone +- Language and domain: n/a +- Language reviewer: none +- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`; the assistant also wrote the code; the review of the files was done by the `python-developer` agent of the session) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | `export`, `create_scores_frame`, `export_frame`, `main`, `EXIT_OK`, `COLUMN_STUDENTS` follow PEP 8 casing; the private helpers start with `_`. | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`_print_csv_part`, `_print_pandas_part`, `PANDAS_MISSING_MESSAGE`); `data_dict` and `scores` are the lecture's names. | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` exit 0. The only suppressions are the two `# noqa: E402` after `pytest.importorskip("pandas")` in the test modules, explained by the guard. | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every function is annotated: `create_scores_frame() -> DataFrame`, `export_frame(...) -> None`, `main(argv: Sequence[str] | None = None) -> int`, the helpers and the tests. | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `main` catches `ImportError` only when `error.name == "pandas"` and re-raises every other one; nothing is swallowed. | +| 6 | No mutable default arguments and no shadowed builtins | Pass | The defaults are immutable (`EXPORT_FILE_NAME`, `None`); no builtin is shadowed. | +| 7 | Files, locks and connections are managed with context managers | N-A | The modules open no file themselves; `DataFrame.to_csv` opens and closes the file. | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Module docstrings and Doxygen `@brief`, `@param`, `@return`, `@throws` on every function, private helpers included; `doxygen Doxyfile` exits 0 with no warnings. | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | The `print` calls of `__main__` are the program's output, the purpose of the command (as in the lectures); the missing-extra diagnostic goes to stderr; no secret or personal data is printed. There is no logging to replace. | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No Design Class Diagram exists (a learning project of this size, recorded in the open issues of [PP-001]); the functions trace to the second lecture and to the tasks of [MIL-004]. | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | `tests/test_export.py` and `tests/test_main.py` cover the Go/No-Go criteria of [MIL-004]: the frame's columns and values, the round trip through `tmp_path`, the default name only in the temporary working folder, the printed results, no file without `--export`, the missing-pandas path; `pytest` reports 39 passed; none depends on order or the network. | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `python -m mypy` runs with `strict = true`: no issues in 12 source files; no new `Any`. | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | No dependency added; `pandas` stays in the extras and is imported only in `analysis` and `export`; `__main__` imports them inside a function so the csv part runs without it. | +| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The lecture's dictionary values, column names, the day and the message are constants in `constants.py`; the export name is defined once. | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `__main__` (delivery) imports `csv_reader`, `analysis`, `export` and `constants`; none of them imports `__main__`; no cycle. | +| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | `create_scores_frame`, `export_frame` and `main` each have one reason to change; the printing is split into small helpers; no class hierarchy. | + +## Overall Verdict + +Go — all Mandatory criteria pass or are not applicable. Assessed on 2026-10-13 by the assistant that wrote the code, as stand-in for S01 in autonomous mode. Mode: autonomous, not independent. The pull request is the second look. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| Challenge this `Go` in the run summary or on the pull request if the stand-in decision is not wanted | S01 | 2026-10-23 | + +--- + +[MIL-004]: ../../milestones/mil-004-create-and-export.md +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[PP-001]: ../../project-plan.md From 1f1f0281b24fbfd87c4ce884b52cfc3ed981f85e Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:32:25 +0800 Subject: [PATCH 5/5] Resolve pending commit links --- docs/sqa/reviews/rc-012-mil-004-code.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/sqa/reviews/rc-012-mil-004-code.md b/docs/sqa/reviews/rc-012-mil-004-code.md index 2adf936..14163c8 100644 --- a/docs/sqa/reviews/rc-012-mil-004-code.md +++ b/docs/sqa/reviews/rc-012-mil-004-code.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-10-13 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [ace3e66] | --- @@ -58,3 +58,4 @@ Go — all Mandatory criteria pass or are not applicable. Assessed on 2026-10-13 [MIL-004]: ../../milestones/mil-004-create-and-export.md [QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md [PP-001]: ../../project-plan.md +[ace3e66]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-weather/commit/ace3e66841729c62fd9773f1a2975b9237261f09