Skip to content

Commit 359b3e5

Browse files
EstrellaXDclaude
andcommitted
fix: resolve all deprecation warnings
Pydantic V2: - Replace @validator with @field_validator in models/config.py - Replace .dict() with .model_dump() in Config, Settings, and BangumiDatabase - Replace .parse_obj() with .model_validate() in Settings and tests - Replace Field(example=) with Field(json_schema_extra=) in response models Datetime: - Replace datetime.utcnow() with datetime.now(timezone.utc) in jwt.py - Update factories.py to use timezone-aware datetime FastAPI: - Migrate from deprecated @router.on_event() to lifespan context manager - Move startup/shutdown handlers from program.py to main.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7b5c8d9 commit 359b3e5

9 files changed

Lines changed: 42 additions & 31 deletions

File tree

backend/src/main.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
22
import os
3+
from contextlib import asynccontextmanager
34

45
import uvicorn
56
from fastapi import FastAPI, Request
67
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
78
from fastapi.staticfiles import StaticFiles
89
from fastapi.templating import Jinja2Templates
910
from module.api import v1
11+
from module.api.program import program
1012
from module.conf import VERSION, settings, setup_logger
1113

1214
setup_logger(reset=True)
@@ -26,8 +28,19 @@
2628
}
2729

2830

31+
@asynccontextmanager
32+
async def lifespan(app: FastAPI):
33+
import asyncio
34+
35+
# Startup
36+
asyncio.create_task(program.startup())
37+
yield
38+
# Shutdown
39+
await program.stop()
40+
41+
2942
def create_app() -> FastAPI:
30-
app = FastAPI()
43+
app = FastAPI(lifespan=lifespan)
3144

3245
# mount routers
3346
app.include_router(v1, prefix="/api")
@@ -61,6 +74,7 @@ def html(request: Request, path: str):
6174
context = {"request": request}
6275
return templates.TemplateResponse("index.html", context)
6376
else:
77+
6478
@app.get("/", status_code=302, tags=["html"])
6579
def index():
6680
return RedirectResponse("/docs")

backend/src/module/api/program.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import asyncio
21
import logging
32
import os
43
import signal
@@ -18,14 +17,7 @@
1817
router = APIRouter(tags=["program"])
1918

2019

21-
@router.on_event("startup")
22-
async def startup():
23-
asyncio.create_task(program.startup())
24-
25-
26-
@router.on_event("shutdown")
27-
async def shutdown():
28-
await program.stop()
20+
# Note: Lifespan events (startup/shutdown) are now handled in main.py via lifespan context manager
2921

3022

3123
@router.get(

backend/src/module/conf/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def load(self):
3939
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
4040
config = json.load(f)
4141
config = self._migrate_old_config(config)
42-
config_obj = Config.parse_obj(config)
42+
config_obj = Config.model_validate(config)
4343
self.__dict__.update(config_obj.__dict__)
4444
logger.info("Config loaded")
4545

@@ -69,7 +69,7 @@ def _migrate_old_config(config: dict) -> dict:
6969

7070
def save(self, config_dict: dict | None = None):
7171
if not config_dict:
72-
config_dict = self.dict()
72+
config_dict = self.model_dump()
7373
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
7474
json.dump(config_dict, f, indent=4, ensure_ascii=False)
7575

@@ -79,7 +79,7 @@ def init(self):
7979
self.save()
8080

8181
def __load_from_env(self):
82-
config_dict = self.dict()
82+
config_dict = self.model_dump()
8383
for key, section in ENV_TO_ATTR.items():
8484
for env, attr in section.items():
8585
if env in os.environ:
@@ -92,7 +92,7 @@ def __load_from_env(self):
9292
else:
9393
attr_name = attr[0] if isinstance(attr, tuple) else attr
9494
config_dict[key][attr_name] = self.__val_from_env(env, attr)
95-
config_obj = Config.parse_obj(config_dict)
95+
config_obj = Config.model_validate(config_dict)
9696
self.__dict__.update(config_obj.__dict__)
9797
logger.info("Config loaded from env")
9898

backend/src/module/database/bangumi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def update(self, data: Bangumi | BangumiUpdate, _id: int = None) -> bool:
283283
return False
284284
if not db_data:
285285
return False
286-
bangumi_data = data.dict(exclude_unset=True)
286+
bangumi_data = data.model_dump(exclude_unset=True)
287287
for key, value in bangumi_data.items():
288288
setattr(db_data, key, value)
289289
self.session.add(db_data)

backend/src/module/models/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from os.path import expandvars
22
from typing import Literal
33

4-
from pydantic import BaseModel, Field, validator
4+
from pydantic import BaseModel, Field, field_validator
55

66

77
class Program(BaseModel):
@@ -102,8 +102,9 @@ class ExperimentalOpenAI(BaseModel):
102102
"", description="Azure OpenAI deployment id, ignored when api type is openai"
103103
)
104104

