Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .docker/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
COMPOSE_PROJECT_NAME=ssau-schedule

REDIS_HOST=schedule-redis
BACKEND_HOST=backend
11 changes: 11 additions & 0 deletions .docker/DockerfileBackend
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-slim

ENV WORKDIR /app
WORKDIR $WORKDIR

COPY poetry.lock pyproject.toml $WORKDIR/
ENV POETRY_VERSION=1.8.3

RUN python3 -m pip install poetry==$POETRY_VERSION && \
poetry config virtualenvs.create false && \
poetry install --no-cache --no-root --only main
11 changes: 11 additions & 0 deletions .docker/DockerfileFrontend
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.11-slim

ENV WORKDIR /app
WORKDIR $WORKDIR

COPY poetry.lock pyproject.toml $WORKDIR/
ENV POETRY_VERSION=1.8.3

RUN python3 -m pip install poetry==$POETRY_VERSION && \
poetry config virtualenvs.create false && \
poetry install --no-cache --no-root --only main
53 changes: 53 additions & 0 deletions .docker/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
services:
backend:
image: backend
restart: unless-stopped
pull_policy: always
build:
context: ..
dockerfile: .docker/DockerfileBackend
command: ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--reload"]
ports:
- "8000:8000"
env_file:
- .env
volumes:
- ../backend:/app/backend

frontend:
image: frontend
restart: unless-stopped
pull_policy: always
build:
context: ..
dockerfile: .docker/DockerfileFrontend
command: [ "uvicorn", "frontend.main:app", "--host", "0.0.0.0", "--reload" ]
ports:
- "8001:8000"
env_file:
- .env
volumes:
- ../frontend:/app/frontend
depends_on:
backend:
condition: service_started

redis:
image: redis:7-alpine
container_name: schedule-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
env_file:
- .env
command: redis-server --appendonly yes
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 5s
timeout: 3s
retries: 3

volumes:
redis_data:
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
*__pycache__
25 changes: 25 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import uvicorn
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from backend.src.schedule.router import router as schedule_router

app = FastAPI(title="SSAUScheduleBackend")

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(schedule_router)

if __name__ == "__main__":
uvicorn.run(
__name__ + ":app",
host='127.0.0.1',
port=8000,
reload=True
)
11 changes: 11 additions & 0 deletions backend/src/connections/redis/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class RedisConfig(BaseSettings):
host: str = "localhost"
port: int = 6379
db: int = 0

model_config = SettingsConfigDict(
env_prefix="redis_"
)
22 changes: 22 additions & 0 deletions backend/src/connections/redis/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Any, Optional

from redis.asyncio import Redis

from backend.src.connections.redis.config import RedisConfig


class RedisConnection:
def __init__(self, config: RedisConfig):
self.config = config
self.client: Optional[Redis] = None

async def __aenter__(self) -> Redis:
self.client = Redis(
host=self.config.host,
port=self.config.port,
db=self.config.db
)
return self.client

async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
await self.client.close()
5 changes: 5 additions & 0 deletions backend/src/schedule/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic_settings import BaseSettings


class GatewayConfig(BaseSettings):
api_base: str = "https://ssau.ru"
37 changes: 37 additions & 0 deletions backend/src/schedule/depends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Annotated

from fastapi import Depends

from backend.src.connections.redis.config import RedisConfig
from backend.src.connections.redis.connection import RedisConnection
from backend.src.schedule.config import GatewayConfig
from backend.src.schedule.gateway import ScheduleGateway
from backend.src.schedule.repository import ScheduleRepository
from backend.src.schedule.service import ScheduleService


async def get_redis() -> RedisConnection:
yield RedisConnection(RedisConfig())

RedisDepends = Annotated[RedisConnection, Depends(get_redis)]


async def get_schedule_repository(redis: RedisDepends) -> ScheduleRepository:
yield ScheduleRepository(redis)

ScheduleRepositoryDepends = Annotated[ScheduleRepository, Depends(get_schedule_repository)]


async def get_schedule_gateway() -> ScheduleGateway:
yield ScheduleGateway(GatewayConfig())

ScheduleGatewayDepends = Annotated[ScheduleGateway, Depends(get_schedule_gateway)]


async def get_schedule_service(
repository: ScheduleRepositoryDepends,
gateway: ScheduleGatewayDepends
) -> ScheduleService:
yield ScheduleService(gateway, repository)

ScheduleServiceDepends = Annotated[ScheduleService, Depends(get_schedule_service)]
149 changes: 149 additions & 0 deletions backend/src/schedule/gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import re
from typing import Optional

import httpx
from bs4 import BeautifulSoup

