├── .dockerignore ├── app ├── __init__.py ├── api │ ├── __init__.py │ └── api_v1 │ │ ├── __init__.py │ │ ├── endpoints │ │ ├── __init__.py │ │ └── schedule.py │ │ └── router.py ├── core │ ├── __init__.py │ ├── config.py │ └── schedule_utils.py ├── crud │ ├── __init__.py │ └── schedule.py ├── models │ ├── __init__.py │ └── schedule.py ├── database │ ├── __init__.py │ ├── database.py │ └── database_connection.py ├── schedule_parser │ ├── __init__.py │ └── excel.py └── main.py ├── .env.example ├── docs ├── README.md └── files.json ├── cron ├── start_server.py ├── .idea ├── misc.xml ├── vcs.xml ├── .gitignore ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml └── rtu-mirea-schedule.iml ├── requirements.txt ├── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md └── workflows │ └── codeql-analysis.yml ├── README.md ├── docker-compose.yml ├── LICENSE ├── docker-compose.production.yml └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/crud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api_v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/schedule_parser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/api_v1/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DOMAIN=localhost 2 | DEBUG=false 3 | SECRET_REFRESH_KEY=secret -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Эта папка содержит документы с расписанием, которые будут переопределять расписание официального сайта. -------------------------------------------------------------------------------- /cron: -------------------------------------------------------------------------------- 1 | 0 * * * * root wget -qO- --no-check-certificate "http://backend:5000/api/refresh?secret_key=$SECRET_REFRESH_KEY" >/dev/null 2>&1 -------------------------------------------------------------------------------- /start_server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from app.main import app 4 | 5 | if __name__ == "__main__": 6 | uvicorn.run(app, host="0.0.0.0", port=5000) 7 | -------------------------------------------------------------------------------- /docs/files.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "file": "расписание колледж.xlsx", 4 | "institute": "КПК", 5 | "type": 1, 6 | "degree": 4 7 | } 8 | ] -------------------------------------------------------------------------------- /app/api/api_v1/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from .endpoints.schedule import router as schedule_router 4 | 5 | router = APIRouter() 6 | router.include_router(schedule_router) 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pymongo==4.3.3 3 | uvicorn==0.20.0 4 | fastapi==0.89.0 5 | pydantic==1.10.2 6 | motor==3.1.1 7 | config 8 | pandas 9 | python-dotenv 10 | pytz 11 | git+https://github.com/mirea-ninja/rtu-schedule-parser@main -------------------------------------------------------------------------------- /app/database/database.py: -------------------------------------------------------------------------------- 1 | from motor.motor_asyncio import AsyncIOMotorClient 2 | 3 | 4 | class DataBase: 5 | client: AsyncIOMotorClient = None 6 | 7 | 8 | db = DataBase() 9 | 10 | 11 | def get_database() -> AsyncIOMotorClient: 12 | return db.client 13 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/database/database_connection.py: -------------------------------------------------------------------------------- 1 | from motor.motor_asyncio import AsyncIOMotorClient 2 | 3 | from ..core.config import MONGODB_URL 4 | from .database import db 5 | 6 | 7 | async def connect_to_mongo(): 8 | db.client = AsyncIOMotorClient(MONGODB_URL) 9 | 10 | 11 | async def close_mongo_connection(): 12 | db.client.close() 13 | -------------------------------------------------------------------------------- /.idea/rtu-mirea-schedule.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:latest 2 | 3 | ENV PYTHONDONTWRITEBYTECODE=1 4 | ENV PYTHONUNBUFFERED=1 5 | 6 | COPY requirements.txt /app/ 7 | 8 | WORKDIR /app/ 9 | 10 | RUN apt-get update && \ 11 | apt-get install -y python3-venv && \ 12 | pip install --no-cache-dir --upgrade pip && \ 13 | pip cache remove rtu-schedule-parser && \ 14 | pip install --no-cache-dir -r requirements.txt 15 | 16 | COPY /app/ /app/app/ 17 | 18 | ENV PORT="${PORT:-5000}" 19 | ENV APP_MODULE="app.main:app" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Создайте отчет, который поможет нам исправить имеющиеся проблемы 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: 0niel 7 | 8 | --- 9 | 10 | **Описание ошибки** 11 | Четкое и краткое описание того, в чем заключается ошибка. 12 | 13 | **Ожидаемое поведение** 14 | Четкое и краткое описание того, что вы ожидали увидеть. 15 | 16 | **Скриншоты или видео** 17 | Если это необходимо, чтобы понять и оценить ошибку, то добавьте скриншоты или видео 18 | 19 | **Дополнительный текст** 20 | Добавьте сюда любой другой текст, касающийся этой проблемы. 21 | -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv(".env") 6 | 7 | SECRET_REFRESH_KEY = str(os.getenv("SECRET_REFRESH_KEY", None)) 8 | 9 | MAX_CONNECTIONS_COUNT = int(os.getenv("MAX_CONNECTIONS_COUNT", 10)) 10 | MIN_CONNECTIONS_COUNT = int(os.getenv("MIN_CONNECTIONS_COUNT", 10)) 11 | 12 | MONGODB_URL = os.getenv("MONGODB_URL", "mongodb://127.0.0.1:27017/") 13 | 14 | API_V1_PREFIX = "/api" 15 | 16 | DATABASE_NAME = "schedule" 17 | SCHEDULE_COLLECTION_NAME = "schedule" 18 | SESSION_COLLECTION_NAME = "session" 19 | SCHEDULE_UPDATES_COLLECTION = "schedule_updates" 20 | SCHEDULE_GROUPS_STATS = "schedule_groups_stats" 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Предложите идею для этого проекта 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: 0niel 7 | 8 | --- 9 | 10 | **Связан ли ваш запрос на функцию с проблемой? Пожалуйста, опишите.** 11 | Четкое и краткое описание того, в чем заключается проблема. Например: "Я всегда расстраиваюсь, когда ..." 12 | 13 | **Опишите решение, которое вы хотели бы видеть** 14 | Четкое и краткое описание того, что вы хотите, чтобы произошло. 15 | 16 | **Опишите альтернативы, которые вы рассматривали** 17 | Четкое и краткое описание любых альтернативных решений или функций, которые вы рассмотрели. 18 | 19 | **Дополнительный текст** 20 | Добавьте любой другой тест или скриншоты здесь. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schedule-RTU 2 | 3 | Forked from https://github.com/RTUITLab/Schedule-RTU-API 4 | 5 | Based on https://github.com/YaSlavar/parser_mirea 6 | 7 | 8 | Service for getting jsons with a schedule for a given group of RTU MIREA 9 | 10 | # Build service from Docker image 11 | Requirements: 12 | * Docker 13 | 14 | ## Run container: 15 | 16 | Clone or download this repo and build container 17 | * ```docker build -t schedule-rtu:latest .``` 18 | 19 | Run container 20 | * ```docker run -it -p 5000:5000 schedule-rtu:latest``` 21 | 22 | App running on ```http://0.0.0.0:5000/``` 23 | 24 | You can find api on ```http://localhost:5000/redoc ``` 25 | 26 | ## Deploy 27 | 28 | Run next command to generate swarm stack file 29 | ```bash 30 | # bash 31 | docker-compose -f docker-compose.yml -f docker-compose.production.yml config | sed "s/[0-9]\+\.[0-9]\+$/'\0'/g" >| stack.yml 32 | ``` 33 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from fastapi import FastAPI 4 | from starlette.middleware.cors import CORSMiddleware 5 | 6 | from .api.api_v1.router import router as api_router 7 | from .core.config import API_V1_PREFIX 8 | from .database.database_connection import (close_mongo_connection, 9 | connect_to_mongo) 10 | 11 | app = FastAPI( 12 | title="Schedule API", 13 | debug=os.getenv("DEBUG", False), 14 | description="RTU MIREA Schedule API", 15 | openapi_url=f"{API_V1_PREFIX}/openapi.json", 16 | ) 17 | 18 | app.add_middleware( 19 | CORSMiddleware, 20 | allow_origins=["*"], 21 | allow_credentials=True, 22 | allow_methods=["*"], 23 | allow_headers=["*"], 24 | ) 25 | 26 | app.add_event_handler("startup", connect_to_mongo) 27 | app.add_event_handler("shutdown", close_mongo_connection) 28 | 29 | app.include_router(api_router, prefix=API_V1_PREFIX) 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | mongodb: 5 | image: mongo:latest 6 | hostname: mongodb 7 | container_name: mongodb 8 | volumes: 9 | - mongodb_data:/data/db 10 | deploy: 11 | labels: 12 | - traefik.enable=false 13 | resources: 14 | limits: 15 | cpus: '0.50' 16 | memory: 512M 17 | reservations: 18 | cpus: '0.25' 19 | memory: 256M 20 | 21 | backend: 22 | build: 23 | context: . 24 | args: 25 | INSTALL_DEV: ${INSTALL_DEV-false} 26 | depends_on: 27 | - mongodb 28 | volumes: 29 | - ./docs:/app/docs # schedule docs 30 | env_file: 31 | - .env 32 | ports: 33 | - "5000:5000" 34 | environment: 35 | DEBUG: ${DEBUG-false} 36 | MONGODB_URL: mongodb://mongodb:27017 37 | ENV: ${ENV-production} 38 | DOMAIN: ${DOMAIN-schedule.mirea.ninja} 39 | 40 | volumes: 41 | mongodb_data: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mirea Ninja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/core/schedule_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytz 4 | 5 | 6 | class ScheduleUtils: 7 | @staticmethod 8 | def get_week(date: datetime.datetime = None) -> int: 9 | """Возвращает номер учебной недели по дате 10 | 11 | Args: 12 | date (datetime.datetime, optional): Дата, для которой необходимо получить учебную неделю. 13 | """ 14 | now = ScheduleUtils.now_date() if date is None else date 15 | start_date = ScheduleUtils.get_semester_start(date) 16 | 17 | if now.timestamp() < start_date.timestamp(): 18 | return 1 19 | 20 | week = now.isocalendar()[1] - start_date.isocalendar()[1] 21 | 22 | if now.isocalendar()[2] != 0: 23 | week += 1 24 | 25 | return week 26 | 27 | @staticmethod 28 | def get_semester_start(date: datetime.datetime = None) -> datetime.datetime: 29 | """Возвращает дату начала семестра по дате 30 | 31 | Args: 32 | date (datetime.datetime, optional): Дата для расчёта начала семестра. 33 | """ 34 | date = ScheduleUtils.now_date() if date is None else date 35 | if date.month >= 9: 36 | return ScheduleUtils.get_first_semester() 37 | else: 38 | return ScheduleUtils.get_second_semester() 39 | 40 | @staticmethod 41 | def now_date() -> datetime.datetime: 42 | return datetime.datetime.now(pytz.timezone("Europe/Moscow")) 43 | 44 | @staticmethod 45 | def get_first_semester() -> datetime.datetime: 46 | return datetime.datetime(ScheduleUtils.now_date().year, 9, 1) 47 | 48 | @staticmethod 49 | def get_second_semester() -> datetime.datetime: 50 | return datetime.datetime(ScheduleUtils.now_date().year, 2, 9) 51 | -------------------------------------------------------------------------------- /app/models/schedule.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class WeekModelResponse(BaseModel): 8 | week: int 9 | 10 | 11 | class GroupsListResponse(BaseModel): 12 | count: int 13 | groups: List[str] 14 | 15 | 16 | class LessonModel(BaseModel): 17 | name: str 18 | weeks: List[int] 19 | time_start: str 20 | time_end: str 21 | types: str 22 | teachers: List[str] 23 | rooms: List[str] 24 | 25 | 26 | class ScheduleLessonsModel(BaseModel): 27 | lessons: List[List[LessonModel]] 28 | 29 | 30 | class ScheduleByWeekdaysModelResponse(BaseModel): 31 | monday: ScheduleLessonsModel = Field(..., alias="1", title="monday") 32 | tuesday: ScheduleLessonsModel = Field(..., alias="2", title="tuesday") 33 | wednesday: ScheduleLessonsModel = Field(..., alias="3", title="wednesday") 34 | thursday: ScheduleLessonsModel = Field(..., alias="4", title="thursday") 35 | friday: ScheduleLessonsModel = Field(..., alias="5", title="friday") 36 | saturday: ScheduleLessonsModel = Field(..., alias="6", title="saturday") 37 | 38 | 39 | class ScheduleByWeekdaysModel(BaseModel): 40 | monday: ScheduleLessonsModel 41 | tuesday: ScheduleLessonsModel 42 | wednesday: ScheduleLessonsModel 43 | thursday: ScheduleLessonsModel 44 | friday: ScheduleLessonsModel 45 | saturday: ScheduleLessonsModel 46 | 47 | 48 | class ScheduleModel(BaseModel): 49 | group: str 50 | schedule: ScheduleByWeekdaysModelResponse 51 | 52 | 53 | class TeacherLessonModel(BaseModel): 54 | group: str 55 | weekday: int 56 | lesson_number: int 57 | lesson: LessonModel 58 | 59 | 60 | class RoomLessonModel(BaseModel): 61 | room: str 62 | weekday: int 63 | lesson: LessonModel 64 | 65 | 66 | class RoomScheduleModel(BaseModel): 67 | schedules: List[RoomLessonModel] 68 | 69 | 70 | class TeacherSchedulesModelResponse(BaseModel): 71 | schedules: List[TeacherLessonModel] 72 | 73 | 74 | class ScheduleUpdateModel(BaseModel): 75 | groups: List[str] 76 | updated_at: datetime.datetime 77 | 78 | 79 | class GroupStatsModel(BaseModel): 80 | group: str 81 | received: int 82 | -------------------------------------------------------------------------------- /docker-compose.production.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | proxy: 5 | image: jwilder/nginx-proxy 6 | ports: 7 | - "80:80" 8 | - "443:443" 9 | volumes: 10 | - conf:/etc/nginx/conf.d 11 | - vhost:/etc/nginx/vhost.d 12 | - dhparam:/etc/nginx/dhparam 13 | - certs:/etc/nginx/certs:ro 14 | - /var/run/docker.sock:/tmp/docker.sock:ro 15 | - acme:/etc/acme.sh 16 | - html:/usr/share/nginx/html 17 | networks: 18 | - proxy 19 | restart: always 20 | 21 | letsencrypt: 22 | image: nginxproxy/acme-companion:latest 23 | volumes_from: 24 | - proxy 25 | volumes: 26 | - certs:/etc/nginx/certs:rw 27 | - /var/run/docker.sock:/var/run/docker.sock:ro 28 | - acme:/etc/acme.sh 29 | restart: always 30 | 31 | mongodb: 32 | image: mongo:latest 33 | hostname: mongodb 34 | container_name: mongodb 35 | volumes: 36 | - mongodb_data:/data/db 37 | deploy: 38 | resources: 39 | limits: 40 | cpus: '0.50' 41 | memory: 512M 42 | reservations: 43 | cpus: '0.25' 44 | memory: 256M 45 | networks: 46 | - default 47 | 48 | backend: 49 | build: 50 | context: . 51 | args: 52 | INSTALL_DEV: ${INSTALL_DEV-true} 53 | command: /start-reload.sh 54 | env_file: 55 | - .env 56 | volumes: 57 | - .:/app 58 | - ./docs:/app/docs # schedule docs 59 | environment: 60 | VIRTUAL_HOST: ${DOMAIN-schedule.mirea.ninja} 61 | DEBUG: false 62 | MONGODB_URL: mongodb://mongodb:27017 63 | ENV: production 64 | DOMAIN: ${DOMAIN-schedule.mirea.ninja} 65 | LETSENCRYPT_HOST: ${DOMAIN-schedule.mirea.ninja} 66 | VIRTUAL_PORT: 5000 67 | PORT: 5000 68 | restart: always 69 | networks: 70 | - proxy 71 | - default 72 | 73 | cron: 74 | image: alpine:latest 75 | volumes: 76 | - .:/app 77 | - ./cron:/etc/cron.d/my-cron 78 | environment: 79 | SECRET_REFRESH_KEY: ${SECRET_REFRESH_KEY} 80 | command: [ "sh", "-c", "crond -f -l 8 -L /dev/stdout && tail -f /dev/null" ] 81 | restart: always 82 | networks: 83 | - proxy 84 | 85 | volumes: 86 | mongodb_data: 87 | conf: 88 | vhost: 89 | dhparam: 90 | certs: 91 | acme: 92 | html: 93 | 94 | networks: 95 | proxy: 96 | external: 97 | true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | documents/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | .vscode/ 132 | migrations/ 133 | .DS_store 134 | 135 | # Parser 136 | /output 137 | /xls 138 | *.db 139 | *.xls 140 | *.xlsx 141 | 142 | stack.yml -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 6 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /app/api/api_v1/endpoints/schedule.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, Path 4 | from fastapi.responses import JSONResponse 5 | from starlette.exceptions import HTTPException 6 | from starlette.status import HTTP_404_NOT_FOUND 7 | 8 | from app.core.config import SECRET_REFRESH_KEY 9 | from app.core.schedule_utils import ScheduleUtils 10 | from app.crud.schedule import (find_room, find_teacher, get_full_schedule, 11 | get_groups, get_groups_stats, 12 | update_group_stats) 13 | from app.database.database import AsyncIOMotorClient, get_database 14 | from app.models.schedule import (GroupsListResponse, GroupStatsModel, 15 | RoomScheduleModel, ScheduleModel, 16 | TeacherSchedulesModelResponse, 17 | WeekModelResponse) 18 | from app.schedule_parser.excel import parse_schedule 19 | 20 | router = APIRouter() 21 | 22 | 23 | @router.post( 24 | "/refresh", 25 | description="Refresh shedule", 26 | response_description="Return 'ok' after updating", 27 | ) 28 | async def refresh( 29 | secret_key: str = None, db: AsyncIOMotorClient = Depends(get_database) 30 | ): 31 | if ( 32 | SECRET_REFRESH_KEY is None 33 | or SECRET_REFRESH_KEY == "" 34 | or SECRET_REFRESH_KEY == "None" 35 | ): 36 | await parse_schedule(db) 37 | return JSONResponse({"status": "ok"}) 38 | elif secret_key == SECRET_REFRESH_KEY: 39 | await parse_schedule(db) 40 | return JSONResponse({"status": "ok"}) 41 | return JSONResponse({"status": "Invalid secret API key"}) 42 | 43 | 44 | @router.get( 45 | "/schedule/{group}/full_schedule", 46 | response_description="Return full schedule of one group", 47 | response_model=ScheduleModel, 48 | ) 49 | async def full_schedule( 50 | group: str = Path(..., min_length=10), 51 | db: AsyncIOMotorClient = Depends(get_database), 52 | ): 53 | schedule = await get_full_schedule(db, group) 54 | 55 | if not schedule: 56 | raise HTTPException( 57 | status_code=HTTP_404_NOT_FOUND, 58 | detail=f"Schedule for group '{group}' not found", 59 | ) 60 | 61 | await update_group_stats(db, group) 62 | 63 | return schedule 64 | 65 | 66 | @router.get( 67 | "/schedule/groups", 68 | response_description="List of all groups", 69 | response_model=GroupsListResponse, 70 | ) 71 | async def groups_list(db: AsyncIOMotorClient = Depends(get_database)): 72 | groups = await get_groups(db) 73 | 74 | if not groups: 75 | raise HTTPException( 76 | status_code=HTTP_404_NOT_FOUND, 77 | detail="Groups not found", 78 | ) 79 | 80 | if len(groups) == 0: 81 | raise HTTPException( 82 | status_code=HTTP_404_NOT_FOUND, 83 | detail="Groups are empty. Maybe schedule is not parsed yet", 84 | ) 85 | 86 | return GroupsListResponse(groups=groups, count=len(groups)) 87 | 88 | 89 | @router.get( 90 | "/schedule/current_week", 91 | response_description="Get current week", 92 | response_model=WeekModelResponse, 93 | ) 94 | async def current_week(): 95 | return WeekModelResponse(week=ScheduleUtils.get_week(ScheduleUtils.now_date())) 96 | 97 | 98 | @router.get( 99 | "/schedule/teacher/{teacher_name}", 100 | response_description="Find teacher schedule by teacher name", 101 | response_model=TeacherSchedulesModelResponse, 102 | ) 103 | async def teacher_schedule( 104 | teacher_name: str = Path(...), db: AsyncIOMotorClient = Depends(get_database) 105 | ): 106 | schedule = await find_teacher(db, teacher_name) 107 | 108 | if not schedule: 109 | raise HTTPException( 110 | status_code=HTTP_404_NOT_FOUND, 111 | detail=f"Teacher with name {teacher_name} not found", 112 | ) 113 | 114 | return schedule 115 | 116 | 117 | @router.get( 118 | "/schedule/room/{room_name}", 119 | response_description="Find room schedule by room name", 120 | response_model=RoomScheduleModel, 121 | ) 122 | async def room_schedule( 123 | room_name: str = Path(...), db: AsyncIOMotorClient = Depends(get_database) 124 | ): 125 | schedule = await find_room(db, room_name) 126 | 127 | if not schedule: 128 | raise HTTPException( 129 | status_code=HTTP_404_NOT_FOUND, 130 | detail=f"Room with name {room_name} not found", 131 | ) 132 | 133 | return schedule 134 | 135 | 136 | @router.get( 137 | "/schedule/groups_stats/", 138 | response_description="Get statistics of requests to group schedules", 139 | response_model=List[GroupStatsModel], 140 | ) 141 | async def groups_stats(db: AsyncIOMotorClient = Depends(get_database)): 142 | stats = await get_groups_stats(db) 143 | 144 | if not stats: 145 | raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Stats not found") 146 | 147 | return stats 148 | -------------------------------------------------------------------------------- /app/schedule_parser/excel.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | from collections import defaultdict 6 | from concurrent.futures import ThreadPoolExecutor, as_completed 7 | from itertools import groupby 8 | from typing import Generator 9 | 10 | import rtu_schedule_parser.utils.academic_calendar as academic_calendar 11 | from motor.motor_asyncio import AsyncIOMotorClient 12 | from rtu_schedule_parser import ExcelScheduleParser, LessonsSchedule 13 | from rtu_schedule_parser.constants import Degree, Institute, ScheduleType 14 | from rtu_schedule_parser.downloader import ScheduleDownloader 15 | from rtu_schedule_parser.schedule import (ExamsSchedule, LessonEmpty, 16 | LessonsSchedule) 17 | 18 | from ..crud.schedule import save_schedule 19 | from ..models.schedule import (LessonModel, ScheduleByWeekdaysModel, 20 | ScheduleLessonsModel) 21 | 22 | logging.basicConfig(level=logging.INFO) 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def _parse_document(doc) -> list[LessonsSchedule | ExamsSchedule]: 27 | logger.info(f"Обработка документа: {doc}") 28 | try: 29 | parser = ExcelScheduleParser(doc[0], doc[1], doc[2], doc[3]) 30 | return parser.parse(force=True).get_schedule() 31 | except Exception: 32 | logger.error(f"Парсинг завершился с ошибкой ({doc})") 33 | 34 | 35 | def _get_documents_by_json(docs_dir: str) -> list: 36 | # Формат json: 37 | # [ 38 | # { 39 | # "file": "./расписание колледж.xlsx", 40 | # "institute": "КПК", 41 | # "type": 1, 42 | # "degree": 4 43 | # } 44 | # ] 45 | try: 46 | with open(os.path.join(docs_dir, "files.json"), "r") as f: 47 | files = json.load(f) 48 | 49 | documents = [] 50 | 51 | for file in files: 52 | file_path = os.path.join(docs_dir, file["file"]) 53 | logger.info(f"Файл {file['file']} добавлен на парсинг из `files.json`") 54 | 55 | if not os.path.exists(file_path): 56 | logger.error( 57 | f"Файл {file['file']} не найден. См. `files.json`. Пропускаем..." 58 | ) 59 | continue 60 | 61 | documents.append( 62 | ( 63 | file_path, 64 | academic_calendar.get_period(datetime.datetime.now()), 65 | Institute.get_by_short_name(file["institute"]), 66 | Degree(file["degree"]), 67 | ) 68 | ) 69 | 70 | return documents 71 | 72 | except FileNotFoundError: 73 | logger.info("`files.json` не найден. Пропускаем...") 74 | return [] 75 | 76 | 77 | def _get_documents() -> list: 78 | """Get documents for specified institute and degree""" 79 | docs_dir = os.path.dirname(os.path.abspath(__file__)) 80 | docs_dir = os.path.join(docs_dir, "docs") 81 | 82 | downloader = ScheduleDownloader(base_file_dir=docs_dir) 83 | 84 | if os.path.exists(docs_dir): 85 | for root, dirs, files in os.walk(docs_dir, topdown=False): 86 | for name in files: 87 | os.remove(os.path.join(root, name)) 88 | for name in dirs: 89 | os.rmdir(os.path.join(root, name)) 90 | else: 91 | os.mkdir(docs_dir) 92 | 93 | all_docs = downloader.get_documents( 94 | specific_schedule_types={ScheduleType.SEMESTER}, 95 | specific_degrees={Degree.BACHELOR, Degree.MASTER, Degree.PHD}, 96 | specific_institutes=set(Institute), 97 | ) 98 | 99 | logger.info(f"Найдено {len(all_docs)} документов для парсинга") 100 | 101 | downloaded = downloader.download_all(all_docs) 102 | 103 | # сначала документы с Degree.PHD, потом Degree.MASTER, потом Degree.BACHELOR (то есть по убыванию) 104 | downloaded = sorted(downloaded, key=lambda x: x[0].degree, reverse=True) 105 | 106 | logger.info(f"Скачано {len(downloaded)} файлов") 107 | 108 | documents = [ 109 | (doc[1], doc[0].period, doc[0].institute, doc[0].degree) for doc in downloaded 110 | ] 111 | 112 | docs_dir = os.path.dirname(os.path.abspath(__file__)) 113 | docs_dir = os.path.join(docs_dir, "..", "..", "docs") 114 | 115 | documents += _get_documents_by_json(docs_dir) 116 | 117 | return documents 118 | 119 | 120 | def _parse() -> Generator[list[LessonsSchedule | ExamsSchedule], None, None]: 121 | with ThreadPoolExecutor(max_workers=4) as executor: 122 | tasks = [] 123 | for doc in _get_documents(): 124 | task = executor.submit(_parse_document, doc) 125 | tasks.append(task) 126 | 127 | for future in as_completed(tasks): 128 | if ( 129 | schedules := future.result() 130 | ): # type: list[LessonsSchedule | ExamsSchedule] 131 | groups = {schedule.group for schedule in schedules} 132 | logger.info(f"Получено расписание документа. Группы: {groups}") 133 | yield schedules 134 | 135 | 136 | async def parse_schedule(conn: AsyncIOMotorClient) -> None: 137 | """Parse parser and save it to database""" 138 | 139 | for schedules in _parse(): 140 | for schedule in schedules: 141 | try: 142 | # В модели расписания 1-6 -- дни недели. По ключу 1 лежит расписание на понедельник, 143 | # 2 -- на вторник и т.д. "lessons" содержит список списков пар. Первый список относится к номеру пары, 144 | # второй список -- различные пары в это время 145 | lessons = schedule.lessons 146 | 147 | lessons_by_weekdays = defaultdict(list) 148 | 149 | for lesson in lessons: 150 | weekday = lesson.weekday.value[0] 151 | 152 | if type(lesson) is not LessonEmpty: 153 | room = lesson.room 154 | if room: 155 | room = [ 156 | f"{room.name} ({room.campus.short_name})" 157 | if room.campus is not None 158 | else room.name 159 | ] 160 | else: 161 | room = [] 162 | 163 | name = lesson.name 164 | 165 | if lesson.subgroup: 166 | name += f" ({lesson.subgroup} подгруппа)" 167 | 168 | lesson = LessonModel( 169 | name=name, 170 | weeks=lesson.weeks, 171 | time_start=lesson.time_start.strftime("%-H:%M"), 172 | time_end=lesson.time_end.strftime("%-H:%M"), 173 | types=lesson.type.value if lesson.type else "", 174 | teachers=lesson.teachers, 175 | rooms=room, 176 | ) 177 | 178 | lessons_by_weekdays[str(weekday)].append(lesson) 179 | 180 | # Объединяем пары, которые проходят в одно и то же время и в один день недели 181 | for weekday, lessons in lessons_by_weekdays.items(): 182 | lessons_by_weekdays[weekday] = [ 183 | list(g) for k, g in groupby(lessons, lambda x: x.time_start) 184 | ] 185 | 186 | # Удаляем пустые пары (LessonEmpty), конвертируем в ScheduleLessonsModel 187 | for weekday, lessons in lessons_by_weekdays.items(): 188 | lessons_by_weekdays[weekday] = ScheduleLessonsModel( 189 | lessons=[ 190 | [ 191 | lesson 192 | for lesson in lessons_by_weekday 193 | if type(lesson) is not LessonEmpty 194 | ] 195 | for lessons_by_weekday in lessons 196 | ] 197 | ) 198 | 199 | by_weekdays = ScheduleByWeekdaysModel( 200 | monday=lessons_by_weekdays["1"], 201 | tuesday=lessons_by_weekdays["2"], 202 | wednesday=lessons_by_weekdays["3"], 203 | thursday=lessons_by_weekdays["4"], 204 | friday=lessons_by_weekdays["5"], 205 | saturday=lessons_by_weekdays["6"], 206 | ) 207 | 208 | await save_schedule(conn, schedule.group, by_weekdays) 209 | 210 | except Exception as e: 211 | logger.error(f"Error while parsing schedule: {e}") 212 | -------------------------------------------------------------------------------- /app/crud/schedule.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import List 3 | 4 | from app.core.config import (DATABASE_NAME, SCHEDULE_COLLECTION_NAME, 5 | SCHEDULE_GROUPS_STATS, 6 | SCHEDULE_UPDATES_COLLECTION) 7 | from app.database.database import AsyncIOMotorClient 8 | from app.models.schedule import (GroupStatsModel, LessonModel, RoomLessonModel, 9 | RoomScheduleModel, 10 | ScheduleByWeekdaysModelResponse, 11 | ScheduleModel, ScheduleUpdateModel, 12 | TeacherLessonModel, 13 | TeacherSchedulesModelResponse) 14 | 15 | loop = asyncio.get_event_loop() 16 | 17 | 18 | async def save_schedule( 19 | conn: AsyncIOMotorClient, group: str, schedule: ScheduleByWeekdaysModelResponse 20 | ): 21 | await conn[DATABASE_NAME][SCHEDULE_COLLECTION_NAME].replace_one( 22 | {"group": group}, {"group": group, "schedule": schedule.dict()}, upsert=True 23 | ) 24 | 25 | 26 | async def get_full_schedule(conn: AsyncIOMotorClient, group_name: str) -> ScheduleModel: 27 | """Получение полного расписания для выбранной группы""" 28 | schedule = await conn[DATABASE_NAME][SCHEDULE_COLLECTION_NAME].find_one( 29 | {"group": group_name}, {"_id": 0} 30 | ) 31 | 32 | if schedule: 33 | schedule_with_num_keys = { 34 | "1": schedule["schedule"]["monday"], 35 | "2": schedule["schedule"]["tuesday"], 36 | "3": schedule["schedule"]["wednesday"], 37 | "4": schedule["schedule"]["thursday"], 38 | "5": schedule["schedule"]["friday"], 39 | "6": schedule["schedule"]["saturday"], 40 | } 41 | return ScheduleModel(group=schedule["group"], schedule=schedule_with_num_keys) 42 | 43 | 44 | async def get_groups(conn: AsyncIOMotorClient) -> List[str]: 45 | """Получение списка всех групп, для которых доступно расписание""" 46 | cursor = conn[DATABASE_NAME][SCHEDULE_COLLECTION_NAME].find( 47 | {}, {"group": 1, "_id": 0} 48 | ) 49 | groups = await cursor.to_list(None) 50 | if groups := [group["group"] for group in groups]: 51 | return groups 52 | 53 | 54 | async def find_teacher( 55 | conn: AsyncIOMotorClient, teacher_name: str 56 | ) -> TeacherSchedulesModelResponse: 57 | # todo: [А-Яа-я]+\s+[А-Яа-я]\.\s*[А-Я]\. 58 | request_data = { 59 | "$or": [ 60 | { 61 | "schedule.monday.lessons": { 62 | "$elemMatch": { 63 | "$elemMatch": { 64 | "teachers": {"$regex": teacher_name, "$options": "i"} 65 | } 66 | } 67 | } 68 | }, 69 | { 70 | "schedule.tuesday.lessons": { 71 | "$elemMatch": { 72 | "$elemMatch": { 73 | "teachers": {"$regex": teacher_name, "$options": "i"} 74 | } 75 | } 76 | } 77 | }, 78 | { 79 | "schedule.wednesday.lessons": { 80 | "$elemMatch": { 81 | "$elemMatch": { 82 | "teachers": {"$regex": teacher_name, "$options": "i"} 83 | } 84 | } 85 | } 86 | }, 87 | { 88 | "schedule.thursday.lessons": { 89 | "$elemMatch": { 90 | "$elemMatch": { 91 | "teachers": {"$regex": teacher_name, "$options": "i"} 92 | } 93 | } 94 | } 95 | }, 96 | { 97 | "schedule.friday.lessons": { 98 | "$elemMatch": { 99 | "$elemMatch": { 100 | "teachers": {"$regex": teacher_name, "$options": "i"} 101 | } 102 | } 103 | } 104 | }, 105 | { 106 | "schedule.saturday.lessons": { 107 | "$elemMatch": { 108 | "$elemMatch": { 109 | "teachers": {"$regex": teacher_name, "$options": "i"} 110 | } 111 | } 112 | } 113 | }, 114 | ] 115 | } 116 | 117 | cursor = conn[DATABASE_NAME][SCHEDULE_COLLECTION_NAME].find( 118 | request_data, {"_id": 0} 119 | ) 120 | schedules = await cursor.to_list(None) 121 | 122 | result = [] 123 | 124 | for schedule in schedules: 125 | schedule_with_num_keys = { 126 | "1": schedule["schedule"]["monday"], 127 | "2": schedule["schedule"]["tuesday"], 128 | "3": schedule["schedule"]["wednesday"], 129 | "4": schedule["schedule"]["thursday"], 130 | "5": schedule["schedule"]["friday"], 131 | "6": schedule["schedule"]["saturday"], 132 | } 133 | 134 | for i in range(1, 7): 135 | lessons_in_day = schedule_with_num_keys[str(i)]["lessons"] 136 | for lesson_num in range(len(lessons_in_day)): 137 | for lesson in lessons_in_day[lesson_num]: 138 | for teacher in lesson["teachers"]: 139 | if teacher.lower().find(teacher_name.lower()) != -1: 140 | teacher_lesson = TeacherLessonModel( 141 | group=schedule["group"], 142 | weekday=i, 143 | lesson_number=lesson_num, 144 | lesson=LessonModel(**lesson), 145 | ) 146 | result.append(teacher_lesson) 147 | 148 | teacher_schedule = TeacherSchedulesModelResponse(schedules=result) 149 | 150 | if result: 151 | return teacher_schedule 152 | 153 | 154 | async def find_room(conn: AsyncIOMotorClient, room_name: str) -> RoomScheduleModel: 155 | request_data = { 156 | "$or": [ 157 | { 158 | "schedule.monday.lessons": { 159 | "$elemMatch": { 160 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 161 | } 162 | } 163 | }, 164 | { 165 | "schedule.tuesday.lessons": { 166 | "$elemMatch": { 167 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 168 | } 169 | } 170 | }, 171 | { 172 | "schedule.wednesday.lessons": { 173 | "$elemMatch": { 174 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 175 | } 176 | } 177 | }, 178 | { 179 | "schedule.thursday.lessons": { 180 | "$elemMatch": { 181 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 182 | } 183 | } 184 | }, 185 | { 186 | "schedule.friday.lessons": { 187 | "$elemMatch": { 188 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 189 | } 190 | } 191 | }, 192 | { 193 | "schedule.saturday.lessons": { 194 | "$elemMatch": { 195 | "$elemMatch": {"room": {"$regex": room_name, "$options": "i"}} 196 | } 197 | } 198 | }, 199 | ] 200 | } 201 | 202 | cursor = conn[DATABASE_NAME][SCHEDULE_COLLECTION_NAME].find( 203 | request_data, {"_id": 0} 204 | ) 205 | schedules = await cursor.to_list(None) 206 | 207 | result = [] 208 | 209 | for schedule in schedules: 210 | schedule_with_num_keys = { 211 | "1": schedule["schedule"]["monday"], 212 | "2": schedule["schedule"]["tuesday"], 213 | "3": schedule["schedule"]["wednesday"], 214 | "4": schedule["schedule"]["thursday"], 215 | "5": schedule["schedule"]["friday"], 216 | "6": schedule["schedule"]["saturday"], 217 | } 218 | 219 | for i in range(1, 7): 220 | lessons_in_day = schedule_with_num_keys[str(i)]["lessons"] 221 | for lesson_num in range(len(lessons_in_day)): 222 | for lesson in lessons_in_day[lesson_num]: 223 | if lesson["room"].lower().find(room_name.lower()) != -1: 224 | room_lesson = RoomLessonModel( 225 | group=schedule["group"], 226 | weekday=i, 227 | lesson_number=lesson_num, 228 | lesson=LessonModel(**lesson), 229 | ) 230 | result.append(room_lesson) 231 | 232 | room_schedule = RoomScheduleModel(schedules=result) 233 | 234 | if result: 235 | return room_schedule 236 | 237 | 238 | async def update_schedule_updates( 239 | conn: AsyncIOMotorClient, updates: List[ScheduleUpdateModel] 240 | ): 241 | for update in updates: 242 | groups_list = [] 243 | request_to_db = {"$or": groups_list} 244 | groups_list.extend( 245 | {"groups": {"$elemMatch": {"$regex": group}}} for group in update.groups 246 | ) 247 | 248 | update_in_db = await conn[DATABASE_NAME][SCHEDULE_UPDATES_COLLECTION].find_one( 249 | request_to_db 250 | ) 251 | 252 | if update_in_db: 253 | await conn[DATABASE_NAME][SCHEDULE_UPDATES_COLLECTION].update_one( 254 | {"_id": update_in_db["_id"]}, {"$set": update.dict()} 255 | ) 256 | else: 257 | await conn[DATABASE_NAME][SCHEDULE_UPDATES_COLLECTION].insert_one( 258 | update.dict() 259 | ) 260 | 261 | 262 | async def get_all_schedule_updates( 263 | conn: AsyncIOMotorClient, 264 | ) -> List[ScheduleUpdateModel]: 265 | cursor = conn[DATABASE_NAME][SCHEDULE_UPDATES_COLLECTION].find({}, {"_id": 0}) 266 | 267 | updates = await cursor.to_list(None) 268 | if updates := [ScheduleUpdateModel(**update) for update in updates]: 269 | return updates 270 | 271 | 272 | async def get_schedule_update_by_group( 273 | conn: AsyncIOMotorClient, group: str 274 | ) -> ScheduleUpdateModel: 275 | update = await conn[DATABASE_NAME][SCHEDULE_UPDATES_COLLECTION].find_one( 276 | {"groups": {"$elemMatch": {"$regex": group}}}, {"_id": 0} 277 | ) 278 | 279 | if update: 280 | return ScheduleUpdateModel(**update) 281 | 282 | 283 | async def update_group_stats(conn: AsyncIOMotorClient, group: str): 284 | update = await conn[DATABASE_NAME][SCHEDULE_GROUPS_STATS].update_one( 285 | {"group": group}, {"$inc": {"received": 1}}, upsert=True 286 | ) 287 | 288 | 289 | async def get_groups_stats(conn: AsyncIOMotorClient) -> List[GroupStatsModel]: 290 | cursor = conn[DATABASE_NAME][SCHEDULE_GROUPS_STATS].find({}, {"_id": 0}) 291 | 292 | groups_stats = await cursor.to_list(None) 293 | if groups_stats := [GroupStatsModel(**stats) for stats in groups_stats]: 294 | return groups_stats 295 | --------------------------------------------------------------------------------