Skip to content

Commit 9d9c4ef

Browse files
authored
Pydantic v2 (#59)
## Изменения Мигрировано на Pydantic v2 ## Реализации Заменены устаревшие методы Pydantic. ## Check-List - [x] Вы проверили свой код перед отправкой запроса? - [x] Вы написали тесты к реализованным функциям? - не требуется - [x] Вы не забыли применить black и isort?
1 parent 525ccfa commit 9d9c4ef

11 files changed

Lines changed: 53 additions & 42 deletions

File tree

migrations/env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def run_migrations_offline():
4040
script output.
4141
4242
"""
43-
url = settings.DB_DSN
43+
url = str(settings.DB_DSN)
4444
context.configure(
4545
url=url,
4646
target_metadata=target_metadata,
@@ -60,7 +60,7 @@ def run_migrations_online():
6060
6161
"""
6262
configuration = config.get_section(config.config_ini_section)
63-
configuration['sqlalchemy.url'] = settings.DB_DSN
63+
configuration['sqlalchemy.url'] = str(settings.DB_DSN)
6464
connectable = engine_from_config(
6565
configuration,
6666
prefix='sqlalchemy.',

print_service/base.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
from pydantic import BaseModel
1+
from pydantic import BaseModel, ConfigDict
22

33

44
class Base(BaseModel):
55
def __repr__(self) -> str:
66
attrs = []
7-
for k, v in self.__class__.schema().items():
7+
for k, v in self.__class__.model_json_schema().items():
88
attrs.append(f"{k}={v}")
99
return "{}({})".format(self.__class__.__name__, ', '.join(attrs))
1010

11-
class Config:
12-
orm_mode = True
11+
model_config = ConfigDict(from_attributes=True, extra="ignore")
1312

1413

1514
class StatusResponseModel(Base):

print_service/routes/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class RebootInput(BaseModel):
2626
class InstantCommandSender:
2727
def __init__(self, settings: Settings = None) -> None:
2828
settings = settings or get_settings()
29-
self.redis: Redis = Redis.from_url(settings.REDIS_DSN)
29+
self.redis: Redis = Redis.from_url(str(settings.REDIS_DSN))
3030

3131
def update(self, terminal_token: str):
3232
terminal = self.redis.get(terminal_token)

print_service/routes/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
docs_url=None if __version__ != 'dev' else '/docs',
2828
redoc_url=None,
2929
)
30-
app.add_middleware(DBSessionMiddleware, db_url=settings.DB_DSN, engine_args=dict(pool_pre_ping=True))
30+
app.add_middleware(
31+
DBSessionMiddleware, db_url=str(settings.DB_DSN), engine_args=dict(pool_pre_ping=True)
32+
)
3133

3234
app.add_middleware(
3335
CORSMiddleware,

print_service/routes/exc_handlers.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def too_large_size(req: starlette.requests.Request, exc: TooLargeSize):
3636
status="Error",
3737
message=f"{exc}",
3838
ru=f"Размер файла превышает максимально допустимый: {settings.MAX_SIZE}",
39-
).dict(),
39+
).model_dump(),
4040
status_code=413,
4141
)
4242

@@ -48,7 +48,7 @@ async def too_many_pages(req: starlette.requests.Request, exc: TooManyPages):
4848
status="Error",
4949
message=f"{exc}",
5050
ru=f"Количество запрошенных страниц превышает допустимое число: {settings.MAX_PAGE_COUNT}",
51-
).dict(),
51+
).model_dump(),
5252
status_code=413,
5353
)
5454

@@ -60,7 +60,7 @@ async def invalid_format(req: starlette.requests.Request, exc: TooManyPages):
6060
status="Error",
6161
message=f"{exc}",
6262
ru="Количество запрошенных страниц превышает их количество в файле",
63-
).dict(),
63+
).model_dump(),
6464
status_code=416,
6565
)
6666

