Skip to content

Commit 71369b5

Browse files
authored
Print-pages limit. (#50)
## Изменения В настройках можно указать ограничение на количество листов, которые можно распечатать на принтере. ## Реализации 1. Добавлена функция, считающая количество листов, которые израсходуются на печать данного файла. 2. Создан новый столбец с данными о количестве напечатанных листов в таблице, использующейся для статистики. 3. Во всем проекте HTTP-ошибки заменены на написанные вручную ошибки для лучшей читаемости кода. 4. HTTP-ошибки возвращаются автоматически после возникновения прописанной вручную ошибки. ## Детали 1. Реализованная функция подсчета, позволяет из строки вида '1, 2, 4-10, 1-8' получить количество листов, которые будут напечатаны. 2. Новый столбец sheet_used добавлен в таблицу PrintFact. 3. Добавлены exception-хэндлеры, которые ловят ошибки, а также файл exception, в котором данные ошибки прописаны. ## Check-List - [x] Вы проверили свой код перед отправкой запроса? - [x] Вы написали тесты к реализованным функциям? - [x] Вы не забыли применить black и isort? ***P.S. Проставьте x в квадратные скобки в нужных пунктах. Example: [x]***
1 parent d151392 commit 71369b5

15 files changed

Lines changed: 423 additions & 28 deletions

File tree

.github/workflows/build_and_publish.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108
--env ALLOW_STUDENT_NUMBER=true \
109109
--env STATIC_FOLDER=/app/static \
110110
--env STORAGE_TIME=30 \
111+
--env MAX_PAGE_COUNT=20 \
111112
--env AUTH_URL=https://auth.api.test.profcomff.com/ \
112113
--name ${{ env.CONTAITER_NAME }} \
113114
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test
@@ -174,6 +175,7 @@ jobs:
174175
--env ALLOW_STUDENT_NUMBER=true \
175176
--env STATIC_FOLDER=/app/static \
176177
--env STORAGE_TIME=168 \
178+
--env MAX_PAGE_COUNT=20 \
177179
--env AUTH_URL=https://auth.api.profcomff.com/ \
178180
--name ${{ env.CONTAITER_NAME }} \
179181
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""page_count
2+
3+
Revision ID: d63e9f7661dd
4+
Revises: f6fb6304fb74
5+
Create Date: 2023-05-15 18:38:40.964981
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'd63e9f7661dd'
14+
down_revision = 'f6fb6304fb74'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.add_column('print_fact', sa.Column('sheets_used', sa.Integer(), nullable=True))
21+
22+
23+
def downgrade():
24+
op.drop_column('print_fact', 'sheets_used')

print_service/base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Base(BaseModel):
5+
def __repr__(self) -> str:
6+
attrs = []
7+
for k, v in self.__class__.schema().items():
8+
attrs.append(f"{k}={v}")
9+
return "{}({})".format(self.__class__.__name__, ', '.join(attrs))
10+
11+
class Config:
12+
orm_mode = True
13+
14+
15+
class StatusResponseModel(Base):
16+
status: str
17+
message: str

print_service/exceptions.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from print_service.settings import get_settings
2+
3+
4+
settings = get_settings()
5+
6+
7+
class ObjectNotFound(Exception):
8+
pass
9+
10+
11+
class TerminalTokenNotFound(ObjectNotFound):
12+
pass
13+
14+
15+
class TerminalQRNotFound(ObjectNotFound):
16+
pass
17+
18+
19+
class PINNotFound(ObjectNotFound):
20+
def __init__(self, pin: str):
21+
self.pin = pin
22+
23+
24+
class UserNotFound(ObjectNotFound):
25+
pass
26+
27+
28+
class FileNotFound(ObjectNotFound):
29+
def __init__(self, count: int):
30+
self.count = count
31+
32+
33+
class TooManyPages(Exception):
34+
def __init__(self):
35+
super().__init__(f'Content too large, count of page: {settings.MAX_PAGE_COUNT} is allowed')
36+
37+
38+
class TooLargeSize(Exception):
39+
def __init__(self):
40+
super().__init__(f'Content too large, {settings.MAX_SIZE} bytes allowed')
41+
42+
43+
class InvalidPageRequest(Exception):
44+
def __init__(self):
45+
super().__init__(f'Invalid format')
46+
47+
48+
class UnionStudentDuplicate(Exception):
49+
def __init__(self):
50+
super().__init__('Duplicates by union_numbers or student_numbers')
51+
52+
53+
class NotInUnion(Exception):
54+
def __init__(self):
55+
super().__init__(f'User is not found in trade union list')
56+
57+
58+
class PINGenerateError(Exception):
59+
def __init__(self):
60+
super().__init__(f'Can not generate PIN. Too many users?')
61+
62+
63+
class FileIsNotReceived(Exception):
64+
def __init__(self):
65+
super().__init__(f'No file was recieved')
66+
67+
68+
class InvalidType(Exception):
69+
def __init__(self, content_type: str):
70+
super().__init__(
71+
f'Only {", ".join(settings.CONTENT_TYPES)} files allowed, but {content_type} was recieved'
72+
)
73+
74+
75+
class AlreadyUploaded(Exception):
76+
def __init__(self):
77+
super().__init__(f'File has been already uploaded')
78+
79+
80+
class IsCorrupted(Exception):
81+
def __init__(self):
82+
super().__init__(f'File is corrupted')
83+
84+
85+
class IsNotUploaded(Exception):
86+
def __init__(self):
87+
super().__init__(f'File has not been uploaded yet')
88+
89+
90+
class UnprocessableFileInstance(Exception):
91+
def __init__(self):
92+
super().__init__(f'Unprocessable file instance')

print_service/models/__init__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import math
34
from datetime import datetime
45

56
from sqlalchemy import Column, DateTime, Integer, String
@@ -45,6 +46,37 @@ class File(Model):
4546
owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='files')
4647
print_facts: Mapped[list[PrintFact]] = relationship('PrintFact', back_populates='file')
4748

