├── .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 |
4 |
5 |
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 |
10 |
11 |
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 |
--------------------------------------------------------------------------------