Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logging #105

Merged
merged 2 commits into from
May 9, 2024
Merged
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
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ services:
APP_API_ADDRESS: 0.0.0.0
APP_API_PORT: 80

APP_LOG_FORMAT: plain

APP_API_SECRET_KEY: ${SECRET_KEY}

APP_PG_DSN: postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}
Expand Down
2 changes: 1 addition & 1 deletion lms/admin/views/pages/distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def _call_create_distribution(self, params: DistributionParams) -> None:
async def _get_product_list(self) -> Sequence[Product]:
async with self.session_factory() as session:
now = datetime.now()
return await ProductRepository(session=session).read_actual_list(dt=now)
return await ProductRepository(session=session).get_actual_list(dt=now)

@cached_property
def token(self) -> str:
Expand Down
6 changes: 3 additions & 3 deletions lms/db/repositories/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ async def find_product_by_offer(self, offer_id: int) -> Product:
except NoResultFound as e:
raise ProductNotFoundError from e

async def read_actual_list(self, dt: datetime) -> Sequence[Product]:
async def get_actual_list(self, dt: datetime) -> Sequence[Product]:
stmt = (
select(ProductDb)
.where(ProductDb.end_date > dt.date())
.order_by(ProductDb.created_at)
)
objs = (await self._session.scalars(stmt)).all()
return [Product.model_validate(obj) for obj in objs]
result = await self._session.scalars(stmt)
return [Product.model_validate(obj) for obj in result]
29 changes: 24 additions & 5 deletions lms/db/repositories/reviewer.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
from collections.abc import Sequence

from sqlalchemy import select
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncSession

from lms.db.models import Reviewer as ReviewerDb
from lms.db.repositories.base import Repository
from lms.generals.models.reviewer import Reviewer
from lms.generals.models.reviewer import CreateReviewerModel, Reviewer


class ReviewerRepository(Repository[ReviewerDb]):
def __init__(self, session: AsyncSession) -> None:
super().__init__(model=ReviewerDb, session=session)