49+
@property
50+
def flatten_pages(self) -> list[int] | None:
51+
'''Возвращает расширенный список из элементов списков внутренних целочисленных точек переданного множества отрезков
52+
"1-5, 3, 2" --> [1, 2, 3, 4, 5, 3, 2]'''
53+
if self.number_of_pages is None:
54+
return None
55+
result = list()
56+
if self.option_pages == '':
57+
return result
58+
for part in self.option_pages.split(','):
59+
x = part.split('-')
60+
result.extend(range(int(x[0]), int(x[-1]) + 1))
61+
return result
62+
63+
@property
64+
def sheets_count(self) -> int | None:
65+
'''Возвращает количество элементов списков внутренних целочисленных точек переданного множества отрезков
66+
"1-5, 3, 2" --> 7
67+
P.S. 1, 2, 3, 4, 5, 3, 2 -- 7 чисел'''
68+
if self.number_of_pages is None:
69+
return None
70+
if not self.flatten_pages:
71+
return (
72+
math.ceil(self.number_of_pages - (self.option_two_sided * self.number_of_pages / 2))
73+
* self.option_copies
74+
)
75+
if self.option_two_sided:
76+
return math.ceil(len(self.flatten_pages) / 2) * self.option_copies
77+
else:
78+
return len(self.flatten_pages) * self.option_copies
79+
4880

4981
class PrintFact(Model):
5082
__tablename__ = 'print_fact'
@@ -56,3 +88,5 @@ class PrintFact(Model):
5688

5789
owner: Mapped[UnionMember] = relationship('UnionMember', back_populates='print_facts')
5890
file: Mapped[File] = relationship('File', back_populates='print_facts')
91+
92+
sheets_used: Mapped[int] = Column(Integer)

print_service/routes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1+
from . import exc_handlers
12
from .base import app
3+
4+
5+
__all__ = ["app", "exc_handlers"]

print_service/routes/admin.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from fastapi import APIRouter, Depends, HTTPException
66
from redis import Redis
77

8+
from print_service.exceptions import TerminalTokenNotFound
89
from print_service.schema import BaseModel
910
from print_service.settings import Settings, get_settings
1011

