From 635c524a7e83fe07a613857914292ff8b3060314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=80=E6=A2=A6?= <3501646051@qq.com> Date: Sat, 4 Apr 2026 14:43:31 +0800 Subject: [PATCH] refactor(backend): optimize database safety, validator performance, and scheduler concurrency - Fix SQL injection risks in proxy_repo and task_repo - Atomic acquire_pending with UPDATE ... RETURNING - Reuse aiohttp ClientSession in ValidatorService - Replace polling with asyncio.Event in SchedulerService - Optimize ValidationQueue.drain with asyncio.Condition - Concurrent plugin crawling with asyncio.gather - Unify ProxyRaw model import path - Fix test baseline and remove tracked __pycache__ files --- app/api/lifespan.py | 1 + app/api/routes/scheduler.py | 2 +- app/core/plugin_system/base.py | 15 +------ app/core/tasks/queue.py | 20 ++++++--- app/plugins/fate0.py | 7 ++-- app/plugins/ip89.py | 3 +- app/plugins/kuaidaili.py | 3 +- app/plugins/proxylist_download.py | 1 + app/plugins/proxyscrape.py | 11 ++--- app/plugins/speedx.py | 5 ++- app/plugins/yundaili.py | 3 +- app/repositories/proxy_repo.py | 10 ++++- app/repositories/task_repo.py | 19 ++++----- app/services/plugin_service.py | 14 +++---- app/services/scheduler_service.py | 11 +++-- app/services/validator_service.py | 38 ++++++++++++------ tests/__pycache__/conftest.cpython-311.pyc | Bin 3530 -> 0 bytes tests/conftest.py | 25 +++++------- .../test_full_workflow.cpython-311.pyc | Bin 8203 -> 0 bytes .../test_health_api.cpython-311.pyc | Bin 2878 -> 0 bytes .../test_plugins_api.cpython-311.pyc | Bin 9995 -> 0 bytes .../test_proxies_api.cpython-311.pyc | Bin 9308 -> 0 bytes .../test_scheduler_api.cpython-311.pyc | Bin 6577 -> 0 bytes .../test_settings_api.cpython-311.pyc | Bin 6603 -> 0 bytes tests/integration/test_proxies_api.py | 4 +- .../__pycache__/test_models.cpython-311.pyc | Bin 8278 -> 0 bytes .../test_repositories.cpython-311.pyc | Bin 5415 -> 0 bytes 27 files changed, 103 insertions(+), 89 deletions(-) delete mode 100644 tests/__pycache__/conftest.cpython-311.pyc delete mode 100644 tests/e2e/__pycache__/test_full_workflow.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/test_health_api.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/test_plugins_api.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/test_proxies_api.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/test_scheduler_api.cpython-311.pyc delete mode 100644 tests/integration/__pycache__/test_settings_api.cpython-311.pyc delete mode 100644 tests/unit/__pycache__/test_models.cpython-311.pyc delete mode 100644 tests/unit/__pycache__/test_repositories.cpython-311.pyc diff --git a/app/api/lifespan.py b/app/api/lifespan.py index 1b859d5..ceda5b2 100644 --- a/app/api/lifespan.py +++ b/app/api/lifespan.py @@ -38,4 +38,5 @@ async def lifespan(app: FastAPI): # 关闭调度器 await scheduler_service.stop() + await scheduler_service.validation_queue.validator.close() logger.info("API server shutdown") diff --git a/app/api/routes/scheduler.py b/app/api/routes/scheduler.py index fa3b903..29a2f54 100644 --- a/app/api/routes/scheduler.py +++ b/app/api/routes/scheduler.py @@ -49,7 +49,7 @@ async def stop_scheduler(scheduler: SchedulerService = Depends(get_scheduler_ser @router.post("/validate-now") async def validate_now(scheduler: SchedulerService = Depends(get_scheduler_service)): try: - scheduler.validate_all_now() + await scheduler.validate_all_now() return success_response("已开始全量验证", {"started": True}) except Exception as e: logger.error(f"Validate now failed: {e}") diff --git a/app/core/plugin_system/base.py b/app/core/plugin_system/base.py index 0390e3b..8a4919a 100644 --- a/app/core/plugin_system/base.py +++ b/app/core/plugin_system/base.py @@ -1,20 +1,7 @@ """插件基类 - 所有爬虫插件必须继承此基类""" from abc import ABC, abstractmethod -from dataclasses import dataclass from typing import List, Dict, Any - - -@dataclass -class ProxyRaw: - """爬虫产出的原始代理数据""" - ip: str - port: int - protocol: str = "http" - - def __post_init__(self): - self.protocol = self.protocol.lower().strip() - if self.protocol not in ("http", "https", "socks4", "socks5"): - self.protocol = "http" +from app.models.domain import ProxyRaw class BaseCrawlerPlugin(ABC): diff --git a/app/core/tasks/queue.py b/app/core/tasks/queue.py index 1336f0e..0cbf5a7 100644 --- a/app/core/tasks/queue.py +++ b/app/core/tasks/queue.py @@ -40,6 +40,8 @@ class ValidationQueue: self._workers: list[asyncio.Task] = [] self._running = False self._db_lock = asyncio.Lock() + self._pending_count = 0 + self._condition = asyncio.Condition() # 统计 self.valid_count = 0 @@ -58,6 +60,8 @@ class ValidationQueue: logger.info(f"ValidationQueue recovered {recovered} interrupted tasks") if pending: logger.info(f"ValidationQueue has {pending} pending tasks to process") + async with self._condition: + self._pending_count = pending for i in range(self.worker_count): self._workers.append(asyncio.create_task(self._worker_loop(i))) @@ -86,6 +90,9 @@ class ValidationQueue: async with get_db() as db: inserted = await self.task_repo.insert_batch(db, proxies) if inserted: + async with self._condition: + self._pending_count += inserted + self._condition.notify_all() for _ in range(min(inserted, self.worker_count)): self._signal.put_nowait(None) @@ -94,12 +101,9 @@ class ValidationQueue: async def drain(self): """等待队列中当前所有 pending 任务处理完毕""" - while True: - async with get_db() as db: - count = await self.task_repo.get_pending_count(db) - if count == 0: - break - await asyncio.sleep(0.5) + async with self._condition: + if self._pending_count > 0: + await self._condition.wait_for(lambda: self._pending_count == 0) async def _worker_loop(self, worker_id: int): while True: @@ -143,6 +147,10 @@ class ValidationQueue: await self.task_repo.complete_task(db, task["id"], False, 0.0) self.invalid_count += 1 logger.debug(f"ValidationQueue: invalid {proxy.ip}:{proxy.port}") + async with self._condition: + self._pending_count = max(0, self._pending_count - 1) + if self._pending_count == 0: + self._condition.notify_all() def reset_stats(self): self.valid_count = 0 diff --git a/app/plugins/fate0.py b/app/plugins/fate0.py index 1c359b8..60ebc70 100644 --- a/app/plugins/fate0.py +++ b/app/plugins/fate0.py @@ -6,9 +6,10 @@ from app.core.log import logger class Fate0Plugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "fate0" - display_name = "Fate0聚合源" - description = "从 GitHub 持续更新的高质量代理聚合列表" + display_name = "Fate0聚合站" + description = "来自 GitHub 持续更新的高质量代理聚合列表" def __init__(self): super().__init__() @@ -34,5 +35,5 @@ class Fate0Plugin(BaseHTTPPlugin): except Exception: continue if results: - logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理") + logger.info(f"{self.display_name} 解析完成,获取 {len(results)} 个潜在代理") return results diff --git a/app/plugins/ip89.py b/app/plugins/ip89.py index 2b99add..9276a9e 100644 --- a/app/plugins/ip89.py +++ b/app/plugins/ip89.py @@ -7,6 +7,7 @@ from app.core.log import logger class Ip89Plugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "ip89" display_name = "89免费代理" description = "从 89ip.cn 爬取免费代理" @@ -35,5 +36,5 @@ class Ip89Plugin(BaseHTTPPlugin): results.append(ProxyRaw(ip, int(port), "http")) if results: - logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理") + logger.info(f"{self.display_name} 解析完成,获取 {len(results)} 个潜在代理") return results diff --git a/app/plugins/kuaidaili.py b/app/plugins/kuaidaili.py index 74efed8..6828c3c 100644 --- a/app/plugins/kuaidaili.py +++ b/app/plugins/kuaidaili.py @@ -9,6 +9,7 @@ VALID_PROTOCOLS = ("http", "https", "socks4", "socks5") class KuaiDaiLiPlugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "kuaidaili" display_name = "快代理" description = "从快代理网站爬取免费代理" @@ -45,5 +46,5 @@ class KuaiDaiLiPlugin(BaseHTTPPlugin): results.append(ProxyRaw(ip, int(port), protocol)) if results: - logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理") + logger.info(f"{self.display_name} 解析完成,获取 {len(results)} 个潜在代理") return results diff --git a/app/plugins/proxylist_download.py b/app/plugins/proxylist_download.py index 4c7883c..48e84f1 100644 --- a/app/plugins/proxylist_download.py +++ b/app/plugins/proxylist_download.py @@ -5,6 +5,7 @@ from app.core.log import logger class ProxyListDownloadPlugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "proxylist_download" display_name = "ProxyListDownload" description = "从 ProxyListDownload API 获取代理" diff --git a/app/plugins/proxyscrape.py b/app/plugins/proxyscrape.py index e02bd60..f41a027 100644 --- a/app/plugins/proxyscrape.py +++ b/app/plugins/proxyscrape.py @@ -6,19 +6,20 @@ from app.core.log import logger class ProxyScrapePlugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} """ - 从 ProxyScrape 公开 API 获取代理。 - 覆盖 http/https/socks4/socks5 全协议,专门用于测试插件系统的可扩展性。 + 从 ProxyScrape 公开 API 获取代理库 + 覆盖 http/https/socks4/socks5 全协议,专门用于测试插件系统的可扩展性 """ name = "proxyscrape" - display_name = "ProxyScrape测试源" + display_name = "ProxyScrape测试站" description = "从 ProxyScrape API 获取各类型代理(HTTP/HTTPS/SOCKS4/SOCKS5),用于测试架构扩展" enabled = True def __init__(self): super().__init__() - # 使用多个公开 GitHub 代理列表作为源,稳定性较高 + # 使用多个公开 GitHub 代理列表作为源,稳定性较差 self.urls = [ ("http", "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt"), ("https", "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/https.txt"), @@ -71,5 +72,5 @@ class ProxyScrapePlugin(BaseHTTPPlugin): ip = f"{random.randint(1, 223)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 254)}" port = random.randint(1024, 65535) test_proxies.append(ProxyRaw(ip, port, protocol)) - logger.info(f"生成 {len(test_proxies)} 个测试代理: HTTP/HTTPS/SOCKS4/SOCKS5 各 3 个") + logger.info(f"生成 {len(test_proxies)} 个测试代理 HTTP/HTTPS/SOCKS4/SOCKS5 各 3 个") return test_proxies diff --git a/app/plugins/speedx.py b/app/plugins/speedx.py index 0e86e0c..27804d1 100644 --- a/app/plugins/speedx.py +++ b/app/plugins/speedx.py @@ -6,8 +6,9 @@ from app.core.log import logger class SpeedXPlugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "speedx" - display_name = "SpeedX代理源" + display_name = "SpeedX代理库" description = "从 SpeedX GitHub 仓库获取 SOCKS 代理列表" def __init__(self): @@ -47,5 +48,5 @@ class SpeedXPlugin(BaseHTTPPlugin): results.append(ProxyRaw(ip, int(port), protocol)) if results: - logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理") + logger.info(f"{self.display_name} 解析完成,获取 {len(results)} 个潜在代理") return results diff --git a/app/plugins/yundaili.py b/app/plugins/yundaili.py index c5bc2f2..8f91746 100644 --- a/app/plugins/yundaili.py +++ b/app/plugins/yundaili.py @@ -9,6 +9,7 @@ VALID_PROTOCOLS = ("http", "https", "socks4", "socks5") class YunDaiLiPlugin(BaseHTTPPlugin): + default_config = {"max_pages": 5} name = "yundaili" display_name = "云代理" description = "从云代理网站爬取免费代理" @@ -47,5 +48,5 @@ class YunDaiLiPlugin(BaseHTTPPlugin): results.append(ProxyRaw(ip, int(port), protocol)) if results: - logger.info(f"{self.display_name} 解析完成,获得 {len(results)} 个潜在代理") + logger.info(f"{self.display_name} 解析完成,获取 {len(results)} 个潜在代理") return results diff --git a/app/repositories/proxy_repo.py b/app/repositories/proxy_repo.py index 0ee418c..f6adffa 100644 --- a/app/repositories/proxy_repo.py +++ b/app/repositories/proxy_repo.py @@ -190,7 +190,12 @@ class ProxyRepository: params.append(max_score) where_clause = " AND ".join(conditions) - order_clause = f"{sort_by} {sort_order}" + allowed_sort_by = {"ip", "port", "protocol", "score", "last_check"} + allowed_sort_order = {"ASC", "DESC"} + if sort_by not in allowed_sort_by or sort_order.upper() not in allowed_sort_order: + order_clause = "last_check DESC" + else: + order_clause = f"{sort_by} {sort_order.upper()}" offset = (page - 1) * page_size count_query = f"SELECT COUNT(*) FROM proxies WHERE {where_clause}" @@ -268,7 +273,8 @@ class ProxyRepository: async def clean_expired(db: aiosqlite.Connection, days: int) -> int: try: await db.execute( - "DELETE FROM proxies WHERE last_check < datetime('now', '-{} days')".format(days) + "DELETE FROM proxies WHERE last_check < datetime('now', '-' || ? || ' days')", + (days,), ) await db.commit() return db.total_changes diff --git a/app/repositories/task_repo.py b/app/repositories/task_repo.py index b682019..1a89052 100644 --- a/app/repositories/task_repo.py +++ b/app/repositories/task_repo.py @@ -33,22 +33,16 @@ class ValidationTaskRepository: try: async with db.execute( """ - SELECT id, ip, port, protocol FROM validation_tasks - WHERE status = 'pending' - ORDER BY id ASC - LIMIT 1 + UPDATE validation_tasks + SET status = 'processing', updated_at = CURRENT_TIMESTAMP + WHERE id = (SELECT id FROM validation_tasks WHERE status = 'pending' ORDER BY id ASC LIMIT 1) + RETURNING id, ip, port, protocol """ ) as cursor: row = await cursor.fetchone() if not row: return None - task_id = row[0] - await db.execute( - "UPDATE validation_tasks SET status = 'processing', updated_at = CURRENT_TIMESTAMP WHERE id = ?", - (task_id,), - ) - await db.commit() - return {"id": task_id, "ip": row[1], "port": row[2], "protocol": row[3]} + return {"id": row[0], "ip": row[1], "port": row[2], "protocol": row[3]} except Exception as e: logger.error(f"acquire_pending failed: {e}") return None @@ -126,7 +120,8 @@ class ValidationTaskRepository: async def cleanup_old(db: aiosqlite.Connection, days: int = 7) -> int: try: await db.execute( - "DELETE FROM validation_tasks WHERE updated_at < datetime('now', '-{} days')".format(days) + "DELETE FROM validation_tasks WHERE updated_at < datetime('now', '-' || ? || ' days')", + (days,), ) await db.commit() return db.total_changes diff --git a/app/services/plugin_service.py b/app/services/plugin_service.py index 8071cac..8fb9f39 100644 --- a/app/services/plugin_service.py +++ b/app/services/plugin_service.py @@ -1,4 +1,5 @@ """插件业务服务""" +import asyncio from datetime import datetime from typing import List, Optional from app.core.db import get_db @@ -108,14 +109,13 @@ class PluginService: async def run_all_plugins(self) -> List[ProxyRaw]: """执行所有启用插件的爬取""" all_results: List[ProxyRaw] = [] - for plugin in registry.list_plugins(): - if not plugin.enabled: + tasks = [self.run_plugin(plugin.name) for plugin in registry.list_plugins() if plugin.enabled] + results_list = await asyncio.gather(*tasks, return_exceptions=True) + for results in results_list: + if isinstance(results, Exception): + logger.error(f"Run all plugins error: {results}") continue - try: - results = await self.run_plugin(plugin.name) - all_results.extend(results) - except Exception as e: - logger.error(f"Run all plugins error at {plugin.name}: {e}") + all_results.extend(results) # 去重 seen = set() unique = [] diff --git a/app/services/scheduler_service.py b/app/services/scheduler_service.py index aedfd1a..9cae791 100644 --- a/app/services/scheduler_service.py +++ b/app/services/scheduler_service.py @@ -20,12 +20,14 @@ class SchedulerService: self.proxy_repo = proxy_repo self.interval_minutes = 30 self.running = False + self._stop_event = asyncio.Event() self._task: asyncio.Task | None = None async def start(self): if self.running: logger.warning("Scheduler already running") return + self._stop_event.clear() self.running = True await self.validation_queue.start() self._task = asyncio.create_task(self._run_loop()) @@ -33,6 +35,7 @@ class SchedulerService: async def stop(self): self.running = False + self._stop_event.set() if self._task: self._task.cancel() try: @@ -55,10 +58,10 @@ class SchedulerService: except Exception as e: logger.error(f"Scheduler loop error: {e}") # 等待下一次 - for _ in range(self.interval_minutes * 60): - if not self.running: - break - await asyncio.sleep(1) + try: + await asyncio.wait_for(self._stop_event.wait(), timeout=self.interval_minutes * 60) + except asyncio.TimeoutError: + pass async def _do_validate_all(self): """验证数据库中所有存量代理""" diff --git a/app/services/validator_service.py b/app/services/validator_service.py index 7aeaf19..55537ee 100644 --- a/app/services/validator_service.py +++ b/app/services/validator_service.py @@ -25,8 +25,24 @@ class ValidatorService: ): self.timeout = timeout self.connect_timeout = connect_timeout + self.max_concurrency = max_concurrency self.semaphore = asyncio.Semaphore(max_concurrency) + # 共享 HTTP/HTTPS ClientSession + self._http_connector = aiohttp.TCPConnector( + ssl=False, + limit=max_concurrency, + limit_per_host=max_concurrency, + force_close=False, + ) + self._timeout = aiohttp.ClientTimeout( + total=timeout, connect=connect_timeout + ) + self._http_session = aiohttp.ClientSession( + connector=self._http_connector, + timeout=self._timeout, + ) + def _get_test_url(self, protocol: str) -> str: """获取测试 URL""" urls = self.TEST_URLS.get(protocol.lower(), self.TEST_URLS["http"]) @@ -53,20 +69,14 @@ class ValidatorService: async def _validate_http(self, ip: str, port: int, protocol: str, start: float) -> Tuple[bool, float]: """验证 HTTP/HTTPS 代理""" proxy_url = f"http://{ip}:{port}" - connector = aiohttp.TCPConnector(ssl=False, limit=0, force_close=True) - timeout = aiohttp.ClientTimeout(total=self.timeout, connect=self.connect_timeout) test_url = self._get_test_url(protocol) - try: - async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session: - async with session.get(test_url, proxy=proxy_url, allow_redirects=True) as response: - if response.status in (200, 301, 302): - latency = round((time.time() - start) * 1000, 2) - logger.info(f"HTTP valid: {ip}:{port} ({protocol}) {latency}ms") - return True, latency - return False, 0.0 - finally: - await connector.close() + async with self._http_session.get(test_url, proxy=proxy_url, allow_redirects=True) as response: + if response.status in (200, 301, 302): + latency = round((time.time() - start) * 1000, 2) + logger.info(f"HTTP valid: {ip}:{port} ({protocol}) {latency}ms") + return True, latency + return False, 0.0 async def _validate_socks(self, ip: str, port: int, protocol: str, start: float) -> Tuple[bool, float]: """验证 SOCKS4/SOCKS5 代理""" @@ -95,3 +105,7 @@ class ValidatorService: return False, 0.0 finally: await connector.close() + + async def close(self): + """关闭共享的 HTTP ClientSession""" + await self._http_session.close() diff --git a/tests/__pycache__/conftest.cpython-311.pyc b/tests/__pycache__/conftest.cpython-311.pyc deleted file mode 100644 index be7c139f13480627ed7dd5b01d77decd915031e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3530 zcmbVOYitx%6u$G=M`!klEe{LpDrt2QiX|ZOh)G3LsiYbq8urho-5GGn?#^Up3ayr8 zfr=2I5{ODw+?YGc5OR@7Bu*qoV$)2Vp1DRkv$Pkvu%z;p4Vaa>B zZUojGgK8y7kX6J4qbx}H=vWhA6-)4gBD@Ar%ud`DAy``(pRMNh%peWpj3bv>nI zVX#Y2DynR{H5vXVo3zaV4HoKZsB;5A-XaV*m0_EhkOOwxX_R3cSCgDNhez7x-l^|aSE~g<|l-troS<6y~SWmV%NGs=gh_ zUC24|kDK_ODu^#kifanun%m-*TjG{Uaa%#$mV3P@1acjii@f)fj-wq%b`9*x(L4v^ zNe$S?fH~I*q!w%^9LE5Tk2nC3JI*u2r~=TbhcG_^TWR9_%=yvMm9M6LIR$>Fd0{!t z0|fkp0}rx*&>E2abcNJILw^xSdCy)VE;$)H9m~C4o&OZ-Pp&<^Hn*cF!oIa-pr5pT z1Uhc095~0^WoXoHv7cOKoMGfJQ@yo*2xgbL5tv}i_OOlQ7=*A26o=V<_WiP7Njzq! z_Az!|7d(pJ4es4as<}FUi0$R}l0)E`d%@!^t(dE)5q{b7yVB1`r+@f1`sSWJ??g*i zzMejDeP-;ch7spO@dLw9x2|80t^+5r&Zrzx)7#yp%ZVN>jgTV_8bogr<1C0*LfdpF z>G9Uln9*p|vOok?k*tEM-v?rQT{tqlIv;AD3^f-*&AFX_2A>|@JH9F(Y@ZCa7lQ4x zgmE=2PX2hg7^pwtv+^wG(7%tv|9qz@iESX@iTTjY|vjpKgjgf_qD_Yecr zadvFSIu5F}jx_?@`Fn=CjD33WgxD^F_asx}GPbT2gvu^sa~E(KD^%eTx;uEi^y!7^ z&#z6Nxl)=qclXz?rU$?6j4@gr2qE^cQg`)%ivm|e(gam60bc2mbs7}UHw}i*LE}jV zg#gFUd%*&G>c%%SXOB|g_07$|87y8sRA$NHIJXxFBG`udVM2tshy$MC$<1hLyy30UY+5{o*L z`%Av!hcIJx*sKf$Pett!2oPgXt@OxjE&#*Rvm9jQOL9CnHGXOO$5VH&er^Htq1d`* zLrd$+ue7wbw5HC&6UF2)Y~9-cgYX_@Q>w}1%fj>mR)&&P()5v_$q>>)sjMz*MncgN zJ*otc4%02kY1sf7^;?Rmj);{bnCa8IGHO~*P~5e3Ai;(7BS0+3XWc|>xGgl@5}Jm) zN7s$lk01O^n-n?6t3Hu}_5cE5&LYdmsX2vjc(-86{9n?qGwy3`cC zKur;QNkx-eq;Bh8#ZzuJA{!l!KE6`2-csd*B5HWbuR$~1)~&;(ku=`G=gTg5m(VAN zbxY8@APV{vWD2E*=VRWxA9}ZE6sadIzp8Bp0XmUB2o$0K!xTwVo-`GSKSy(s1adSN z$=duv<#GHwB$9WQB6&W)P$`nld1nDzv!UG5fymiZo{3H}(E<~lWqBricIVLkLS!=- z-^fRH6zX4}CBU60_c_8oTTz}3EHsOY3C Generator[asyncio.AbstractEventLoop, None, None]: - """创建事件循环""" - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="function") async def app(): """创建应用实例""" # 初始化测试数据库 await init_db() app = create_app() - return app + async with app.router.lifespan_context(app): + yield app -@pytest.fixture +@pytest_asyncio.fixture async def client(app) -> AsyncGenerator[AsyncClient, None]: """创建异步 HTTP 客户端""" transport = ASGITransport(app=app) @@ -34,20 +27,20 @@ async def client(app) -> AsyncGenerator[AsyncClient, None]: yield client -@pytest.fixture +@pytest_asyncio.fixture async def db(): """获取数据库连接""" async with get_db() as db: yield db -@pytest.fixture +@pytest_asyncio.fixture async def proxy_repo(): """获取代理仓库""" return ProxyRepository() -@pytest.fixture +@pytest_asyncio.fixture async def sample_proxy(db, proxy_repo): """创建一个测试代理""" await proxy_repo.insert_or_update(db, "192.168.1.1", 8080, "http", 50) diff --git a/tests/e2e/__pycache__/test_full_workflow.cpython-311.pyc b/tests/e2e/__pycache__/test_full_workflow.cpython-311.pyc deleted file mode 100644 index b366646bef3fb48c3355d6354660a795e3e05554..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8203 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 diff --git a/tests/integration/__pycache__/test_health_api.cpython-311.pyc b/tests/integration/__pycache__/test_health_api.cpython-311.pyc deleted file mode 100644 index 6cb3af8136375aed7786a1a192c0de7c20109c01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2878 zcmc&#O>7%Q6rTO@?m9nANlF{Lq;b;-NK)B};E0e=5wsvhNUb=S!)oy?iBo57X4e(@ zqoIwUKqV>xih-y#LJf+ds3$;$12@D8ZIBPvN=TJZetL5ixa7o}-FUrmY_*{WX5T)& zee>STdo$mA`(1l`3xaX%8#~=EA@mpfs19*g*ggfq0wO4h2uJt~m*jZ#3L?ThBH|K{ z&y*o&{9jct7Y$=2;J?lIGml;_~_s{i(N z_+CIdN^-CpPq?I@i{qiH={Q8_4Z_c(B@kDt3Y3(aSVEL_mP!*#ai3+FgdiKqX+N)< z))`&PSYwb!z6*BsatWrmxpI5+@|{#Q7Z#ZS?-h8re*pFx;vlyft_bq}LAWIj@`2YI zR}kT+g*VWG@PRnX&+y~^3=uB4dzk#I=rT9M7lS)i-Rp9m6^J+^TtuD^3Dx8ofrKKy z7mBuy17-?g4S#X3= z-Evwzy_(7r-4Vyltg$RQqN!)zcBE7$ts9o3P~FUB4HM>xW@#paUJS-gp9LpxCentb zk5bJ_XN?5YsAKMBs9G)^&rLcVu8wB2mZ}>hmj&~w*q%F!@3&B35A#D{Tky=GiiD!E zo6)bL#nWH*EcV!)&(zUXrHqf3@X4b22OhAi1iDXWKc4;Q(zQ!h#WLd%FD&t7uyp2!z)WD(+g&rK~5r}sWzF3vH?dex&E{}$*;mj4?-2+%0h&}jL;1%&qgj=XkP=NZ890X+ka@_?Swe~wmucu%)oEBQV+{$_Qx z^~>k$U*FjL`Lm51^OXI@M%S$V2YoqBLt)vS-hu4WWKb+7`fwc@+`JMW_x(mxV96fN`>-d&8Zo?OH6 z5{}!7OE~@~)MZ~`x)dE;J-3F3OL*8;T*AZN)Kn=tuzGq850&tct+<4T9_Tf>m{@&w z4JS%CVJj|G1-G6@`db`DRSj)IS5>D)RVT8n_krA|s_*2r% z4ls!|IXCVd<$3QcUZ8`ZWBti|8CXT&IIfJkZPZ;x?Q`x^kp?)}c0aq%bbmLPaQFEa D9D>ee diff --git a/tests/integration/__pycache__/test_plugins_api.cpython-311.pyc b/tests/integration/__pycache__/test_plugins_api.cpython-311.pyc deleted file mode 100644 index d570926fc12cd551bb72a7a53723a476e337101a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9995 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 diff --git a/tests/integration/__pycache__/test_proxies_api.cpython-311.pyc b/tests/integration/__pycache__/test_proxies_api.cpython-311.pyc deleted file mode 100644 index 416271469eb2bebe53c454c169a21741b3edd682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9308 zcmdT~U2NOd6{bW|lI0&cRT5iv(yIAu-6mF@tXZ91e9$db%)Nt6>0#OGf-F+cU%{7-&4cjm1Hv^SVEtQxY~$<1e= zcAgTcVIK_1FZhOOkvYm%p+^JNn&3Z4U4k#~R29^4&;tpN;y{Kxkc7LN|Cb zxl(8a0e#A>Z}8vCtU(*1PyUba4azPEh3l)e-+i$Au1c!y~h? z-eRm*jXKLjs1#`{M%GWu*CG$7)dIRR<3AdI?d8)iPcWrO^z7c#g-JlHQ>&#ESv9$G z;w9s+6j?pFVe;72W;L>@R!WhMNnv{XRH|kbDy7J}Y2otrw?VnpQEtMxDI2MSpwLPE zZ>Sig#)Uutf(fh;5Q0+yAgmY7Lk0h@8|=#jP@S$(9f)e7de}yFdRbIw3XITb5?HX= z5SpO1*=()RyTwBv@zB#i@77#9yn+h74?g*no!<)>u?x7M4iXGeEu7+kO2FFWa~jh> z@YLg70gcJ>Bcj%Tl9Q#psD*%PN+y{Bu8&G-PEKZss0HN=QMf~6T3D}Sh#->1CTrl# z_(%)%L2$Vv=pl_sNwT8Tn>3)Z6T}P+9cDn?j2gUQh9#|7UjagEBI5Hoq&tQjxeN6= zcX2hv@Q9Nr!dB??M)TFU$j9pifEBS4Zj}E5q7nlkYSrpeth3ab7Ss)7`7Li<8xOp z&Aoo+#=Gy{_~Q@fKX{w80hl2FHRgz-WOa&nF*?o28f?RUJy*z3PsA8Y*92E?8T@FyCTwTfn$izD4-7kQs#y z*Y%nA=g(c4fBoIL_kWs?T9=W8ff)pM;YuRC39g&*lP0JeuA?ok!v#Xd$EvLiox0O7 zFEZ@t>%$l)j%Jmy`~&s8RUf+xE$;>4%9wg*V>^nm9ct8BCPJSW8B<=3^f-#)3><0o zs^u_$0?ks+D8M$>oH;K-wgMVUGJTQyIpa#+S&zsISR=2n{EQ}OjzF;45Lj@0D2Fp? z#qs?E2L^@)?78-fQg%G?ViwYf@qDw*MXaLC3sEINF*HG&0!Erk;g@hW$o(pa}y@_HRxb@PKBuc^y=$#znPqy zx_te@Nn=-{ZS z=jM8m?S+04tvh2|_wD}K*nwi~fEsm{T5Q{PLtYatrW3k*u0h4Ig8KCx3Nz&&@k_p2 ztfoO&O+&8Lw80~$Z99z55!0W4m(l>S-jci3N}V{wE6F2UjJK}^D;w8*V^8na?w;+B zboX@kES$YZpVha+`yr2ju(ocM-hkG{n2kABo6bZ|Yc~24V85M9S9Tnp$z4oml5UTy zFr+%f`|Gi&9ugN&`7(&wBHp~Z)VATWptf#y>jeE%0{}w z9SqeG=DU{tFd`Zr1z`yXV~l@eKNkMn_}51GHrqB-Y#S=mem%d{PoQ9TeOsa6HSfZE zFy`I&pmJxH#V_lyQwW&Zlr4b{TKB;9@~C7=tz2jNPa9-oc%7i0Zu)LH&3vt5lDCtz%MiWx`E+02rg z3#@Hs!%b2kw+@EkwMi0>V;-DH%E$6;Ht(7ZhHDp_k{LKAPAglKv86)$p*xl1N_}@iLnTtb1+Ow6*1ecb@lu+68x+g+8`@uv z-vIa0o$%YvYmvuPd->n@+j@8tON1f*c$06h7l+tl^;|2T&8DQJ@tX}cdik%tUfk*Z z+_#LtL*Oa&;IklHk=HjHd#o6HOpQ9rl98uw8@Lu3Q0?UwF1+JgPdq7G!a0?N3V*@Z zPY36xKb-sUOun<8yDHd0=`19MWGfi26I#ZZpJS@4E{BIRWRzFxpZVSLmebIgYs0Be zl)GnRdy26=YSdZkyUUXJ40f&3LL8UoM@5d)!W=i6!Ji_b-o$aw=lE2mCCG6?CJE|5 zb_{u^F{3vWW@YtO^h~gm>n0FKWj|tL;VgSWAQQ*#*457ds5cv)Y zj!ixYqD=dIz7n-ZrS_Dl<`eo~Ik4aFgMRDF&vgC2JX-Y;{Rd5@AT0m@ diff --git a/tests/integration/__pycache__/test_scheduler_api.cpython-311.pyc b/tests/integration/__pycache__/test_scheduler_api.cpython-311.pyc deleted file mode 100644 index 170990ea783eacb32bd67301f307aec23271d235..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6577 zcmeHLUu+ab7@xh{`_n?Zg_gn%FinMo)HQ`^3C{PhnfmR=?$$EAdy!Lu`&h9~4 zOZY1RB$gLKflD}*KSiPzg+L@m;}efta}UiXCM0@C@#zRYm_8W4*}dJ{z1u6LK%&vT zxyj5ozx`%*<~QHW_gyY4D@E}9m5_pm8H9c%744z3g@-Gla2j!_6LA!03sIev4YeZ9 zei(7|B^yFl;jccOHqO?L8XU3pP#+&Tl=|*c>ddfr{kF~C`{$0NkDg55_;~#5Q{F~z zw%`jygFd0_eV&VjcquxAAL(!uS=VEGK+*4hv5IP3f5o? zqF@~cs00WKAC9^Nn5*ztpF@bVeQ19RowgsL_u2+*-FgdW?@{^?Y_Fq()POBv>`{9? zZ+-UK2kab82ChD49h?JNow?TCNY%}`tmUFh(}R#1!8vkTM0XBl=6+T?;lDfP1yq*e zts6VMc{CTqfEW`d=2(i`?%#ozWx6ZE@v;lY!r@?emrQd3F(6k2!y=D&2SRLbFdP$k zp}`^Bck!Y;LsiKtFVWpXBrKN(g-S7@0Q_|Q(-*%b=%VNrHtUWi7* z0*{Fi5XicE;h9(libcUk`r3s9VlWc+5ecjBQxGOLozD#@)Rt?&>AjZAyP4gzIw`~-!6?%n`?8#ry;C7dg!?UMo~Z+oNN zipp4=s48SK7TYk{QJ5^RLNHtcrH(6hnT|#T5tk8pImu>0rn3*vftp;U5>5{qYmWP0oDviBUt0iu3b!H4GSiKi zIfFgNd!!owXnAw8yjgNvq`RKl^UsRD3lmj|;DuU=@k1KfaI5UtxiJeKjF)!Z}TRhw0+9A2zr{g2%|ML>oDCR%v3g6%92O5FqsVk8>n zx&D@^kTn0T78rrG;q;A``;+B<$!(Ez!}$zITQkHZR>va}(+Fv#YGmj21-F99=GX3Q zl$y6^6SG}yR;!Z``9E$rml3w9fdQ*CAKaZFct3n&II<^eK!&H{Pa`|>Eqo+e+OrK`Uf|0DQ9O&zFFd>OOl`b& zxH{?aA1znoFH>+hv{=i~Zt#8~Ou{sebV$rOo~*E%GIE)pLmmi-_W2xVJbEXzf@K|xpOTT_x!?f$?AL!| zbe%-k-F3`3{C3hYFJp5%R%Z}o5A4Y4(Ir*LIWr4zI(!)fSq?xX6*)kOql3yIlII|b ztlEh26Nul4@Mlbb9g312CM6&@jXm|RoIK`3m4EMuoAY7DhXsC5jL?N@#o=eraI{aWV_6!{-T IT9qFE2H=JmIRF3v diff --git a/tests/integration/__pycache__/test_settings_api.cpython-311.pyc b/tests/integration/__pycache__/test_settings_api.cpython-311.pyc deleted file mode 100644 index 265741f775e0e3942ed707b5d3f0de395068a018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6603 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 diff --git a/tests/unit/__pycache__/test_repositories.cpython-311.pyc b/tests/unit/__pycache__/test_repositories.cpython-311.pyc deleted file mode 100644 index dec936f6d5389b20a1ecf3eddc4a3efb1c075229..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5415 zcmcIoU2NOd6~3fMNtR^Ejv7iyI@?K;B}x#v*0L-u+M(;ZHEmFL*o?IBFmSZUB}!yV zBdM--ox*L>RN1R*0x`{OG$Y&agFbhjv|lA4&7;zmXIRtpjMFTM z^aZp8KUT~NH0kPM>1eZbcd>+#*?jhOv1HpUJzXrFZI<3HmaaBSpXi2J?bCx3lA?Y~ zPQ5z&Z7H2n5^74G&2TVsV{S)mui_4CQV?=M`8@n>GBC3z5CyY4PcJqZG>Y?N%|(aE zEWwPgShIc}*84TbNp#I|kvZp>cZkjmQt0Q=n-q8gukDxNX$ktVV)doP3p39w7#^52 z=Gp`D&XRK7H*>~nE7C1*=tUE^EuX&IS1A|=%%>}}3z`{ISH1j;%3B{+vsbIX{&V$e z{??7(e){gkJKnL!V$rdq$D(7=vD&-C2U1KlGo<7crKF@vAF(>4NXZ}Qv^b@E(s(g>8CFww%94^vsya*R z6g=xA__{N!!m2BynVEz-ii^aYEPg^#qUl+EJE^sIWiHYIEzypqcwbP8pl-vxfHs^c zyk{*GD~4kESpM9qc=Mau#K}VFrBdi6ZO|@RXF0Gv_fYnATjJfsCi41w}HujZaC1`cSZ;HtUYL@}^iGXT75aY?fxQUuD*ZWV1bNx4%v-WA66JF<1z#gB>O={7}*OC z-GhNI3o~MBT6dim6hT#GIbawJV&iRC1+l`xLBY($@RsrZr3VNu8!qM^=?dP4(GKkA zU!dx+brWzzivGy*p5^JkPplrj87uh5Oa5`q-9gs5ku~m6kvp{fbb%W!aiiLRU9#-D zcVGk2eFr}Y?#z98ac?2GzZBfB`G^Gf8?|371S6$jMDq~|M(!V_GG2hrD4hcQeE#j4 zpcKB*egQ_DqDyp(ecddi?d#P`3)S~8_258rc!<@0j>7|s!-G-IGvY$5w@zxTG_%$E~d!==#GK_Oj{#M*&kl> zH&N>=H=i!}Uo81w)Z86p9pXp$P%#YD%Ez_vp+flaQuuLgn_aT5PXaq~`#(HV2plNE zzV9XyIB-7*pY? zYD${i4=`!!%iw*yo-~nMsgk`|y^`-3C2BZUOvt-In*{I~c@nS0Q?exc;Em22WgIKy z0Z@(9$A}Gs1nSN0=Js`kd$ZY5>}(y>)*ufc#A#5p7>X{-`6pNR-lPkm6Q$4zZO|^C zD+hY#wcJ$E%!42sx*zZxnNJT$pL}qThKX#y>fxj3S)ph6fWUK#ZVd%(gz(q^yj4=r zi``oG3=!wpdFBn{l)@TMa(?hOQ{mD-YHwezWEX3{{9Q*X$U`u9GQd%;hBP>p=%+$)DPqV)s^l3JG z791&Fz|Yp`V~-fUlQ952J~kXKa)+0vv@adkp69jL_Y0g*;skBLF68WxqkcS>C3l^v zj}}9Z=C|d)xB5&W^jsrq8}`B2bW_7E?VNE+JIf~T64L5T430ca$sjp(gKfI(xGBkE~5qZ+Jr3M zc%)x<^Zbl3E%ChW;rZ#5m`UQYkLQ1w5t8*cE}j=taj15tXYo*VW?GQX>MlW zatOvDKa3U5t>nX49l;8xe;*<8EfT?Ge9R*XAElIkgIcieGBctvBkQd9!got-c!Tz_ zk8L1OpE;0ohgI`1a{BJoz>7VAYIwjkI~yMS*Z@?+0N0E(8w_ECp=N`jW&HH7`A;30nvtFL2)`C?4SGq{A|OD5@#`w3$mzAAqM>R{VJ%2`kw4vM16 pXqSd|l~Ml!xy#7EK<+a7g4QY<&X*|)I@ei$M(Y3D