@@ -70,7 +70,7 @@ async def terminal_not_found_by_qr(req: starlette.requests.Request, exc: Termina
7070
return JSONResponse(
7171
content=StatusResponseModel(
7272
status="Error", message="Terminal not found by QR", ru="QR-код не найден"
73-
).dict(),
73+
).model_dump(),
7474
status_code=400,
7575
)
7676

@@ -80,7 +80,7 @@ async def terminal_not_found_by_token(req: starlette.requests.Request, exc: Term
8080
return JSONResponse(
8181
content=StatusResponseModel(
8282
status="Error", message="Terminal not found by token", ru="Токен не найден"
83-
).dict(),
83+
).model_dump(),
8484
status_code=400,
8585
)
8686

@@ -90,7 +90,7 @@ async def user_not_found(req: starlette.requests.Request, exc: UserNotFound):
9090
return JSONResponse(
9191
content=StatusResponseModel(
9292
status="Error", message="User not found", ru="Пользователь не найден"
93-
).dict(),
93+
).model_dump(),
9494
status_code=404,
9595
)
9696

@@ -102,7 +102,7 @@ async def student_duplicate(req: starlette.requests.Request, exc: UnionStudentDu
102102
status="Error",
103103
message=f"{exc}",
104104
ru="Один или более пользователей в списке не являются уникальными",
105-
).dict(),
105+
).model_dump(),
106106
status_code=400,
107107
)
108108

@@ -114,7 +114,7 @@ async def not_in_union(req: starlette.requests.Request, exc: NotInUnion):
114114
status="Error",
115115
message=f"{exc}",
116116
ru="Отсутствует членство в профсоюзе",
117-
).dict(),
117+
).model_dump(),
118118
status_code=403,
119119
)
120120

@@ -126,15 +126,15 @@ async def generate_error(req: starlette.requests.Request, exc: PINGenerateError)
126126
status="Error",
127127
message=f"{exc}",
128128
ru="Ошибка генерации ПИН-кода",
129-
).dict(),
129+
).model_dump(),
130130
status_code=500,
131131
)
132132

133133

134134
@app.exception_handler(FileIsNotReceived)
135135
async def file_not_received(req: starlette.requests.Request, exc: FileIsNotReceived):
136136
return JSONResponse(
137-
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл не получен").dict(),
137+
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл не получен").model_dump(),
138138
status_code=400,
139139
)
140140

@@ -144,7 +144,7 @@ async def pin_not_found(req: starlette.requests.Request, exc: PINNotFound):
144144
return JSONResponse(
145145
content=StatusResponseModel(
146146
status="Error", message=f"Pin {exc.pin} not found", ru="ПИН не найден"
147-
).dict(),
147+
).model_dump(),
148148
status_code=404,
149149
)
150150

@@ -156,23 +156,25 @@ async def invalid_type(req: starlette.requests.Request, exc: InvalidType):
156156
status="Error",
157157
message=f"{exc}",
158158
ru=f"Неподдерживаемый формат файла. Допустимые: {', '.join(settings.CONTENT_TYPES)}",
159-
).dict(),
159+
).model_dump(),
160160
status_code=415,
161161
)
162162

163163

164164
@app.exception_handler(AlreadyUploaded)
165165
async def already_upload(req: starlette.requests.Request, exc: AlreadyUploaded):
166166
return JSONResponse(
167-
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл уже загружен").dict(),
167+
content=StatusResponseModel(
168+
status="Error", message=f"{exc}", ru="Файл уже загружен"
169+
).model_dump(),
168170
status_code=415,
169171
)
170172

171173

172174
@app.exception_handler(IsCorrupted)
173175
async def is_corrupted(req: starlette.requests.Request, exc: IsCorrupted):
174176
return JSONResponse(
175-
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл повреждён").dict(),
177+
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл повреждён").model_dump(),
176178
status_code=415,
177179
)
178180

@@ -182,7 +184,7 @@ async def unprocessable_file_instance(req: starlette.requests.Request, exc: Unpr
182184
return JSONResponse(
183185
content=StatusResponseModel(
184186
status="Error", message=f"{exc}", ru="Необрабатываемый экземпляр файла"
185-
).dict(),
187+
).model_dump(),
186188
status_code=422,
187189
)
188190