@@ -52,7 +53,7 @@ async def manual_update_terminal(
5253
sender.redis.close()
5354
return {'status': 'ok'}
5455
sender.redis.close()
55-
raise HTTPException(400, 'Terminal not found by token')
56+
raise TerminalTokenNotFound()
5657

5758

5859
@router.post("/reboot")
@@ -65,4 +66,4 @@ async def reboot_terminal(
6566
sender.redis.close()
6667
return {'status': 'ok'}
6768
sender.redis.close()
68-
raise HTTPException(400, 'Terminal not found by token')
69+
raise TerminalTokenNotFound()
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import starlette.requests
2+
from starlette.responses import JSONResponse
3+
4+
from print_service.base import StatusResponseModel
5+
from print_service.exceptions import (
6+
AlreadyUploaded,
7+
FileIsNotReceived,
8+
FileNotFound,
9+
InvalidPageRequest,
10+
InvalidType,
11+
IsCorrupted,
12+
IsNotUploaded,
13+
NotInUnion,
14+
PINGenerateError,
15+
PINNotFound,
16+
TerminalQRNotFound,
17+
TerminalTokenNotFound,
18+
TooLargeSize,
19+
TooManyPages,
20+
UnionStudentDuplicate,
21+
UnprocessableFileInstance,
22+
UserNotFound,
23+
)
24+
from print_service.routes.base import app
25+
26+
27+
@app.exception_handler(TooLargeSize)
28+
async def too_large_size(req: starlette.requests.Request, exc: TooLargeSize):
29+
return JSONResponse(
30+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=413
31+
)
32+
33+
34+
@app.exception_handler(TooManyPages)
35+
async def too_many_pages(req: starlette.requests.Request, exc: TooManyPages):
36+
return JSONResponse(
37+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=413
38+
)
39+
40+
41+
@app.exception_handler(InvalidPageRequest)
42+
async def invalid_format(req: starlette.requests.Request, exc: TooManyPages):
43+
return JSONResponse(
44+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=416
45+
)
46+
47+
48+
@app.exception_handler(TerminalQRNotFound)
49+
async def terminal_not_found_by_qr(req: starlette.requests.Request, exc: TerminalQRNotFound):
50+
return JSONResponse(
51+
content=StatusResponseModel(status="Error", message=f"Terminal not found by QR").dict(),
52+
status_code=400,
53+
)
54+
55+
56+
@app.exception_handler(TerminalTokenNotFound)
57+
async def terminal_not_found_by_token(req: starlette.requests.Request, exc: TerminalTokenNotFound):
58+
return JSONResponse(
59+
content=StatusResponseModel(status="Error", message=f"Terminal not found by token").dict(),
60+
status_code=400,
61+
)
62+
63+
64+
@app.exception_handler(UserNotFound)
65+
async def user_not_found(req: starlette.requests.Request, exc: UserNotFound):
66+
return JSONResponse(
67+
content=StatusResponseModel(status="Error", message=f"User not found").dict(), status_code=404
68+
)
69+
70+
71+
@app.exception_handler(UnionStudentDuplicate)
72+
async def student_duplicate(req: starlette.requests.Request, exc: UnionStudentDuplicate):
73+
return JSONResponse(
74+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=400
75+
)
76+
77+
78+
@app.exception_handler(NotInUnion)
79+
async def not_in_union(req: starlette.requests.Request, exc: NotInUnion):
80+
return JSONResponse(
81+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=403
82+
)
83+
84+
85+
@app.exception_handler(PINGenerateError)
86+
async def generate_error(req: starlette.requests.Request, exc: PINGenerateError):
87+
return JSONResponse(
88+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=500
89+
)
90+
91+
92+
@app.exception_handler(FileIsNotReceived)
93+
async def file_not_received(req: starlette.requests.Request, exc: FileIsNotReceived):
94+
return JSONResponse(
95+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=400
96+
)
97+
98+
99+
@app.exception_handler(PINNotFound)
100+
async def pin_not_found(req: starlette.requests.Request, exc: PINNotFound):
101+
return JSONResponse(
102+
content=StatusResponseModel(status="Error", message=f"Pin {exc.pin} not found").dict(),
103+
status_code=404,
104+
)
105+
106+
107+
@app.exception_handler(InvalidType)
108+
async def invalid_type(req: starlette.requests.Request, exc: InvalidType):
109+
return JSONResponse(
110+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=415
111+
)
112+
113+
114+
@app.exception_handler(AlreadyUploaded)
115+
async def already_upload(req: starlette.requests.Request, exc: AlreadyUploaded):
116+
return JSONResponse(
117+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=415
118+
)
119+
120+
121+
@app.exception_handler(IsCorrupted)
122+
async def is_corrupted(req: starlette.requests.Request, exc: IsCorrupted):
123+
return JSONResponse(
124+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=415
125+
)
126+
127+
128+
@app.exception_handler(UnprocessableFileInstance)
129+
async def unprocessable_file_instance(req: starlette.requests.Request, exc: UnprocessableFileInstance):
130+
return JSONResponse(
131+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=422
132+
)
133+
134+
135+
@app.exception_handler(FileNotFound)
136+
async def file_not_found(req: starlette.requests.Request, exc: FileNotFound):
137+
return JSONResponse(
138+
content=StatusResponseModel(status="Error", message=f"{exc.count} file(s) not found").dict(),
139+
status_code=404,
140+
)
141+
142+
143+
@app.exception_handler(IsNotUploaded)
144+
async def not_uploaded(req: starlette.requests.Request, exc: IsNotUploaded):
145+
return JSONResponse(
146+
content=StatusResponseModel(status="Error", message=f"{exc}").dict(), status_code=415
147+
)

0 commit comments

Comments
 (0)