async def get_by_subject_id(
async def create(self, new_reviewer: CreateReviewerModel) -> Reviewer:
query = (
insert(ReviewerDb)
.values(
subject_id=new_reviewer.subject_id,
first_name=new_reviewer.first_name,
laste_name=new_reviewer.last_name,
email=new_reviewer.email,
desired=new_reviewer.desired,
max_=new_reviewer.max_,
min_=new_reviewer.min_,
abs_max=new_reviewer.abs_max,
is_active=new_reviewer.is_active,
)
.returning(ReviewerDb)
)
result = await self._session.scalars(query)
return Reviewer.model_validate(result.one())

async def get_list_by_subject_id(
self, subject_id: int, is_acitve: bool = True
) -> Sequence[Reviewer]:
query = (
Expand All @@ -23,5 +42,5 @@ async def get_by_subject_id(
)
.order_by(ReviewerDb.id)
)
objs = (await self._session.scalars(query)).all()
return [Reviewer.model_validate(obj) for obj in objs]
result = await self._session.scalars(query)
return [Reviewer.model_validate(obj) for obj in result]
12 changes: 7 additions & 5 deletions lms/db/repositories/student_product.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import Iterable, Sequence
from collections.abc import Iterable, Mapping
from typing import Any, NamedTuple

from sqlalchemy import func, insert, select
Expand Down Expand Up @@ -110,7 +110,7 @@ async def distribute_data(
self,
subject_id: int,
vk_ids: Iterable[int],
) -> Sequence[StudentDistributeData]:
) -> Mapping[int, StudentDistributeData]:
query = (
select(
StudentDb.vk_id,
Expand All @@ -127,6 +127,8 @@ async def distribute_data(
StudentDb.vk_id.in_(vk_ids),
)
)
return [
StudentDistributeData(*res) for res in await self._session.execute(query)
]
student_data_map = {}
for res in await self._session.execute(query):
data = StudentDistributeData(*res)
student_data_map[data.vk_id] = data
return student_data_map
18 changes: 18 additions & 0 deletions lms/generals/models/reviewer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
from pydantic import BaseModel, ConfigDict


class CreateReviewerModel(BaseModel):
model_config = ConfigDict(from_attributes=True)

first_name: str
last_name: str
subject_id: int
email: str
desired: int
max_: int
min_: int
abs_max: int
is_active: bool


class Reviewer(BaseModel):
model_config = ConfigDict(from_attributes=True)

Expand All @@ -14,3 +28,7 @@ class Reviewer(BaseModel):
min_: int
abs_max: int
is_active: bool

@property
def name(self) -> str:
return f"{self.first_name} {self.last_name}"
99 changes: 48 additions & 51 deletions lms/logic/distribute_homeworks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime

Expand Down Expand Up @@ -39,13 +39,13 @@ async def make_distribution(
homeworks = await self._get_soho_homeworks(params.homework_ids)
subject = await self._get_subject(params.product_ids[0])
reviewers = await self._get_reviewers(subject_id=subject.id)
student_data = await self._get_students_data(
student_map = await self._get_student_data_map(
subject_id=subject.id,
homeworks=homeworks,
)
filtered_homeworks, error_homeworks = await self._filter_homeworks(
filtered_homeworks, error_homeworks = await _filter_homeworks(
homeworks=homeworks,
student_data=student_data,
student_map=student_map,
)
distribution = Distribution(
created_at=created_at,
Expand Down Expand Up @@ -90,64 +90,20 @@ async def _get_subject(self, product_id: int) -> Subject:
return await self.uow.subject.read_by_id(product.subject_id)

async def _get_reviewers(self, subject_id: int) -> Sequence[DistributionReviewer]:
reviewers = await self.uow.reviewer.get_by_subject_id(subject_id)
reviewers = await self.uow.reviewer.get_list_by_subject_id(subject_id)
return [DistributionReviewer(**r.model_dump()) for r in reviewers]

async def _get_students_data(
async def _get_student_data_map(
self,
subject_id: int,
homeworks: Sequence[SohoHomework],
) -> Sequence[StudentDistributeData]:
) -> Mapping[int, StudentDistributeData]:
vk_ids = set(hw.student_vk_id for hw in homeworks if hw.student_vk_id)
return await self.uow.student_product.distribute_data(
subject_id=subject_id,
vk_ids=vk_ids,
)

async def _filter_homeworks(
self,
student_data: Sequence[StudentDistributeData],
homeworks: Sequence[SohoHomework],
) -> tuple[Sequence[StudentHomework], Sequence[ErrorHomework]]:
pre_filtered_homeworks: list[StudentHomework] = list()
error_homeworks: list[ErrorHomework] = list()
student_map = {st.vk_id: st for st in student_data}
for hw in homeworks:
if hw.student_vk_id is None:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.HOMEWORK_WITHOUT_VK_ID,
)
)
elif hw.student_vk_id not in student_map:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.STUDENT_WITH_VK_ID_NOT_FOUND,
)
)
elif student_map[hw.student_vk_id].is_expulsed:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.STUDENT_WAS_EXPULSED,
)
)
else:
pre_filtered_homeworks.append(
StudentHomework(
student_name=student_map[hw.student_vk_id].name,
student_vk_id=student_map[hw.student_vk_id].vk_id,
student_soho_id=hw.student_soho_id,
submission_url=hw.chat_url,
teacher_product_id=student_map[
hw.student_vk_id
].teacher_product_id,
)
)
return pre_filtered_homeworks, error_homeworks

async def _add_folder_to_notification(
self, subject_id: int, folder_id: str
) -> None:
Expand Down Expand Up @@ -206,3 +162,44 @@ def _write_data_in_new_sheet(
values=distribution.serialize_for_sheet(),
major_dimension=SHEET_MAJOR_DIMENSION,
)


