From 6f719a1b8627f0887b930df88082d11f200e5580 Mon Sep 17 00:00:00 2001 From: Ivanov Ivan Ivanovich Date: Mon, 29 Sep 2025 19:52:06 +0300 Subject: [PATCH 1/4] Variant 8: YAML reader + Q2 calculator + tests + .gitignore + LICENSE --- .gitignore | 39 ++++++++++++++++++++++++++++++ LICENSE | 21 ++++++++++++++++ data/data.yaml | 8 ++++++ requirements.txt | 3 ++- src/QuartileCalculator.py | 26 ++++++++++++++++++++ src/YamlDataReader.py | 43 +++++++++++++++++++++++++++++++++ src/main.py | 27 ++++++++++++--------- test/main.py | 14 +++++------ test/test_QuartileCalculator.py | 18 ++++++++++++++ test/test_YamlDataReader.py | 42 ++++++++++++++++++++++++++++++++ 10 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 data/data.yaml create mode 100644 src/QuartileCalculator.py create mode 100644 src/YamlDataReader.py create mode 100644 test/test_QuartileCalculator.py create mode 100644 test/test_YamlDataReader.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3612ab5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.env +.venv +env/ +venv/ + +# IDEs +.vscode/ +.idea/ + +# Pytest +.pytest_cache/ +.coverage +htmlcov/ + +# Jupyter Notebook checkpoints +.ipynb_checkpoints/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a093e11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Fenrir. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/data/data.yaml b/data/data.yaml new file mode 100644 index 0000000..5544338 --- /dev/null +++ b/data/data.yaml @@ -0,0 +1,8 @@ +- Иванов Иван Иванович + математика 67 + литература 100 + программирование 91 +- Петров Петр Петрович + математика 78 + химия 87 + социология 61 diff --git a/requirements.txt b/requirements.txt index 07ead35..6f19555 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pytest -pycodestyle \ No newline at end of file +pycodestyle +pyyaml \ No newline at end of file diff --git a/src/QuartileCalculator.py b/src/QuartileCalculator.py new file mode 100644 index 0000000..091355f --- /dev/null +++ b/src/QuartileCalculator.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from typing import Dict, List +from statistics import quantiles + + +class QuartileCalculator: + """ + Принимает на вход словарь { ФИО: рейтинг } и позволяет получить студентов, + попавших в заданную квартиль. Для 2-й квартиль используем правило: + q1 < rating <= q2 (верхняя граница включительно). + """ + + def __init__(self, ratings: Dict[str, float]) -> None: + self.ratings = ratings + + def second_quartile_students(self) -> List[str]: + values = list(self.ratings.values()) + if not values: + return [] + + # Получим границы квартилей (Q1, Q2, Q3) + q1, q2, _q3 = quantiles(values, n=4, method="inclusive") + + return sorted( + [name for name, r in self.ratings.items() if (r > q1 and r <= q2)] + ) diff --git a/src/YamlDataReader.py b/src/YamlDataReader.py new file mode 100644 index 0000000..e6d3daf --- /dev/null +++ b/src/YamlDataReader.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from typing import Dict, List, Tuple +import yaml # type: ignore +from Types import DataType +from DataReader import DataReader + + +class YamlDataReader(DataReader): + """ + Ожидаемый формат YAML (список студентов): + - Иванов Иван Иванович: + математика: 67 + литература: 100 + программирование: 91 + - Петров Петр Петрович: + математика: 78 + химия: 87 + социология: 61 + """ + + def __init__(self) -> None: + self.students: DataType = {} + + def read(self, path: str) -> DataType: + with open(path, "r", encoding="utf-8") as f: + loaded = yaml.safe_load(f) + + # loaded — это список словарей { "ФИО": { "предмет": балл, ... } } + self.students = {} + if not isinstance(loaded, list): + return self.students + + for item in loaded: + if not isinstance(item, dict): + continue + for full_name, subjects in item.items(): + subj_list: List[Tuple[str, int]] = [] + if isinstance(subjects, dict): + for subj, score in subjects.items(): + subj_list.append((str(subj), int(score))) + self.students[str(full_name)] = subj_list + + return self.students diff --git a/src/main.py b/src/main.py index 3f6570e..4a7c0d0 100644 --- a/src/main.py +++ b/src/main.py @@ -1,28 +1,33 @@ # -*- coding: utf-8 -*- import argparse import sys - from CalcRating import CalcRating from TextDataReader import TextDataReader +from YamlDataReader import YamlDataReader +from QuartileCalculator import QuartileCalculator -def get_path_from_arguments(args) -> str: +def get_path_from_arguments(args) -> tuple[str, bool]: parser = argparse.ArgumentParser(description="Path to datafile") - parser.add_argument("-p", dest="path", type=str, required=True, - help="Path to datafile") - args = parser.parse_args(args) - return args.path + parser.add_argument("-p", dest="path", type=str, required=True, help="Path to datafile") + parser.add_argument("--yaml", action="store_true", help="Use YAML reader") + parsed = parser.parse_args(args) + return parsed.path, parsed.yaml -def main(): - path = get_path_from_arguments(sys.argv[1:]) +def main() -> None: + path, use_yaml = get_path_from_arguments(sys.argv[1:]) - reader = TextDataReader() + reader = YamlDataReader() if use_yaml else TextDataReader() students = reader.read(path) - print("Students: ", students) + print("Students:", students) rating = CalcRating(students).calc() - print("Rating: ", rating) + print("Rating:", rating) + + # Вариант 8: вывести студентов второй квартиль + q = QuartileCalculator(rating) + print("2nd quartile students:", q.second_quartile_students()) if __name__ == "__main__": diff --git a/test/main.py b/test/main.py index 4c0a562..345db35 100644 --- a/test/main.py +++ b/test/main.py @@ -4,8 +4,8 @@ @pytest.fixture() -def correct_arguments_string() -> tuple[list[str], str]: - return ["-p", "/home/user/file.txt"], "/home/user/file.txt" +def correct_arguments_string() -> tuple[list[str], tuple[str, bool]]: + return ["-p", "/home/user/file.txt"], ("/home/user/file.txt", False) @pytest.fixture() @@ -13,14 +13,12 @@ def noncorrect_arguments_string() -> list[str]: return ["/home/user/file.txt"] -def test_get_path_from_correct_arguments( - correct_arguments_string: tuple[list[str], str]) -> None: - path = get_path_from_arguments(correct_arguments_string[0]) - assert path == correct_arguments_string[1] +def test_get_path_from_correct_arguments(correct_arguments_string) -> None: + path, is_yaml = get_path_from_arguments(correct_arguments_string[0]) + assert (path, is_yaml) == correct_arguments_string[1] -def test_get_path_from_noncorrect_arguments( - noncorrect_arguments_string: list[str]) -> None: +def test_get_path_from_noncorrect_arguments(noncorrect_arguments_string: list[str]) -> None: with pytest.raises(SystemExit) as e: get_path_from_arguments(noncorrect_arguments_string[0]) assert e.type == SystemExit diff --git a/test/test_QuartileCalculator.py b/test/test_QuartileCalculator.py new file mode 100644 index 0000000..14c249c --- /dev/null +++ b/test/test_QuartileCalculator.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from src.QuartileCalculator import QuartileCalculator +from src.CalcRating import CalcRating +from src.Types import DataType + + +def test_second_quartile_students() -> None: + # Дадим 4 студента, чтобы квартильные границы были корректны + data: DataType = { + "A": [("s1", 60), ("s2", 60)], # среднее = 60 + "B": [("s1", 70), ("s2", 70)], # среднее = 70 -> Q2 + "C": [("s1", 80), ("s2", 80)], # среднее = 80 -> Q3 + "D": [("s1", 90), ("s2", 90)], # среднее = 90 -> Q4 + } + ratings = CalcRating(data).calc() + q = QuartileCalculator(ratings) + # Во вторую квартиль попадают значения (q1, q2] — здесь это только B + assert q.second_quartile_students() == ["B"] diff --git a/test/test_YamlDataReader.py b/test/test_YamlDataReader.py new file mode 100644 index 0000000..6e879f8 --- /dev/null +++ b/test/test_YamlDataReader.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import pytest +from src.Types import DataType +from src.YamlDataReader import YamlDataReader + + +class TestYamlDataReader: + @pytest.fixture() + def yaml_text_and_data(self) -> tuple[str, DataType]: + yaml_text = ( + "- Иванов Иван Иванович:\n" + " математика: 67\n" + " литература: 100\n" + " программирование: 91\n" + "- Петров Петр Петрович:\n" + " математика: 78\n" + " химия: 87\n" + " социология: 61\n" + ) + expected: DataType = { + "Иванов Иван Иванович": [ + ("математика", 67), + ("литература", 100), + ("программирование", 91), + ], + "Петров Петр Петрович": [ + ("математика", 78), + ("химия", 87), + ("социология", 61), + ], + } + return yaml_text, expected + + @pytest.fixture() + def filepath_and_data(self, yaml_text_and_data: tuple[str, DataType], tmpdir) -> tuple[str, DataType]: + p = tmpdir.mkdir("datadir").join("students.yaml") + p.write_text(yaml_text_and_data[0], encoding="utf-8") + return str(p), yaml_text_and_data[1] + + def test_read_yaml(self, filepath_and_data: tuple[str, DataType]) -> None: + content = YamlDataReader().read(filepath_and_data[0]) + assert content == filepath_and_data[1] From 884f202449f2e9833a0718a5293ae113b3f8b9ba Mon Sep 17 00:00:00 2001 From: Ivanov Ivan Ivanovich Date: Mon, 29 Sep 2025 20:29:36 +0300 Subject: [PATCH 2/4] CI: run tests also on pull requests --- .github/workflows/github-actions-testing.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-actions-testing.yml b/.github/workflows/github-actions-testing.yml index f900f2d..1f0c55e 100644 --- a/.github/workflows/github-actions-testing.yml +++ b/.github/workflows/github-actions-testing.yml @@ -1,9 +1,12 @@ name: Testing the Python code -on: - push: - branches: - - main +on: + push: + branches: + - main + pull_request: + branches: + - main jobs: build: From 3188f436108e4af9d4fc408e153bdd3fa38e9acc Mon Sep 17 00:00:00 2001 From: Ivanov Ivan Ivanovich Date: Mon, 29 Sep 2025 20:38:37 +0300 Subject: [PATCH 3/4] Fix: split long lines to satisfy PEP8 (E501) --- src/main.py | 14 ++++++++++++-- test/test_YamlDataReader.py | 11 +++++++++-- test/test_main.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 test/test_main.py diff --git a/src/main.py b/src/main.py index 4a7c0d0..3c33483 100644 --- a/src/main.py +++ b/src/main.py @@ -9,8 +9,18 @@ def get_path_from_arguments(args) -> tuple[str, bool]: parser = argparse.ArgumentParser(description="Path to datafile") - parser.add_argument("-p", dest="path", type=str, required=True, help="Path to datafile") - parser.add_argument("--yaml", action="store_true", help="Use YAML reader") + parser.add_argument( + "-p", + dest="path", + type=str, + required=True, + help="Path to datafile" + ) + parser.add_argument( + "--yaml", + action="store_true", + help="Use YAML reader" + ) parsed = parser.parse_args(args) return parsed.path, parsed.yaml diff --git a/test/test_YamlDataReader.py b/test/test_YamlDataReader.py index 6e879f8..04845c0 100644 --- a/test/test_YamlDataReader.py +++ b/test/test_YamlDataReader.py @@ -32,11 +32,18 @@ def yaml_text_and_data(self) -> tuple[str, DataType]: return yaml_text, expected @pytest.fixture() - def filepath_and_data(self, yaml_text_and_data: tuple[str, DataType], tmpdir) -> tuple[str, DataType]: + def filepath_and_data( + self, + yaml_text_and_data: tuple[str, DataType], + tmpdir + ) -> tuple[str, DataType]: p = tmpdir.mkdir("datadir").join("students.yaml") p.write_text(yaml_text_and_data[0], encoding="utf-8") return str(p), yaml_text_and_data[1] - def test_read_yaml(self, filepath_and_data: tuple[str, DataType]) -> None: + def test_read_yaml( + self, + filepath_and_data: tuple[str, DataType] + ) -> None: content = YamlDataReader().read(filepath_and_data[0]) assert content == filepath_and_data[1] diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..6df1fd0 --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from src.main import get_path_from_arguments +import pytest + + +@pytest.fixture() +def correct_arguments_string() -> tuple[list[str], tuple[str, bool]]: + return ["-p", "/home/user/file.txt"], ("/home/user/file.txt", False) + + +@pytest.fixture() +def noncorrect_arguments_string() -> list[str]: + return ["/home/user/file.txt"] + + +def test_get_path_from_correct_arguments( + correct_arguments_string: tuple[list[str], tuple[str, bool]] +) -> None: + path, is_yaml = get_path_from_arguments(correct_arguments_string[0]) + assert (path, is_yaml) == correct_arguments_string[1] + + +def test_get_path_from_noncorrect_arguments( + noncorrect_arguments_string: list[str] +) -> None: + with pytest.raises(SystemExit) as e: + get_path_from_arguments(noncorrect_arguments_string[0]) + assert e.type == SystemExit From 9b0d7a55679009cbb6414256b3451e2fbb3882e4 Mon Sep 17 00:00:00 2001 From: Ivanov Ivan Ivanovich Date: Mon, 29 Sep 2025 20:45:45 +0300 Subject: [PATCH 4/4] Fix: split long function signature for PEP8 (E501) --- test/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/main.py b/test/main.py index 345db35..6df1fd0 100644 --- a/test/main.py +++ b/test/main.py @@ -13,12 +13,16 @@ def noncorrect_arguments_string() -> list[str]: return ["/home/user/file.txt"] -def test_get_path_from_correct_arguments(correct_arguments_string) -> None: +def test_get_path_from_correct_arguments( + correct_arguments_string: tuple[list[str], tuple[str, bool]] +) -> None: path, is_yaml = get_path_from_arguments(correct_arguments_string[0]) assert (path, is_yaml) == correct_arguments_string[1] -def test_get_path_from_noncorrect_arguments(noncorrect_arguments_string: list[str]) -> None: +def test_get_path_from_noncorrect_arguments( + noncorrect_arguments_string: list[str] +) -> None: with pytest.raises(SystemExit) as e: get_path_from_arguments(noncorrect_arguments_string[0]) assert e.type == SystemExit