diff --git a/courses_processor/compare_courses.py b/courses_processor/compare_courses.py index 044bfae..80e7463 100644 --- a/courses_processor/compare_courses.py +++ b/courses_processor/compare_courses.py @@ -7,7 +7,7 @@ preselection_file = "Таблица предвыборности.xlsx" -def standardize_course_name(course): +def standardize_course_name(course: str) -> str | None: if not isinstance(course, str): return None @@ -83,24 +83,24 @@ def standardize_course_name(course): return course -def timetable_parser(timetable_filename: str): - df = pd.read_excel(timetable_filename, sheet_name="Расписание") - timetable = df.iloc[2:, 5:10].to_numpy().flatten() +def timetable_parser(timetable_filename: str) -> set[str]: + timetable_df = pd.read_excel(timetable_filename, sheet_name="Расписание") + timetable = timetable_df.iloc[2:, 5:10].to_numpy().flatten() t = pd.Series(timetable) full_courses = set(t[pd.notna(t)].tolist()) return set(filter(None, map(standardize_course_name, full_courses))) -def preselection_parser(preselection_filename: str, preselection_params): - df = pd.read_excel(preselection_filename, sheet_name=preselection_params["name"]) - courses_row = df.iloc[1, 4:-5] +def preselection_parser(preselection_filename: str, preselection_params: dict[str, str]) -> set[str]: + preselection_df = pd.read_excel(preselection_filename, sheet_name=preselection_params["name"]) + courses_row = preselection_df.iloc[1, 4:-5] courses = set(courses_row.tolist()) return set(filter(None, map(standardize_course_name, courses))) def notion_parser(notion_filename: str) -> set[str]: - df = pd.read_csv(notion_filename) - courses = df["Курсы"].tolist() + notion_df = pd.read_csv(notion_filename) + courses = notion_df["Курсы"].tolist() standardized_courses = [] for course in map(standardize_course_name, courses): if isinstance(course, list): @@ -110,7 +110,7 @@ def notion_parser(notion_filename: str) -> set[str]: return set(standardized_courses) -def main(course_folder: str, preselection_params): +def main(course_folder: str, preselection_params: dict[str, str]) -> tuple[set[str], set[str], set[str]]: notion_courses = notion_parser(course_folder + notion_file) timetable_courses = timetable_parser(course_folder + timetable_file) preselection_courses = preselection_parser(course_folder + preselection_file, preselection_params) diff --git a/pyproject.toml b/pyproject.toml index 04abeca..9c14ad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,49 +97,16 @@ target-version = "py310" line-length = 120 [tool.ruff.lint] -select= [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "UP", # pyupgrade - "RUF", # ruff - "N", # flake8-naming - "S", # flake8-bandit - "BLE", # flake8-blind-except - "A", # flake8-builtins - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PIE", # flake8-pie - "INP", # flake8-no-pep420 - "T20", # flake8-print - "PT", # flake8-pytest-style - "RET", # flake8-return - "SIM", # flake8-simplify - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "PL", # pylint - "NPY", # NumPy-specific rules - "TCH", # flake8-type-checking - "TID", # flake8-tidy-imports -# "FAST", # fastapi -# "D", # pydocstyle -# "G", # flake8-logging-format -# "EM", # flake8-errmsg -] -ignore = [ - "RUF001", - "ISC001", -] +select= ["ALL"] +ignore = ["D", "EM", "TRY", "G", "COM812", "ISC001"] +allowed-confusables = ["а", "с", "е", "З", "о", "г", "х", "у", "А", "С", "Е", "З", "О", "Р", "В", "М", "К", "р", "В"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F405", "F403", "D"] -"tests/*.py" = ["S", "PLR2004"] +"tests/*.py" = ["S", "PLR2004", "ERA", "D", "ANN", "SLF"] "src/itmo_ai_timetable/db/migrations/versions/*.py" = ["N999"] "courses_processor/*.py" = ["PTH", "T201", "PLW2901", "RUF003", "INP001"] +"src/itmo_ai_timetable/gcal.py" = ["ERA"] [tool.ruff.lint.isort] known-first-party = ["itmo_ai_timetable"] diff --git a/src/itmo_ai_timetable/cleaner.py b/src/itmo_ai_timetable/cleaner.py index 566f4af..950bb54 100644 --- a/src/itmo_ai_timetable/cleaner.py +++ b/src/itmo_ai_timetable/cleaner.py @@ -24,8 +24,8 @@ def course_name_cleaner(course: str) -> str | None: ), "Симулятор DS от Karpov.courses": "Симулятор DS от Karpov.Courses", "DS симулятор от Karpov.courses": "Симулятор DS от Karpov.Courses", - # appears in only course description - # "Программирование на С++": ["C++ Lite", "C++ Hard"], # noqa: RUF003 + # appears in only course description, but this function only uses for preselection and timetable + # "Программирование на С++": ["C++ Lite", "C++ Hard"], "Uplift-моделирование": "UPLIFT-моделирование", "Продвинутое A/B-тестирование": "Продвинутое А/B - тестирование", "А/В тестирование и Reliable ML": "А/В тестирование", diff --git a/src/itmo_ai_timetable/cli.py b/src/itmo_ai_timetable/cli.py index 0dc3d99..a7e0a05 100644 --- a/src/itmo_ai_timetable/cli.py +++ b/src/itmo_ai_timetable/cli.py @@ -4,9 +4,8 @@ import json from pathlib import Path -from repository import Repository - from itmo_ai_timetable.logger import get_logger +from itmo_ai_timetable.repository import Repository from itmo_ai_timetable.schedule_parser import ScheduleParser from itmo_ai_timetable.selection_parser import SelectionParser from itmo_ai_timetable.transform_ics import export_ics @@ -24,14 +23,20 @@ def create_args() -> argparse.Namespace: subparsers = parser.add_subparsers(required=True, dest="subparser_name") schedule_parser = subparsers.add_parser(SubparserName.SCHEDULE, help="Обработка excel в ics") schedule_parser.add_argument( - "--filepath", help="Путь к файлу excel", default="Расписание 1 курс весна 2024.xlsx", type=str + "--filepath", + help="Путь к файлу excel", + default="Расписание 1 курс весна 2024.xlsx", + type=str, ) schedule_parser.add_argument("--output_path", help="Папка для экспорта ics", default="ics", type=str) schedule_parser.add_argument("--sheet", help="Страница с расписанием в excel файле", default=0, type=int) selection_parser = subparsers.add_parser(SubparserName.SELECTION, help="Обработка excel с выборностью") selection_parser.add_argument( - "--filepath", help="Путь к файлу excel", default="Таблица предвыборности осень 2024 (2 курс).xlsx", type=str + "--filepath", + help="Путь к файлу excel", + default="Таблица предвыборности осень 2024 (2 курс).xlsx", + type=str, ) selection_parser.add_argument("--output_path", help="Папка для экспорта ics", default="ics", type=str) selection_parser.add_argument("--sheet_name", help="Страница с расписанием в excel файле", type=str) @@ -41,7 +46,11 @@ def create_args() -> argparse.Namespace: selection_parser.add_argument("--name_column", help="Столбец с именами", default="A", type=str) selection_parser.add_argument("--course_number", help="Номер курса", default=2, type=int) selection_parser.add_argument( - "--db", help="Сохранить результат в db", action=argparse.BooleanOptionalAction, type=bool, default=False + "--db", + help="Сохранить результат в db", + action=argparse.BooleanOptionalAction, + type=bool, + default=False, ) return parser.parse_args() @@ -56,8 +65,8 @@ async def main() -> None: Path.mkdir(output_dir) match args.subparser_name: case SubparserName.SCHEDULE: - df = ScheduleParser(args.filepath, args.sheet_num).parse() - export_ics(df, output_path) + schedule = ScheduleParser(args.filepath, args.sheet_num).parse() + export_ics(schedule, output_path) case SubparserName.SELECTION: results = SelectionParser( args.filepath, @@ -67,7 +76,7 @@ async def main() -> None: args.last_select_column, args.name_column, ).parse() - with Path(output_path).open("w") as f: + with Path(output_path).open("w") as f: # noqa: ASYNC230 json.dump(results, f, ensure_ascii=False, indent=4) course_number = args.course_number if args.db: diff --git a/src/itmo_ai_timetable/db/base.py b/src/itmo_ai_timetable/db/base.py index 93216a3..7974973 100644 --- a/src/itmo_ai_timetable/db/base.py +++ b/src/itmo_ai_timetable/db/base.py @@ -99,7 +99,9 @@ class Class(Base): end_time: Mapped[datetime] class_type: Mapped[str] = mapped_column(nullable=True) class_status_id: Mapped[int] = mapped_column( - ForeignKey("class_status.id"), nullable=False, default=get_class_status_id(ClassStatus.need_to_add) + ForeignKey("class_status.id"), + nullable=False, + default=get_class_status_id(ClassStatus.need_to_add), ) gcal_event_id: Mapped[str] = mapped_column(nullable=True) diff --git a/src/itmo_ai_timetable/db/migrations/env.py b/src/itmo_ai_timetable/db/migrations/env.py index bd2a527..aef6f9f 100644 --- a/src/itmo_ai_timetable/db/migrations/env.py +++ b/src/itmo_ai_timetable/db/migrations/env.py @@ -75,7 +75,6 @@ def run_migrations_online() -> None: and associate a connection with the context. """ - connectable = config.attributes.get("connection", None) if connectable is None: diff --git a/src/itmo_ai_timetable/db/session_manager.py b/src/itmo_ai_timetable/db/session_manager.py index 8735f1b..37d9793 100644 --- a/src/itmo_ai_timetable/db/session_manager.py +++ b/src/itmo_ai_timetable/db/session_manager.py @@ -15,7 +15,7 @@ class SessionManager: def __new__(cls) -> "SessionManager": if cls._instance is None: cls._instance = super().__new__(cls) - cls._instance._initialize() + cls._instance._initialize() # noqa: SLF001 return cls._instance def _initialize(self) -> None: @@ -41,7 +41,7 @@ def engine(self) -> AsyncEngine: def with_async_session(func: Callable[..., Any]) -> Callable[..., Any]: @wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> Any: + async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 if "session" in kwargs: return await func(*args, **kwargs) session_maker = SessionManager().session_maker diff --git a/src/itmo_ai_timetable/repository.py b/src/itmo_ai_timetable/repository.py index 5530062..8b3af27 100644 --- a/src/itmo_ai_timetable/repository.py +++ b/src/itmo_ai_timetable/repository.py @@ -23,7 +23,9 @@ async def get_course(course_name: str, session: AsyncSession) -> Course | None: @staticmethod async def get_existing_classes( - course_id: int, synced_status: ClassStatusTable, session: AsyncSession + course_id: int, + synced_status: ClassStatusTable, + session: AsyncSession, ) -> Sequence[Class]: query = select(Class).filter(and_(Class.course_id == course_id, Class.class_status == synced_status)) result = await session.execute(query) @@ -78,7 +80,7 @@ async def add_classes(classes: list[Pair], *, session: AsyncSession) -> list[str @with_async_session async def get_or_create_user(user_name: str, course_number: int, *, session: AsyncSession) -> User: query = await session.execute( - select(User).filter(and_(User.user_real_name == user_name, User.studying_course == course_number)) + select(User).filter(and_(User.user_real_name == user_name, User.studying_course == course_number)), ) user = query.scalar() if user is None: @@ -90,9 +92,8 @@ async def get_or_create_user(user_name: str, course_number: int, *, session: Asy @staticmethod @with_async_session async def create_matching(selected: dict[str, list[str]], course_number: int, *, session: AsyncSession) -> None: - """ - - :param selected: contains pairs of names of users and courses he selected + """:param selected: contains pairs of names of users and courses he selected + :param course_number: number of course :param session: :return: """ diff --git a/src/itmo_ai_timetable/schedule_parser.py b/src/itmo_ai_timetable/schedule_parser.py index d6298f6..22cab8f 100644 --- a/src/itmo_ai_timetable/schedule_parser.py +++ b/src/itmo_ai_timetable/schedule_parser.py @@ -103,7 +103,7 @@ def _parse_row(self, cells: Iterable[Cell], pair_start: datetime, pair_end: date name=title, pair_type=pair_type, link=link, - ) + ), ) return pairs diff --git a/src/itmo_ai_timetable/selection_parser.py b/src/itmo_ai_timetable/selection_parser.py index ae97bbc..1f84f79 100644 --- a/src/itmo_ai_timetable/selection_parser.py +++ b/src/itmo_ai_timetable/selection_parser.py @@ -1,10 +1,11 @@ from collections import defaultdict import openpyxl -from cleaner import course_name_cleaner from openpyxl.cell import MergedCell from openpyxl.utils import column_index_from_string +from itmo_ai_timetable.cleaner import course_name_cleaner + class SelectionParser: def __init__( @@ -15,7 +16,7 @@ def __init__( first_select_column: str, last_select_column: str, name_column: str = "A", - ): + ) -> None: self.workbook = openpyxl.load_workbook(filepath) self.sheet = self.workbook[sheet_name] self.course_row = course_row @@ -24,7 +25,7 @@ def __init__( self.name_column = name_column self.data_start_row = course_row + 1 - def parse(self) -> dict[str, list[str]]: + def parse(self) -> dict[str, list[str | None]]: courses = self._get_courses() return self._match_names_to_courses(courses) @@ -33,7 +34,7 @@ def _get_courses(self) -> list[tuple[str, str]]: for cell in self.sheet[self.course_row]: if isinstance(cell, MergedCell): if cell.column <= column_index_from_string( - self.start_column + self.start_column, ) or cell.column >= column_index_from_string(self.end_column): break raise ValueError(f"Cell {cell} is merged") @@ -47,7 +48,7 @@ def _get_courses(self) -> list[tuple[str, str]]: courses.append((cell.column_letter, cell.value)) return courses - def _match_names_to_courses(self, courses: list[tuple[str, str]]) -> dict[str, list[str]]: + def _match_names_to_courses(self, courses: list[tuple[str, str]]) -> dict[str, list[str | None]]: matches = defaultdict(list) for row in self.sheet.iter_rows(min_row=self.data_start_row, min_col=1, max_col=1): name = row[0].value diff --git a/src/itmo_ai_timetable/settings.py b/src/itmo_ai_timetable/settings.py index 3ed45f5..a2d7467 100644 --- a/src/itmo_ai_timetable/settings.py +++ b/src/itmo_ai_timetable/settings.py @@ -1,5 +1,3 @@ -from typing import Any - from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -25,7 +23,7 @@ class Settings(BaseSettings): timezone: str = "Europe/Moscow" @property - def database_settings(self) -> Any: + def database_settings(self) -> dict[str, str | int]: return { "database": self.postgres_db, "user": self.postgres_user, diff --git a/src/itmo_ai_timetable/transform_ics.py b/src/itmo_ai_timetable/transform_ics.py index d6496fa..6afd671 100644 --- a/src/itmo_ai_timetable/transform_ics.py +++ b/src/itmo_ai_timetable/transform_ics.py @@ -1,6 +1,6 @@ from pathlib import Path -from ics import Calendar, Event # type: ignore +from ics import Calendar, Event # type: ignore[attr-defined] from itmo_ai_timetable.schemes import Pair diff --git a/tests/conftest.py b/tests/conftest.py index 75e46b8..fc6da0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ def postgres() -> str: settings = Settings() - tmp_name = ".".join([uuid4().hex, "pytest"]) + tmp_name = f"{uuid4().hex}.pytest" settings.postgres_db = tmp_name environ["POSTGRES_DB"] = tmp_name @@ -57,17 +57,13 @@ def alembic_config(postgres) -> Config: @pytest.fixture def alembic_engine(postgres): - """ - Override this fixture to provide pytest-alembic powered tests with a database handle. - """ + """Override this fixture to provide pytest-alembic powered tests with a database handle.""" return create_async_engine(postgres, echo=True) @pytest.fixture async def _migrated_postgres(postgres, alembic_config: Config): - """ - Проводит миграции. - """ + """Проводит миграции.""" await run_async_upgrade(alembic_config, postgres) diff --git a/tests/test_repository.py b/tests/test_repository.py index bac6923..3d828fa 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -124,7 +124,7 @@ async def test_add_classes_new_course(session: AsyncSession): end_time=end_time, pair_type="Lecture", link=None, - ) + ), ] await Repository.add_classes(classes, session=session) @@ -160,7 +160,7 @@ async def test_add_classes_existing_course(session: AsyncSession): end_time=end_time, pair_type="Lecture", link=None, - ) + ), ] await Repository.add_classes(classes, session=session) @@ -198,7 +198,7 @@ async def test_add_classes_update_existing(session: AsyncSession): end_time=end_time, pair_type="Lab", link=None, - ) + ), ] await Repository.add_classes(classes, session=session) @@ -235,7 +235,7 @@ async def test_add_classes_delete_existing(session: AsyncSession): end_time=datetime(2023, 1, 1, 17, 30, tzinfo=tzinfo), pair_type="Seminar", link=None, - ) + ), ] await Repository.add_classes(classes, session=session)