优化
This commit is contained in:
+47
-20
@@ -1,18 +1,50 @@
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
|
||||
from api.core.Gzip import gzip_zip, gzip_unzip
|
||||
from config.main import config
|
||||
|
||||
logger = logging.getLogger('dcalc.redis_cache')
|
||||
|
||||
|
||||
def _read_cached(redis, key, without_gzip):
|
||||
content = redis.get(key)
|
||||
if content is None:
|
||||
return None
|
||||
return json.loads(content if without_gzip else gzip_unzip(content))
|
||||
|
||||
|
||||
def _write_cached(redis, key, info, expire, without_gzip):
|
||||
content = json.dumps(info) if without_gzip else gzip_zip(json.dumps(info))
|
||||
if expire > 0:
|
||||
redis.set(key, content, ex=expire)
|
||||
else:
|
||||
redis.set(key, content)
|
||||
|
||||
|
||||
async def _read_cached_async(redis, key, without_gzip):
|
||||
content = await redis.get(key)
|
||||
if content is None:
|
||||
return None
|
||||
return json.loads(content if without_gzip else gzip_unzip(content))
|
||||
|
||||
|
||||
async def _write_cached_async(redis, key, info, expire, without_gzip):
|
||||
content = json.dumps(info) if without_gzip else gzip_zip(json.dumps(info))
|
||||
if expire > 0:
|
||||
await redis.set(key, content, ex=expire)
|
||||
else:
|
||||
await redis.set(key, content)
|
||||
|
||||
|
||||
async def get_redis_info_async(redis, key, fun, expire=0, without_gzip=False):
|
||||
info = None
|
||||
if not config.DEBUG_MODE and redis:
|
||||
try:
|
||||
if without_gzip:
|
||||
info = json.loads(redis.get(key))
|
||||
else:
|
||||
info = json.loads(gzip_unzip(redis.get(key)))
|
||||
info = await _read_cached_async(redis, key, without_gzip)
|
||||
except Exception:
|
||||
logger.warning('read Redis cache failed key=%s', key, exc_info=True)
|
||||
info = None
|
||||
if info is None:
|
||||
if inspect.iscoroutinefunction(fun):
|
||||
@@ -20,31 +52,26 @@ async def get_redis_info_async(redis, key, fun, expire = 0, without_gzip=False):
|
||||
else:
|
||||
info = fun()
|
||||
if not config.DEBUG_MODE and redis and info is not None:
|
||||
if without_gzip:
|
||||
redis.set(key, json.dumps(info))
|
||||
else:
|
||||
redis.set(key, gzip_zip(json.dumps(info)))
|
||||
if expire > 0:
|
||||
redis.expire(key, expire)
|
||||
try:
|
||||
await _write_cached_async(redis, key, info, expire, without_gzip)
|
||||
except Exception:
|
||||
logger.warning('write Redis cache failed key=%s', key, exc_info=True)
|
||||
return info
|
||||
|
||||
|
||||
def get_redis_info(redis, key, fun, expire=0, without_gzip=False):
|
||||
info = None
|
||||
if not config.DEBUG_MODE and redis:
|
||||
try:
|
||||
if without_gzip:
|
||||
info = json.loads(redis.get(key))
|
||||
else:
|
||||
info = json.loads(gzip_unzip(redis.get(key)))
|
||||
info = _read_cached(redis, key, without_gzip)
|
||||
except Exception:
|
||||
logger.warning('read Redis cache failed key=%s', key, exc_info=True)
|
||||
info = None
|
||||
if info is None:
|
||||
info = fun()
|
||||
if not config.DEBUG_MODE and redis and info is not None:
|
||||
if without_gzip:
|
||||
redis.set(key, json.dumps(info))
|
||||
else:
|
||||
redis.set(key, gzip_zip(json.dumps(info)))
|
||||
if expire > 0:
|
||||
redis.expire(key, expire)
|
||||
try:
|
||||
_write_cached(redis, key, info, expire, without_gzip)
|
||||
except Exception:
|
||||
logger.warning('write Redis cache failed key=%s', key, exc_info=True)
|
||||
return info
|
||||
|
||||
+4
-3
@@ -1,19 +1,20 @@
|
||||
import time
|
||||
from typing import Annotated
|
||||
|
||||
from redis import Redis
|
||||
from sqlalchemy.orm import Session
|
||||
from redis.asyncio import Redis as AsyncRedis
|
||||
from fastapi import Depends, Header
|
||||
|
||||
from api.core.exception import ResponseException
|
||||
from core.character.adventure import get_adv_info
|
||||
from database.connect import get_db, get_redis
|
||||
from database.connect import get_async_redis, get_redis
|
||||
from api.core.Auth import AlterState
|
||||
import base64
|
||||
import json
|
||||
from api.core.Redis import get_redis_info
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_db)]
|
||||
RedisDep = Annotated[Redis, Depends(get_redis)]
|
||||
AsyncRedisDep = Annotated[AsyncRedis, Depends(get_async_redis)]
|
||||
|
||||
|
||||
def alterToken(redis: RedisDep, alter_token: str | None = Header(None)):
|
||||
|
||||
+32
-33
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from api.types.colg import HistoryGold, OfficialGold, RealTimeGold
|
||||
from api.core.Gzip import register_gzip_request
|
||||
@@ -6,7 +7,7 @@ import json
|
||||
from fastapi import APIRouter, Query
|
||||
from api.core.Response import response
|
||||
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||
from api.dp import RedisDep
|
||||
from api.dp import AsyncRedisDep, RedisDep
|
||||
from typing import Annotated
|
||||
import requests
|
||||
from config.main import config
|
||||
@@ -15,6 +16,7 @@ from api.core.Response import Return as Response
|
||||
import aiohttp
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger('dcalc.colg')
|
||||
|
||||
register_gzip_request(router)
|
||||
|
||||
@@ -39,6 +41,29 @@ payload_helper = {
|
||||
'token': config.HELPER_TOKEN,
|
||||
}
|
||||
|
||||
OFFICIAL_GOLD_URL = 'https://dzhu.qq.com/zangyi/activity/app'
|
||||
OFFICIAL_GOLD_REGIONS = ('1', '2', '3A', '3B', '4', '5', '6', '7')
|
||||
|
||||
|
||||
async def _fetch_official_gold() -> list[dict]:
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
|
||||
async def fetch_region(region: str) -> dict:
|
||||
form = {**payload_helper, 'serverNo': region}
|
||||
async with session.post(OFFICIAL_GOLD_URL, data=form) as resp:
|
||||
resp.raise_for_status()
|
||||
payload = await resp.json(content_type=None)
|
||||
data = payload['data']
|
||||
return {
|
||||
'updataTime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'region': f'跨{region}',
|
||||
'goldDealRank': [int(item['price']) for item in data['godDealRank']],
|
||||
'goldTopDealRank': [int(item['price']) for item in data['godTopDealRank']],
|
||||
}
|
||||
|
||||
return await asyncio.gather(*(fetch_region(region) for region in OFFICIAL_GOLD_REGIONS))
|
||||
|
||||
|
||||
def get_date_str(timestamp: int) -> str:
|
||||
utc_time = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||||
@@ -47,7 +72,7 @@ def get_date_str(timestamp: int) -> str:
|
||||
|
||||
|
||||
@router.get('/gold/realtime/', operation_id='realtimeGold', response_model=Response[list[RealTimeGold]])
|
||||
async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
||||
def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
||||
"""
|
||||
返回所有跨区的实时金价信息
|
||||
"""
|
||||
@@ -74,7 +99,7 @@ async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
|
||||
|
||||
|
||||
@router.get('/gold/history/', operation_id='historyGold', response_model=Response[HistoryGold] | Response[None])
|
||||
async def get_history_gold(region: Annotated[int, Query(..., description='跨区id')], timeRange: Annotated[int, Query(..., description='时间跨度天数')] = 7) -> Response[HistoryGold] | Response[None]:
|
||||
def get_history_gold(region: Annotated[int, Query(..., description='跨区id')], timeRange: Annotated[int, Query(..., description='时间跨度天数')] = 7) -> Response[HistoryGold] | Response[None]:
|
||||
"""
|
||||
获取历史金价数据
|
||||
|
||||
@@ -146,7 +171,7 @@ async def get_history_gold(region: Annotated[int, Query(..., description='跨区
|
||||
|
||||
|
||||
@router.get('/gold/official/', operation_id='officialGold', response_model=Response[list[OfficialGold]])
|
||||
async def get_official_gold(redis: RedisDep) -> Response[list[OfficialGold]]:
|
||||
async def get_official_gold(redis: AsyncRedisDep) -> Response[list[OfficialGold]]:
|
||||
"""
|
||||
获取DNF官方金币寄售实时数据
|
||||
|
||||
@@ -156,36 +181,10 @@ async def get_official_gold(redis: RedisDep) -> Response[list[OfficialGold]]:
|
||||
- 该接口会返回所有跨区的官方金币寄售数据,包括最低成交价排名前5的价格和最高成交价排名前5的价格
|
||||
- 数据来源于DNF官方提供的接口,可能会有一定的延迟和不稳定性
|
||||
"""
|
||||
official_url = 'https://dzhu.qq.com/zangyi/activity/app'
|
||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
key = 'openapi:colg:real_time_gold_officical'
|
||||
list = ['1', '2', '3A', '3B', '4', '5', '6', '7']
|
||||
try:
|
||||
|
||||
async def get_data():
|
||||
datas = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for i in list:
|
||||
payload_helper['serverNo'] = i
|
||||
query = '&'.join(f'{k}={v}' for k, v in payload_helper.items())
|
||||
tasks.append(session.post(official_url, data=query, headers=headers, timeout=10))
|
||||
responses = await asyncio.gather(*tasks)
|
||||
for index, resp in enumerate(responses):
|
||||
res = json.loads(await resp.read())
|
||||
datas.append(
|
||||
{
|
||||
'updataTime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'region': f'跨{list[index]}',
|
||||
'goldDealRank': [int(i.get('price')) for i in res['data']['godDealRank']],
|
||||
'goldTopDealRank': [int(i.get('price')) for i in res['data']['godTopDealRank']],
|
||||
}
|
||||
)
|
||||
return datas
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
data = await get_redis_info_async(redis, key, _fetch_official_gold, 30 * 60)
|
||||
except Exception:
|
||||
logger.exception('fetch official gold failed')
|
||||
return response(data=[])
|
||||
# 设置半小时过期一次
|
||||
data = await get_redis_info_async(redis, key, get_data, 30 * 60)
|
||||
return response(data=data)
|
||||
|
||||
+10
-10
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Request, Response
|
||||
from api.core.Auth import createToken
|
||||
from api.core.Response import response
|
||||
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||
from api.dp import RedisDep, AltersDep
|
||||
from api.dp import AsyncRedisDep, RedisDep, AltersDep
|
||||
from core.basic.equipment import get_equipment
|
||||
from core.character.adventure import get_adv_info, get_adv_list
|
||||
from core.basic.character import createCharacter
|
||||
@@ -21,7 +21,7 @@ register_gzip_request(router)
|
||||
|
||||
|
||||
@router.get('/adventure')
|
||||
async def get_adventure_info(request: Request, redis: RedisDep, name: str = None):
|
||||
def get_adventure_info(request: Request, redis: RedisDep, name: str = None):
|
||||
adventure_info = get_redis_info(redis, 'dcalc:adventure', get_adv_list)
|
||||
if name:
|
||||
total = [item for job in adventure_info for item in job.get('children', [])]
|
||||
@@ -31,7 +31,7 @@ async def get_adventure_info(request: Request, redis: RedisDep, name: str = None
|
||||
|
||||
|
||||
@router.get('/token/get/{alter}')
|
||||
async def getToken(redis: RedisDep, alter: str, request: Request, version: str = None, equVersion: str = '0'):
|
||||
def getToken(redis: RedisDep, alter: str, request: Request, version: str = None, equVersion: str = '0'):
|
||||
if get_adv_info(alter) is None:
|
||||
raise ResponseException('无效的职业')
|
||||
token = createToken(alter, equVersion, redis)
|
||||
@@ -39,7 +39,7 @@ async def getToken(redis: RedisDep, alter: str, request: Request, version: str =
|
||||
|
||||
|
||||
@router.get('/character')
|
||||
async def get_character_info(request: Request, state: AltersDep, redis: RedisDep):
|
||||
def get_character_info(request: Request, state: AltersDep, redis: RedisDep):
|
||||
def get_character():
|
||||
return createCharacter(state.alter, state.equVersion).getInfo()
|
||||
|
||||
@@ -56,7 +56,7 @@ async def get_character_info(request: Request, state: AltersDep, redis: RedisDep
|
||||
|
||||
|
||||
@router.get('/info/oath/{suitId}/{skillId}')
|
||||
async def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str, lv: int, redis: RedisDep):
|
||||
def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str, lv: int, redis: RedisDep):
|
||||
def get_info():
|
||||
skill = None
|
||||
version = state.equVersion
|
||||
@@ -73,7 +73,7 @@ async def get_oath_info(state: AltersDep, suitId: str, skillId: str, rarity: str
|
||||
|
||||
|
||||
@router.get('/skill/{skillId}/{level}')
|
||||
async def get_skill_info(state: AltersDep, skillId: str, level: int, redis: RedisDep):
|
||||
def get_skill_info(state: AltersDep, skillId: str, level: int, redis: RedisDep):
|
||||
"""
|
||||
获取技能信息
|
||||
"""
|
||||
@@ -99,7 +99,7 @@ async def get_skill_info(state: AltersDep, skillId: str, level: int, redis: Redi
|
||||
|
||||
|
||||
@router.get('/dnfhelper/{uid}/list')
|
||||
async def get_dnfhelper_list(uid: str, redis: RedisDep):
|
||||
async def get_dnfhelper_list(uid: str, redis: AsyncRedisDep):
|
||||
cache_key = f'dnfhelper:{uid}'
|
||||
|
||||
async def get_list():
|
||||
@@ -111,7 +111,7 @@ async def get_dnfhelper_list(uid: str, redis: RedisDep):
|
||||
|
||||
|
||||
@router.get('/dnfhelper/{uid}/{id}/')
|
||||
async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: RedisDep):
|
||||
async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: AsyncRedisDep):
|
||||
cache_key = f'dnfhelper:{uid}:{id}'
|
||||
|
||||
async def get_detail():
|
||||
@@ -123,7 +123,7 @@ async def get_dnfhelper_detail(uid: str, id: str, state: AltersDep, redis: Redis
|
||||
|
||||
|
||||
@router.get('/dnfhelper/{uid}/')
|
||||
async def get_info_by_dnfhelper(uid: str, state: AltersDep, redis: RedisDep):
|
||||
async def get_info_by_dnfhelper(uid: str, state: AltersDep):
|
||||
try:
|
||||
result = await get_user_info_by_id(uid, state)
|
||||
return response(data=result)
|
||||
@@ -134,7 +134,7 @@ async def get_info_by_dnfhelper(uid: str, state: AltersDep, redis: RedisDep):
|
||||
|
||||
|
||||
@router.post('/skillTree/')
|
||||
async def get_skill_by_code(state: AltersDep, code=Body(None)):
|
||||
def get_skill_by_code(state: AltersDep, code=Body(None)):
|
||||
try:
|
||||
skills = get_skill_tree_info(code.get('code', ''))
|
||||
if skills.get('job', {}).get('class', '') != state.alter.replace('_carry', ''):
|
||||
|
||||
@@ -31,7 +31,7 @@ register_gzip_request(router)
|
||||
|
||||
|
||||
@router.get('/skills/', operation_id='skillsList', response_model=SearchSkillResponse)
|
||||
async def get_skills_list(
|
||||
def get_skills_list(
|
||||
redis: RedisDep,
|
||||
skillName: Annotated[str | None, Query(description='技能名称,支持模糊查询')] = None,
|
||||
) -> SearchSkillResponse:
|
||||
@@ -62,7 +62,7 @@ async def get_skills_list(
|
||||
|
||||
|
||||
@router.get('/jobInfo/', operation_id='jobsInfo', response_model=JobInfoResponse)
|
||||
async def get_jobs_info(
|
||||
def get_jobs_info(
|
||||
redis: RedisDep,
|
||||
jobName: Annotated[str, Query(..., description='职业名称,支持模糊查询')] = None,
|
||||
) -> JobInfoResponse:
|
||||
@@ -98,7 +98,7 @@ async def get_jobs_info(
|
||||
|
||||
|
||||
@router.get('/{jobId}/{jobGrowId}/skills/', operation_id='skillsByJob', response_model=SkillsListResponse)
|
||||
async def get_cn_skills_by_job(
|
||||
def get_cn_skills_by_job(
|
||||
redis: RedisDep,
|
||||
jobId: Annotated[str, Path(..., description='职业')],
|
||||
jobGrowId: Annotated[str, Path(..., description='转职')],
|
||||
@@ -122,7 +122,7 @@ async def get_cn_skills_by_job(
|
||||
|
||||
|
||||
@router.get('/{jobId}/{jobGrowId}/{skillId}/', operation_id='skillDetail', response_model=SkillDetailResponse)
|
||||
async def get_cn_skill_info(
|
||||
def get_cn_skill_info(
|
||||
redis: RedisDep,
|
||||
jobId: Annotated[str, Path(..., description='职业')],
|
||||
jobGrowId: Annotated[str, Path(..., description='转职')],
|
||||
@@ -164,7 +164,7 @@ async def get_cn_skill_info(
|
||||
|
||||
|
||||
@router.get('/skillData/summary/', operation_id='skillDataSummary', response_model=SkillDataSummaryResponse)
|
||||
async def get_skill_data_summary(
|
||||
def get_skill_data_summary(
|
||||
redis: RedisDep,
|
||||
job: Annotated[str, Query(description='职业')],
|
||||
skills: Annotated[
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
import redis
|
||||
|
||||
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
|
||||
"""Database connection helpers live in :mod:`database.connect`."""
|
||||
|
||||
+27
-11
@@ -1,11 +1,17 @@
|
||||
from functools import cache
|
||||
|
||||
import redis
|
||||
import redis.asyncio as async_redis
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.pool import NullPool
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from config.main import BACKEND_DIR, config
|
||||
|
||||
RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL)
|
||||
# redis-py 8 defaults to RESP3. Keep the existing RESP2 wire contract so the
|
||||
# client remains compatible with older Redis servers and proxies.
|
||||
RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL, protocol=2)
|
||||
AsyncRedisPool = async_redis.ConnectionPool.from_url(config.REDIS_URL, protocol=2)
|
||||
_db_engines: set[Engine] = set()
|
||||
|
||||
|
||||
def get_redis_client():
|
||||
@@ -17,16 +23,26 @@ def get_redis():
|
||||
yield redis_conn
|
||||
|
||||
|
||||
async def get_async_redis():
|
||||
redis_conn = async_redis.Redis(connection_pool=AsyncRedisPool)
|
||||
try:
|
||||
yield redis_conn
|
||||
finally:
|
||||
await redis_conn.aclose()
|
||||
|
||||
|
||||
@cache
|
||||
def get_db_engine(version='0'):
|
||||
db_path = BACKEND_DIR / 'dataFiles' / f'dcalc_{version}.db'
|
||||
return create_engine(
|
||||
f'sqlite:///{db_path}',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=NullPool,
|
||||
)
|
||||
engine = create_engine(f'sqlite:///{db_path}')
|
||||
_db_engines.add(engine)
|
||||
return engine
|
||||
|
||||
|
||||
def get_db():
|
||||
with Session(get_db_engine()) as session:
|
||||
yield session
|
||||
async def close_connection_pools() -> None:
|
||||
RedisPool.close()
|
||||
await AsyncRedisPool.aclose()
|
||||
for engine in _db_engines:
|
||||
engine.dispose()
|
||||
_db_engines.clear()
|
||||
get_db_engine.cache_clear()
|
||||
|
||||
@@ -10,6 +10,7 @@ from api.core import Exceptions, Gzip, Middleware
|
||||
from api.core.Logging import configure_logging
|
||||
from api.main import api_router
|
||||
from config.main import config
|
||||
from database.connect import close_connection_pools
|
||||
|
||||
configure_logging()
|
||||
|
||||
@@ -19,7 +20,10 @@ async def lifespan(_: FastAPI):
|
||||
print('dcalc is starting...')
|
||||
print('Application starting...')
|
||||
print('Application startup complete.')
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await close_connection_pools()
|
||||
|
||||
|
||||
application = FastAPI(docs_url='/doc', redoc_url=None, lifespan=lifespan)
|
||||
|
||||
@@ -5,16 +5,20 @@ description = "A backend project for dcalc, a calculator application."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"aiohttp>=3.13.3",
|
||||
"fastapi[all]>=0.136.3",
|
||||
"aiohttp>=3.14.3",
|
||||
"fastapi[all]>=0.141.1",
|
||||
"fastmcp>=3.3.1",
|
||||
"redis>=5.2.1",
|
||||
"requests>=2.32.3",
|
||||
"sqlalchemy>=2.0.38",
|
||||
"pydantic>=2.13.4",
|
||||
"pydantic-settings>=2.15.0",
|
||||
"redis>=8.1.0",
|
||||
"requests>=2.34.2",
|
||||
"sqlalchemy>=2.0.52",
|
||||
"uvicorn[standard]>=0.52.3",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"httpx2>=2.10.0",
|
||||
"pytest>=8.4.0",
|
||||
"ruff>=0.15.15",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import asyncio
|
||||
|
||||
from api.routes import colg
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, region):
|
||||
self.region = region
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, traceback):
|
||||
return False
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
async def json(self, content_type=None):
|
||||
await asyncio.sleep(0)
|
||||
return {
|
||||
'data': {
|
||||
'godDealRank': [{'price': '100'}, {'price': '101'}],
|
||||
'godTopDealRank': [{'price': '200'}, {'price': '201'}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, calls, **kwargs):
|
||||
self.calls = calls
|
||||
self.kwargs = kwargs
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, traceback):
|
||||
return False
|
||||
|
||||
def post(self, url, data):
|
||||
self.calls.append((url, data))
|
||||
return FakeResponse(data['serverNo'])
|
||||
|
||||
|
||||
def test_fetch_official_gold_uses_async_http_with_isolated_forms(monkeypatch):
|
||||
calls = []
|
||||
sessions = []
|
||||
|
||||
def create_session(**kwargs):
|
||||
session = FakeSession(calls, **kwargs)
|
||||
sessions.append(session)
|
||||
return session
|
||||
|
||||
monkeypatch.setattr(colg.aiohttp, 'ClientSession', create_session)
|
||||
|
||||
result = asyncio.run(colg._fetch_official_gold())
|
||||
|
||||
assert [data['serverNo'] for _, data in calls] == list(colg.OFFICIAL_GOLD_REGIONS)
|
||||
assert all(url == colg.OFFICIAL_GOLD_URL for url, _ in calls)
|
||||
assert len({id(data) for _, data in calls}) == len(colg.OFFICIAL_GOLD_REGIONS)
|
||||
assert [item['region'] for item in result] == [f'跨{region}' for region in colg.OFFICIAL_GOLD_REGIONS]
|
||||
assert result[0]['goldDealRank'] == [100, 101]
|
||||
assert result[0]['goldTopDealRank'] == [200, 201]
|
||||
assert sessions[0].kwargs['timeout'].total == 10
|
||||
assert 'serverNo' not in colg.payload_helper
|
||||
@@ -0,0 +1,64 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from api.core.Redis import get_redis_info, get_redis_info_async
|
||||
from config.main import config
|
||||
from database.connect import AsyncRedisPool, RedisPool
|
||||
|
||||
|
||||
class FakeRedis:
|
||||
def __init__(self, value=None, fail_write=False):
|
||||
self.value = value
|
||||
self.fail_write = fail_write
|
||||
self.set_calls = []
|
||||
|
||||
def get(self, key):
|
||||
return self.value
|
||||
|
||||
def set(self, key, value, **kwargs):
|
||||
if self.fail_write:
|
||||
raise RuntimeError('Redis unavailable')
|
||||
self.set_calls.append((key, value, kwargs))
|
||||
|
||||
|
||||
class FakeAsyncRedis(FakeRedis):
|
||||
async def get(self, key):
|
||||
return self.value
|
||||
|
||||
async def set(self, key, value, **kwargs):
|
||||
super().set(key, value, **kwargs)
|
||||
|
||||
|
||||
def test_shared_pool_keeps_resp2_compatibility():
|
||||
assert RedisPool.connection_kwargs['protocol'] == 2
|
||||
assert AsyncRedisPool.connection_kwargs['protocol'] == 2
|
||||
|
||||
|
||||
def test_sync_cache_sets_value_and_ttl_in_one_command(monkeypatch):
|
||||
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||
redis = FakeRedis()
|
||||
|
||||
result = get_redis_info(redis, 'key', lambda: {'value': 1}, expire=60, without_gzip=True)
|
||||
|
||||
assert result == {'value': 1}
|
||||
assert redis.set_calls == [('key', json.dumps(result), {'ex': 60})]
|
||||
|
||||
|
||||
def test_sync_cache_write_failure_does_not_fail_request(monkeypatch):
|
||||
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||
redis = FakeRedis(fail_write=True)
|
||||
|
||||
assert get_redis_info(redis, 'key', lambda: {'value': 1}, without_gzip=True) == {'value': 1}
|
||||
|
||||
|
||||
def test_async_cache_awaits_async_redis_io(monkeypatch):
|
||||
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
||||
redis = FakeAsyncRedis()
|
||||
|
||||
async def load():
|
||||
return {'value': 1}
|
||||
|
||||
result = asyncio.run(get_redis_info_async(redis, 'key', load, expire=60, without_gzip=True))
|
||||
|
||||
assert result == {'value': 1}
|
||||
assert redis.set_calls == [('key', json.dumps(result), {'ex': 60})]
|
||||
Generated
+635
-413
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user