from backend.src.schedule.config import GatewayConfig
from backend.src.schemas.lesson import Lesson, LessonType
from backend.src.schemas.week import Week


class ScheduleGateway:
def __init__(self, config: GatewayConfig):
self.api_base = config.api_base
self.timeout = httpx.Timeout(30.0, connect=10.0)

async def get_faculties_ids(self) -> list[str]:
res = []
async with httpx.AsyncClient(timeout=self.timeout) as session:
faculties_page = await session.get(f"{self.api_base}/rasp")

soup = BeautifulSoup(faculties_page.content.decode("utf-8"), 'html.parser')

faculty_block = soup.find('div', class_='faculties')

for item in faculty_block.find_all('div', class_='faculties__item'):
link_tag = item.find('a')
link = link_tag.get("href")

match = re.search(r'/faculty/(\d+)', link)
faculty_id = match.group(1)

res.append(faculty_id)
return res

async def get_groups_ids(self) -> dict[str, int]:
faculties_ids = await self.get_faculties_ids()

res = dict()

async with httpx.AsyncClient(timeout=self.timeout) as session:
for faculty_id in faculties_ids:
for course_num in range(1, 7):
course_link = f"{self.api_base}/rasp/faculty/{faculty_id}?course={course_num}"

resp = await session.get(course_link)
if resp.status_code == 200:
course_page = resp.content.decode("utf-8")

soup = BeautifulSoup(course_page, 'html.parser')

group_elems = soup.find_all(
"a",
class_="btn-text group-catalog__group"
)
course_dict = dict()
for group in group_elems:
name = group.find("span").text
id_ = re.search(r'groupId=(\d+)', group.get("href")).group(1)

course_dict[name] = int(id_)
res.update(course_dict)
else:
print(resp.content)
return res

async def get_week(
self,
id_: str,
week_num: Optional[int] = None,
is_group_id: bool = True
) -> Week:
if is_group_id:
schedule_link = f"{self.api_base}/rasp?groupId={id_}"
else:
schedule_link = f"{self.api_base}/rasp?staffId={id_}"

return await self._get_week(schedule_link, week_num)

async def _get_week(self, schedule_link: str, week_num: Optional[int] = None) -> Week:
week = Week()

if week_num:
week.num = week_num
schedule_link += f"&selectedWeek={week_num}"

async with httpx.AsyncClient(timeout=self.timeout) as session:
resp = await session.get(schedule_link)
schedule_page = resp.content.decode("utf-8")

soup = BeautifulSoup(schedule_page, 'html.parser')

week_num = soup.find("span", class_="week-nav-current_week").text.strip()
week.num = int(week_num.split()[0].strip())

item_cnt = 0
for item in soup.find_all("div", class_="schedule__item"):
if "schedule__head" in item.get("class"):
continue

lessons_schemas = []
lessons = item.find_all("div", class_="schedule__lesson")
for lesson in lessons:
type_ = lesson.find("div", class_="schedule__lesson-type-chip").text.strip()
name = lesson.find("div", class_="schedule__discipline").text.strip()

teacher_div = lesson.find("div", class_="schedule__teacher")
teacher = None
teacher_id = None
if teacher_div:
teacher = teacher_div.text.strip()

teacher_id=None
teacher_link = teacher_div.find("a", class_="caption-text")
if teacher_link:
teacher_id = re.search(
r'staffId=(\d+)',
teacher_link.get("href")
).group(1)


place = lesson.find("div", class_="schedule__place").text.strip()
groups = [g.text.strip() for g in lesson.find_all("a", class_="schedule__group")]

caption = lesson.find("span", class_="caption-text")
if caption:
caption = caption.text.strip()

lesson_schema = Lesson(
type=LessonType(type_),
lesson_indx=item_cnt % 6,
name=name,
teacher=teacher,
teacher_id=teacher_id,
place=place,
groups=groups,
caption=caption
)
lessons_schemas.append(lesson_schema)

week[item_cnt%6][item_cnt//6] = lessons_schemas

item_cnt += 1

return week




27 changes: 27 additions & 0 deletions backend/src/schedule/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Any

from backend.src.connections.redis.connection import RedisConnection


class ScheduleRepository:
def __init__(self, redis: RedisConnection):
self.redis = redis

async def set(self, key: Any, value: Any) -> None:
async with self.redis as client:
await client.set(key, value)

async def set_keys(self, data: dict) -> None:
for key, val in data.items():
await self.set(key, val)

async def get_keys(self) -> list[str]:
async with self.redis as client:
keys = await client.keys("*")
keys = [key.decode("utf-8") for key in keys]
return keys

async def get(self, key: Any) -> bytes:
async with self.redis as client:
return await client.get(key)

Loading