@@ -192,14 +194,16 @@ async def file_not_found(req: starlette.requests.Request, exc: FileNotFound):
192194
return JSONResponse(
193195
content=StatusResponseModel(
194196
status="Error", message=f"{exc.count} file(s) not found", ru="Файл не найден"
195-
).dict(),
197+
).model_dump(),
196198
status_code=404,
197199
)
198200

199201

200202
@app.exception_handler(IsNotUploaded)
201203
async def not_uploaded(req: starlette.requests.Request, exc: IsNotUploaded):
202204
return JSONResponse(
203-
content=StatusResponseModel(status="Error", message=f"{exc}", ru="Файл не загружен").dict(),
205+
content=StatusResponseModel(
206+
status="Error", message=f"{exc}", ru="Файл не загружен"
207+
).model_dump(),
204208
status_code=415,
205209
)

print_service/routes/file.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from fastapi.exceptions import HTTPException
99
from fastapi.params import Depends
1010
from fastapi_sqlalchemy import db
11-
from pydantic import Field, validator
11+
from pydantic import Field, field_validator
1212
from sqlalchemy import func, or_
1313

1414
from print_service.base import StatusResponseModel
@@ -43,7 +43,7 @@ class PrintOptions(BaseModel):
4343
copies: int = Field(1, description='Количество копий для печати')
4444
two_sided: bool = Field(False, description='Включить печать с двух сторон листа')
4545

46-
@validator('pages', pre=True, always=True)
46+
@field_validator('pages', mode='before')
4747
def validate_pages(cls, value: str):
4848
if not isinstance(value, str):
4949
raise ValueError('Value must be str')
@@ -75,7 +75,7 @@ class SendInput(BaseModel):
7575

7676

7777
class SendInputUpdate(BaseModel):
78-
options: PrintOptions | None
78+
options: PrintOptions | None = None
7979

8080