async def _filter_homeworks(
homeworks: Sequence[SohoHomework],
student_map: Mapping[int, StudentDistributeData],
) -> tuple[Sequence[StudentHomework], Sequence[ErrorHomework]]:
pre_filtered_homeworks: list[StudentHomework] = list()
error_homeworks: list[ErrorHomework] = list()
for hw in homeworks:
if hw.student_vk_id is None:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.HOMEWORK_WITHOUT_VK_ID,
)
)
elif hw.student_vk_id not in student_map:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.STUDENT_WITH_VK_ID_NOT_FOUND,
)
)
elif student_map[hw.student_vk_id].is_expulsed:
error_homeworks.append(
ErrorHomework(
homework=hw,
error_message=DistributionErrorMessage.STUDENT_WAS_EXPULSED,
)
)
else:
pre_filtered_homeworks.append(
StudentHomework(
student_name=student_map[hw.student_vk_id].name,
student_vk_id=student_map[hw.student_vk_id].vk_id,
student_soho_id=hw.student_soho_id,
submission_url=hw.chat_url,
teacher_product_id=student_map[hw.student_vk_id].teacher_product_id,
)
)
return pre_filtered_homeworks, error_homeworks
2 changes: 1 addition & 1 deletion lms/rest/api/v1/product/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ async def create_distribution(
await distributor.make_distribution(params=params, created_at=now)
return StatusResponseSchema(
ok=True,
status_code=201,
status_code=HTTPStatus.CREATED,
message="The distribution was created",
)
2 changes: 1 addition & 1 deletion lms/rest/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)

group = parser.add_argument_group("Logging options")
group.add_argument("--log-level", default=LogLevel.info, choices=LogLevel.choices())
group.add_argument("--log-level", choices=LogLevel.choices(), default=LogLevel.info)
group.add_argument("--log-format", choices=LogFormat.choices(), default=LogFormat.color)

group = parser.add_argument_group("Project options")
Expand Down
47 changes: 34 additions & 13 deletions lms/rest/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

log = logging.getLogger(__name__)

ExceptionHandlersType = tuple[tuple[type[Exception], Callable], ...]


class REST(UvicornService):
__required__ = (
Expand All @@ -42,7 +44,6 @@ class REST(UvicornService):
"project_version",
"secret_key",
)

__dependencies__ = (
"autopilot",
"soho",
Expand All @@ -52,6 +53,12 @@ class REST(UvicornService):
"enroller",
)

EXCEPTION_HANDLERS: ExceptionHandlersType = (
(HTTPException, http_exception_handler),
(RequestValidationError, requset_validation_handler),
(LMSError, lms_exception_handler),
)

session_factory: async_sessionmaker[AsyncSession]
autopilot: Autopilot
soho: Soho
Expand All @@ -72,7 +79,27 @@ async def create_application(self) -> UvicornApplication:
title=self.project_name,
description=self.project_description,
version=self.project_version,
openapi_url="/docs/openapi.json",
docs_url="/docs/swagger",
redoc_url="/docs/redoc",
)

self._set_middlewares(app=app)
self._set_routes(app=app)
self._set_exceptions(app=app)
self._set_dependency_overrides(app=app)

configure_admin(
app=app,
session_factory=self.session_factory,
title=self.project_name,
secret_key=self.secret_key,
debug=self.debug,
)
log.info("REST service app configured")
return app

def _set_middlewares(self, app: FastAPI) -> None:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Expand All @@ -81,11 +108,14 @@ async def create_application(self) -> UvicornApplication:
allow_headers=["*"],
)

def _set_routes(self, app: FastAPI) -> None:
app.include_router(api_router)
app.exception_handler(HTTPException)(http_exception_handler)
app.exception_handler(RequestValidationError)(requset_validation_handler)
app.exception_handler(LMSError)(lms_exception_handler)

def _set_exceptions(self, app: FastAPI) -> None:
for exception, handler in self.EXCEPTION_HANDLERS:
app.add_exception_handler(exception, handler)

def _set_dependency_overrides(self, app: FastAPI) -> None:
app.dependency_overrides.update(
{
UnitOfWorkMarker: lambda: UnitOfWork(self.session_factory),
Expand All @@ -98,12 +128,3 @@ async def create_application(self) -> UvicornApplication:
DistributorMarker: self.get_distributor,
}
)
configure_admin(
app=app,
session_factory=self.session_factory,
title=self.project_name,
secret_key=self.secret_key,
debug=self.debug,
)
log.info("REST service app configured")
return app
Loading