105-
@validator("api_base")
106-
def validate_api_base(cls, value: str):
105+
@field_validator("api_base")
106+
@classmethod
107+
def validate_api_base(cls, value: str) -> str:
107108
if value == "https://api.openai.com/":
108109
return "https://api.openai.com/v1"
109110
return value
@@ -119,5 +120,9 @@ class Config(BaseModel):
119120
notification: Notification = Notification()
120121
experimental_openai: ExperimentalOpenAI = ExperimentalOpenAI()
121122

123+
def model_dump(self, *args, by_alias=True, **kwargs):
124+
return super().model_dump(*args, by_alias=by_alias, **kwargs)
125+
126+
# Keep dict() for backward compatibility
122127
def dict(self, *args, by_alias=True, **kwargs):
123-
return super().dict(*args, by_alias=by_alias, **kwargs)
128+
return self.model_dump(*args, by_alias=by_alias, **kwargs)

backend/src/module/models/response.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33

44
class ResponseModel(BaseModel):
5-
status: bool = Field(..., example=True)
6-
status_code: int = Field(..., example=200)
5+
status: bool = Field(..., json_schema_extra={"example": True})
6+
status_code: int = Field(..., json_schema_extra={"example": 200})
77
msg_en: str
88
msg_zh: str
99
data: dict | None = None
1010

1111

1212
class APIResponse(BaseModel):
13-
status: bool = Field(..., example=True)
14-
msg_en: str = Field(..., example="Success")
15-
msg_zh: str = Field(..., example="成功")
13+
status: bool = Field(..., json_schema_extra={"example": True})
14+
msg_en: str = Field(..., json_schema_extra={"example": "Success"})
15+
msg_zh: str = Field(..., json_schema_extra={"example": "成功"})

backend/src/module/security/jwt.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import datetime, timedelta, timezone
22

33
from jose import JWTError, jwt
44
from passlib.context import CryptContext
@@ -21,9 +21,9 @@ def generate_key():
2121
def create_access_token(data: dict, expires_delta: timedelta | None = None):
2222
to_encode = data.copy()
2323
if expires_delta:
24-
expire = datetime.utcnow() + expires_delta
24+
expire = datetime.now(timezone.utc) + expires_delta
2525
else:
26-
expire = datetime.utcnow() + timedelta(minutes=1440)
26+
expire = datetime.now(timezone.utc) + timedelta(minutes=1440)
2727
to_encode.update({"exp": expire})
2828
encoded_jwt = jwt.encode(to_encode, app_pwd_key, algorithm=app_pwd_algorithm)
2929
return encoded_jwt
@@ -46,7 +46,7 @@ def verify_token(token: str):
4646
if token_data is None:
4747
return None
4848
expires = token_data.get("exp")
49-
if datetime.utcnow() >= datetime.fromtimestamp(expires):
49+
if datetime.now(timezone.utc) >= datetime.fromtimestamp(expires, tz=timezone.utc):
5050
raise JWTError("Token expired")
5151
return token_data
5252

backend/src/test/factories.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Test data factories for creating model instances with sensible defaults."""
22

3-
from datetime import datetime
3+
from datetime import datetime, timezone
44

55
from module.models import Bangumi, RSSItem, Torrent
66
from module.models.config import Config
@@ -78,7 +78,7 @@ def make_passkey(**overrides) -> Passkey:
7878
sign_count=0,
7979
aaguid="00000000-0000-0000-0000-000000000000",
8080
transports='["internal"]',
81-
created_at=datetime.utcnow(),
81+
created_at=datetime.now(timezone.utc),
8282
last_used_at=None,
8383
backup_eligible=False,
8484
backup_state=False,

backend/src/test/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_roundtrip_json(self, tmp_path):
9292
with open(json_path, "r") as f:
9393
loaded = json.load(f)
9494

95-
loaded_config = Config.parse_obj(loaded)
95+
loaded_config = Config.model_validate(loaded)
9696
assert loaded_config.program.rss_time == config.program.rss_time
9797
assert loaded_config.downloader.type == config.downloader.type
9898

0 commit comments

Comments
 (0)