8181
class SendOutput(BaseModel):
@@ -234,7 +234,7 @@ async def update_file_options(
234234
235235
Требует пин-код, полученный в методе POST `/file`. Обновлять настройки
236236
можно бесконечное количество раз. Можно изменять настройки по одной."""
237-
options = inp.options.dict(exclude_unset=True)
237+
options = inp.options.model_dump(exclude_unset=True)
238238
file_model = (
239239
db.session.query(FileModel)
240240
.filter(func.upper(FileModel.pin) == pin.upper())

print_service/routes/qrprint.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import random
44
from asyncio import sleep
55
from datetime import datetime, timedelta
6+
from typing import Set
67

78
from fastapi import APIRouter, Header, HTTPException, WebSocket
89
from fastapi_sqlalchemy import db
9-
from pydantic import conlist
10+
from pydantic import Field
1011
from redis import Redis
12+
from typing_extensions import Annotated
1113

1214
from print_service.exceptions import FileNotFound, InvalidPageRequest, IsNotUploaded, TerminalQRNotFound
1315
from print_service.schema import BaseModel
@@ -22,13 +24,13 @@
2224

2325
class InstantPrintCreate(BaseModel):
2426
qr_token: str
25-
files: conlist(str, min_items=1, max_items=10, unique_items=True)
27+
files: Annotated[Set[str], Field(min_length=1, max_length=10)]
2628

2729

2830
class InstantPrintSender:
2931
def __init__(self, settings: Settings = None) -> None:
3032
settings = settings or get_settings()
31-
self.redis: Redis = Redis.from_url(settings.REDIS_DSN)
33+
self.redis: Redis = Redis.from_url(str(settings.REDIS_DSN))
3234

3335
def send(self, qr_token: str, files: list[str]):
3436
terminal = self.redis.get(qr_token)
@@ -47,7 +49,7 @@ class InstantPrintFetcher:
4749
def __init__(self, terminal_token: str, settings: Settings = None) -> None:
4850
self.terminal_token = terminal_token
4951
settings = settings or get_settings()
50-
self.redis = Redis.from_url(settings.REDIS_DSN)
52+
self.redis = Redis.from_url(str(settings.REDIS_DSN))
5153
self.ttl = settings.QR_TOKEN_TTL
5254
self.delay = settings.QR_TOKEN_DELAY
5355
self.symbols = settings.QR_TOKEN_SYMBOLS
@@ -90,7 +92,7 @@ async def __anext__(self):
9092

9193
@router.post("")
9294
async def instant_print(options: InstantPrintCreate):
93-
options.qr_token = options.qr_token.removeprefix(settings.QR_TOKEN_PREFIX)
95+
options.qr_token = options.qr_token.removeprefix(str(settings.QR_TOKEN_PREFIX))
9496
if redis_conn.send(**options.dict()):
9597
return {'status': 'ok'}
9698
raise TerminalQRNotFound()
@@ -103,7 +105,7 @@ async def instant_print_terminal_connection(
103105
):
104106
await websocket.accept()
105107
manager = InstantPrintFetcher(authorization.removeprefix("token "))
106-
await websocket.send_text(json.dumps({"qr_token": settings.QR_TOKEN_PREFIX + manager.new_qr()}))
108+
await websocket.send_text(json.dumps({"qr_token": str(settings.QR_TOKEN_PREFIX) + manager.new_qr()}))
107109
async for task in manager:
108-
task['qr_token'] = settings.QR_TOKEN_PREFIX + task['qr_token']
110+
task['qr_token'] = str(settings.QR_TOKEN_PREFIX) + task['qr_token']
109111
await websocket.send_text(json.dumps(task))

print_service/routes/user.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi import APIRouter, Depends
66
from fastapi.exceptions import HTTPException
77
from fastapi_sqlalchemy import db
8-
from pydantic import constr
8+
from pydantic import constr, validate_call
99
from sqlalchemy import and_, func, or_
1010

1111
from print_service import __version__
@@ -35,6 +35,8 @@ class UpdateUserList(BaseModel):
3535

3636

3737
# region handlers
38+
39+
3840
@router.get(
3941
'/is_union_member',
4042
status_code=202,
@@ -48,6 +50,7 @@ async def check_union_member(
4850
v: Optional[str] = __version__,
4951
):
5052
"""Проверяет наличие пользователя в списке."""
53+
surname = surname.upper()
5154
user = db.session.query(UnionMember)
5255
if not settings.ALLOW_STUDENT_NUMBER:
5356
user = user.filter(UnionMember.union_number != None)

print_service/settings.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from typing import List
55

66
from auth_lib.fastapi import UnionAuthSettings
7-
from pydantic import AnyUrl, BaseSettings, DirectoryPath, PostgresDsn, RedisDsn
7+
from pydantic import AnyUrl, ConfigDict, DirectoryPath, PostgresDsn, RedisDsn
8+
from pydantic_settings import BaseSettings
89

910

1011
class Settings(UnionAuthSettings, BaseSettings):
@@ -18,7 +19,7 @@ class Settings(UnionAuthSettings, BaseSettings):
1819
MAX_SIZE: int = 26214400 # Максимальный размер файла в байтах
1920
MAX_PAGE_COUNT: int = 50
2021
STORAGE_TIME: int = 7 * 24 # Время хранения файла в часах
21-
STATIC_FOLDER: DirectoryPath | None
22+
STATIC_FOLDER: DirectoryPath | None = None
2223

2324
ALLOW_STUDENT_NUMBER: bool = False
2425

@@ -36,8 +37,7 @@ class Settings(UnionAuthSettings, BaseSettings):
3637
QR_TOKEN_TTL: int = 30 # Show time of QR code in seconds
3738
QR_TOKEN_DELAY: int = 5 # How long QR code valid after hide in seconds
3839

39-
class Config:
40-
env_file = '.env'
40+
model_config = ConfigDict(env_file=".env", extra="allow")
4141

4242

4343
@lru_cache()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ auth_lib_profcomff[fastapi]
1313
redis
1414
PyPDF4
1515
logging-profcomff
16+
pydantic-settings

0 commit comments

Comments
 (0)