From c7c8a6308ab0dba1ffecd65713e94616b9371431 Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:13:01 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=BB=9F=E4=B8=80=E8=A1=8C=E5=B0=BE?= =?UTF-8?q?=E4=B8=BA=20LF=EF=BC=8C=E6=B8=85=E7=90=86=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 .gitattributes 把文本文件钉死为 LF:否则 git 在 Windows 的 autocrlf 会把仓库的 LF 转成工作区 CRLF,与 ruff format 的 line-ending=auto 互相打架, 每次格式化都在改行尾。 --- .gitattributes | 29 + _ruff1.txt | 2078 ------------------------------------------------ _sync.log | 33 - _uvlock.log | 294 ------- 4 files changed, 29 insertions(+), 2405 deletions(-) create mode 100644 .gitattributes delete mode 100644 _ruff1.txt delete mode 100644 _sync.log delete mode 100644 _uvlock.log diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e3c5d3b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,29 @@ +# 统一行尾,避免 Windows 上 CRLF/LF 反复抖动。 +# +# 背景:git 在 Windows 默认 core.autocrlf=true,会把仓库里的 LF 转成工作区的 CRLF。 +# 而 ruff format 的 line-ending = "auto" 会跟着工作区走,结果就是 +# "格式化 → 提交 → 换台机器再格式化" 永远在改行尾,diff 里全是噪音。 +# 这里显式钉死:文本文件一律 LF。 +* text=auto eol=lf + +# Python / 配置 / 文档:明确 LF +*.py text eol=lf +*.pyi text eol=lf +*.toml text eol=lf +*.lock text eol=lf +*.md text eol=lf +*.json text eol=lf +*.txt text eol=lf +*.proto text eol=lf + +# Windows 批处理必须是 CRLF,否则 cmd 解析会出错 +*.bat text eol=crlf +*.cmd text eol=crlf + +# 二进制:不做任何行尾转换 +*.png binary +*.jpg binary +*.ico binary +*.gif binary +*.woff binary +*.woff2 binary diff --git a/_ruff1.txt b/_ruff1.txt deleted file mode 100644 index 1e4c46e..0000000 --- a/_ruff1.txt +++ /dev/null @@ -1,2078 +0,0 @@ -=== ruff 版本 === -ruff 0.16.8 - -=== ruff check --statistics === -225 UP006 [*] non-pep585-annotation - 54 UP045 [*] non-pep604-annotation-optional - 32 UP035 [-] deprecated-import - 23 E501 [ ] line-too-long - 18 PTH120 [ ] os-path-dirname - 15 PTH118 [ ] os-path-join - 12 B904 [ ] raise-without-from-inside-except - 12 SIM105 [ ] suppressible-exception - 11 PTH123 [ ] builtin-open - 8 PTH100 [ ] os-path-abspath - 7 PTH103 [ ] os-makedirs - 6 RUF100 [*] unused-noqa - 5 PTH110 [ ] os-path-exists - 5 UP015 [*] redundant-open-modes - 5 PERF203 [ ] try-except-in-loop - 5 RUF023 [*] unsorted-dunder-slots - 3 PTH107 [ ] os-remove - 3 UP037 [*] quoted-annotation - 3 RUF046 [ ] unnecessary-cast-to-int - 3 I001 [*] unsorted-imports - 2 F541 [*] f-string-missing-placeholders - 2 PERF401 [ ] manual-list-comprehension - 2 RUF012 [ ] mutable-class-default - 2 UP007 [*] non-pep604-annotation-union - 2 F401 [*] unused-import - 1 E741 [ ] ambiguous-variable-name - 1 RUF005 [ ] collection-literal-concatenation - 1 SIM115 [ ] open-file-with-context-handler - 1 PTH105 [ ] os-replace - 1 UP031 [ ] printf-string-formatting - 1 RET504 [ ] unnecessary-assign - 1 RUF022 [*] unsorted-dunder-all - 1 B007 [ ] unused-loop-control-variable - 1 F841 [ ] unused-variable -Found 474 errors. -[*] 312 fixable with the `--fix` option (17 hidden fixes can be enabled with the `--unsafe-fixes` option). - -=== ruff format --check(哪些文件格式不合规) === -unformatted: File would be reformatted - --> README.md:260:1 - | -259 | -260 + -261 | async def main(): --------------------------------------------------------------------------------- -272 | -273 + -274 | asyncio.run(main()) - | - -unformatted: File would be reformatted - --> ai_player.py:53:29 - | -52 | try: - - client = RemoteFarm(endpoint=args.url, farm=args.farm, token=args.token, - - name=f"bot-{args.farm}", realtime=False) -53 + client = RemoteFarm( -54 + endpoint=args.url, -55 + farm=args.farm, -56 + token=args.token, -57 + name=f"bot-{args.farm}", -58 + realtime=False, -59 + ) -60 | except GameError as exc: --------------------------------------------------------------------------------- -65 | snapshot = client.snapshot() - - print(f"已连接 {args.url} · 牧场「{args.farm}」· 第 {snapshot.get('day')} 天 · " - - f"金币 {snapshot.get('gold')}") -66 + print( -67 + f"已连接 {args.url} · 牧场「{args.farm}」· 第 {snapshot.get('day')} 天 · " -68 + f"金币 {snapshot.get('gold')}" -69 + ) -70 | --------------------------------------------------------------------------------- -76 | bot.stop() - - summary = {"name": args.farm, "actions": bot.actions_executed, - - "failures": bot.failures, "interrupted": True} -77 + summary = { -78 + "name": args.farm, -79 + "actions": bot.actions_executed, -80 + "failures": bot.failures, -81 + "interrupted": True, -82 + } -83 | - | - -unformatted: File would be reformatted - --> farm\__init__.py:18:14 - | -17 | __all__ = [ - - "config", "models", "engine", "storage", "keys", "tui", - - "client", "bot", "server", -18 + "config", -19 + "models", -20 + "engine", -21 + "storage", -22 + "keys", -23 + "tui", -24 + "client", -25 + "bot", -26 + "server", -27 | ] - | - -unformatted: File would be reformatted - --> farm\bot.py:30:18 - | -29 | - - def __init__(self, client, verbose: bool = True, pause: float = 0.25, - - name: str = "bot") -> None: - - self.client = client # 任何与 RemoteFarm 接口一致的对象 -30 + def __init__( -31 + self, client, verbose: bool = True, pause: float = 0.25, name: str = "bot" -32 + ) -> None: -33 + self.client = client # 任何与 RemoteFarm 接口一致的对象 -34 | self.verbose = verbose --------------------------------------------------------------------------------- -54 | # ------------------------------------------------------------ 打分 - - def score(self, action: Dict[str, Any], state: Dict[str, Any], - - catalog: Dict[str, Any]) -> float: -55 + def score( -56 + self, action: Dict[str, Any], state: Dict[str, Any], catalog: Dict[str, Any] -57 + ) -> float: -58 | """给一个候选动作打分。分数越高越先执行。 --------------------------------------------------------------------------------- -94 | if name == "buy_animal": - - info = next((a for a in catalog.get("animals", []) - - if a["key"] == params.get("kind")), None) -95 + info = next( -96 + (a for a in catalog.get("animals", []) if a["key"] == params.get("kind")), None -97 + ) -98 | cost = info["cost"] if info else 0 --------------------------------------------------------------------------------- -124 | try: - - result = self.client._action(name, params) # 走通用动作入口 -125 + result = self.client._action(name, params) # 走通用动作入口 -126 | except GameError as exc: --------------------------------------------------------------------------------- -156 | # ------------------------------------------------------------ 主循环 - - def run(self, rounds: int = 0, max_minutes: float = 0.0, - - idle_limit: int = 8) -> Dict[str, Any]: -157 + def run(self, rounds: int = 0, max_minutes: float = 0.0, idle_limit: int = 8) -> Dict[str, Any]: -158 | """跑机器人。 --------------------------------------------------------------------------------- -165 | round_no = 0 - - self.say(f"机器人 {self.name} 上线(第 {start_day} 天," - - f"{self.client.transport_label()})") -166 + self.say(f"机器人 {self.name} 上线(第 {start_day} 天,{self.client.transport_label()})") -167 | --------------------------------------------------------------------------------- -202 | } - - self.say(f"机器人 {self.name} 下线:跑了 {summary['days']} 天、" - - f"执行 {summary['actions']} 个动作、净资产 {summary['net_worth']}") -203 + self.say( -204 + f"机器人 {self.name} 下线:跑了 {summary['days']} 天、" -205 + f"执行 {summary['actions']} 个动作、净资产 {summary['net_worth']}" -206 + ) -207 | return summary --------------------------------------------------------------------------------- -220 | - - bot.choose = choose # type: ignore[assignment] -221 + bot.choose = choose # type: ignore[assignment] -222 | bot.name = kwargs.get("name", "random") - | - -unformatted: File would be reformatted - --> farm\client.py:234:18 - | -233 | - - def __init__(self, endpoint: str = DEFAULT_ENDPOINT, farm: str = "default", - - token: Optional[str] = None, name: str = "", timeout: float = 6.0, - - realtime: bool = True, autostart: bool = True) -> None: -234 + def __init__( -235 + self, -236 + endpoint: str = DEFAULT_ENDPOINT, -237 + farm: str = "default", -238 + token: Optional[str] = None, -239 + name: str = "", -240 + timeout: float = 6.0, -241 + realtime: bool = True, -242 + autostart: bool = True, -243 + ) -> None: -244 | self.endpoint = self._normalize(endpoint) --------------------------------------------------------------------------------- -252 | self.connected = False - - self.mode = "http" # http / ws / poll -253 + self.mode = "http" # http / ws / poll -254 | self.last_error = "" --------------------------------------------------------------------------------- -265 | self._ws_loop: Any = None - - self._catalog: Dict[str, Any] = {"crops": [], "animals": [], - - "feed_price": config.FEED_UNIT_PRICE, - - "water_capacity": config.WATER_CAPACITY, - - "ap_cost": config.AP_COST} -266 + self._catalog: Dict[str, Any] = { -267 + "crops": [], -268 + "animals": [], -269 + "feed_price": config.FEED_UNIT_PRICE, -270 + "water_capacity": config.WATER_CAPACITY, -271 + "ap_cost": config.AP_COST, -272 + } -273 | self._log: Deque[Dict[str, Any]] = deque(maxlen=LOG_KEEP) --------------------------------------------------------------------------------- -286 | if endpoint.startswith("tcp://"): - - endpoint = "http://" + endpoint[len("tcp://"):] -287 + endpoint = "http://" + endpoint[len("tcp://") :] -288 | if not endpoint.startswith(("http://", "https://")): --------------------------------------------------------------------------------- -302 | - - def _http(self, method: str, path: str, payload: Optional[Dict[str, Any]] = None, - - **params: Any) -> Dict[str, Any]: -303 + def _http( -304 + self, method: str, path: str, payload: Optional[Dict[str, Any]] = None, **params: Any -305 + ) -> Dict[str, Any]: -306 | url = self.url(path, **params) --------------------------------------------------------------------------------- -326 | "network", - - f"连不上服务器 {self.endpoint}({exc})。" - - "请先启动:python main.py serve --daemon", -327 + f"连不上服务器 {self.endpoint}({exc})。请先启动:python main.py serve --daemon", -328 | status=503, --------------------------------------------------------------------------------- -415 | self._log_seq = max(self._log_seq, seq) - - self._log = deque(sorted(self._log, key=lambda e: int(e.get("seq", 0))), - - maxlen=LOG_KEEP) -416 + self._log = deque( -417 + sorted(self._log, key=lambda e: int(e.get("seq", 0))), maxlen=LOG_KEEP -418 + ) -419 | -420 | # ------------------------------------------------------------ 写接口 - - def _action(self, action: str, params: Optional[Dict[str, Any]] = None, - - transport: str = "auto") -> Dict[str, Any]: -421 + def _action( -422 + self, action: str, params: Optional[Dict[str, Any]] = None, transport: str = "auto" -423 + ) -> Dict[str, Any]: -424 | """执行一个动作。 --------------------------------------------------------------------------------- -430 | params = params or {} - - use_ws = (transport == "ws") or (transport == "auto" and self.mode == "ws" - - and self.connected) -431 + use_ws = (transport == "ws") or ( -432 + transport == "auto" and self.mode == "ws" and self.connected -433 + ) -434 | if use_ws: --------------------------------------------------------------------------------- -451 | # full=1:让服务器顺手把完整快照带回来,省掉"操作后再查一次" - - data = self._http("POST", "/api/actions", {"action": action, "params": params}, - - full=1) -452 + data = self._http("POST", "/api/actions", {"action": action, "params": params}, full=1) -453 | self._apply(data.get("snapshot") or data.get("state") or {}) --------------------------------------------------------------------------------- -528 | self._stop.clear() - - self._thread = threading.Thread(target=self._realtime_loop, name="farm-ws", - - daemon=True) -529 + self._thread = threading.Thread(target=self._realtime_loop, name="farm-ws", daemon=True) -530 | self._thread.start() --------------------------------------------------------------------------------- -599 | async def _ws_main(self, websockets) -> None: - - async with websockets.connect(self.ws_url, open_timeout=self.timeout, - - ping_interval=20, ping_timeout=20, - - max_size=4 * 1024 * 1024) as socket: -600 + async with websockets.connect( -601 + self.ws_url, -602 + open_timeout=self.timeout, -603 + ping_interval=20, -604 + ping_timeout=20, -605 + max_size=4 * 1024 * 1024, -606 + ) as socket: -607 | self.mode = "ws" --------------------------------------------------------------------------------- -667 | self._merge_log(message.get("log") or []) - - future.set_result({ - - "ok": True, - - "message": message.get("message", "完成"), - - **(message.get("result") or {}), - - "state": message.get("state"), - - "snapshot": message.get("snapshot"), - - "log": message.get("log"), - - }) -668 + future.set_result( -669 + { -670 + "ok": True, -671 + "message": message.get("message", "完成"), -672 + **(message.get("result") or {}), -673 + "state": message.get("state"), -674 + "snapshot": message.get("snapshot"), -675 + "log": message.get("log"), -676 + } -677 + ) -678 | else: - - future.set_exception(GameError(message.get("error", "error"), - - message.get("message", "操作失败"))) -679 + future.set_exception( -680 + GameError(message.get("error", "error"), message.get("message", "操作失败")) -681 + ) -682 | return -683 | -684 | if kind == "day": - - self._merge_log([{ - - "seq": self._log_seq + 1, - - "day": message.get("day", 0), - - "kind": "info", - - "text": f"第 {message.get('day')} 天开始", - - }] + [{ - - "seq": self._log_seq + 2 + i, - - "day": message.get("day", 0), - - "kind": "warn", - - "text": text, - - } for i, text in enumerate(message.get("events") or [])]) -685 + self._merge_log( -686 + [ -687 + { -688 + "seq": self._log_seq + 1, -689 + "day": message.get("day", 0), -690 + "kind": "info", -691 + "text": f"第 {message.get('day')} 天开始", -692 + } -693 + ] -694 + + [ -695 + { -696 + "seq": self._log_seq + 2 + i, -697 + "day": message.get("day", 0), -698 + "kind": "warn", -699 + "text": text, -700 + } -701 + for i, text in enumerate(message.get("events") or []) -702 + ] -703 + ) -704 | return --------------------------------------------------------------------------------- -716 | - - def _ws_action(self, action: str, params: Dict[str, Any], - - timeout: Optional[float] = None) -> Dict[str, Any]: -717 + def _ws_action( -718 + self, action: str, params: Dict[str, Any], timeout: Optional[float] = None -719 + ) -> Dict[str, Any]: -720 | with self._lock: - | - -unformatted: File would be reformatted - --> farm\config.py:15:19 - | -14 | - - START_GOLD = 120 # 初始金币 - - START_LEVEL = 1 # 初始等级 - - START_AP = 10 # 每天行动点上限 - - START_FEED = 5 # 初始饲料库存 -15 + START_GOLD = 120 # 初始金币 -16 + START_LEVEL = 1 # 初始等级 -17 + START_AP = 10 # 每天行动点上限 -18 + START_FEED = 5 # 初始饲料库存 -19 | - - BASE_PLOTS = 6 # 初始土地数量 - - MAX_PLOTS = 16 # 土地数量硬上限 - - PLOT_BASE_PRICE = 200 # 第一块新地的价格 -20 + BASE_PLOTS = 6 # 初始土地数量 -21 + MAX_PLOTS = 16 # 土地数量硬上限 -22 + PLOT_BASE_PRICE = 200 # 第一块新地的价格 -23 | PLOT_PRICE_GROWTH = 1.75 # 每多买一块,价格乘以该系数 -24 | - - FEED_UNIT_PRICE = 6 # 主动购买饲料的单价 -25 + FEED_UNIT_PRICE = 6 # 主动购买饲料的单价 -26 | FEED_EMERGENCY_PRICE = 9 # 自动补货(缺饲料时喂食)的惩罚单价 - - FEED_BULK = 10 # 一次购买饲料的默认批量 -27 + FEED_BULK = 10 # 一次购买饲料的默认批量 -28 | - - WATER_CAPACITY = 3 # 一次浇水可支撑的天数 -29 + WATER_CAPACITY = 3 # 一次浇水可支撑的天数 -30 | PLANT_INITIAL_MOISTURE = 2 # 播种后地块自带的水分 - - PRICE_VOLATILITY = 0.18 # 每天价格波动幅度(±18%) - - LUCKY_CHANCE = 0.12 # 丰收暴击概率(产出翻倍) - - MAX_LOG_ENTRIES = 300 # 内存中保留的事件条数上限 - - MAX_FEED_PURCHASE = 200 # 单次购买饲料的数量上限 -31 + PRICE_VOLATILITY = 0.18 # 每天价格波动幅度(±18%) -32 + LUCKY_CHANCE = 0.12 # 丰收暴击概率(产出翻倍) -33 + MAX_LOG_ENTRIES = 300 # 内存中保留的事件条数上限 -34 + MAX_FEED_PURCHASE = 200 # 单次购买饲料的数量上限 -35 | --------------------------------------------------------------------------------- -50 | -51 + -52 | # 经验曲线:升到 level+1 所需经验 --------------------------------------------------------------------------------- -56 | -57 + -58 | # 升级奖励金币 --------------------------------------------------------------------------------- -76 | "wheat": { - - "name": "小麦", "seed_cost": 5, "grow_days": 2, - - "base_price": 14, "exp": 3, "level_req": 1, -77 + "name": "小麦", -78 + "seed_cost": 5, -79 + "grow_days": 2, -80 + "base_price": 14, -81 + "exp": 3, -82 + "level_req": 1, -83 | }, -84 | "carrot": { - - "name": "胡萝卜", "seed_cost": 8, "grow_days": 3, - - "base_price": 26, "exp": 5, "level_req": 1, -85 + "name": "胡萝卜", -86 + "seed_cost": 8, -87 + "grow_days": 3, -88 + "base_price": 26, -89 + "exp": 5, -90 + "level_req": 1, -91 | }, -92 | "tomato": { - - "name": "番茄", "seed_cost": 16, "grow_days": 4, - - "base_price": 52, "exp": 9, "level_req": 2, -93 + "name": "番茄", -94 + "seed_cost": 16, -95 + "grow_days": 4, -96 + "base_price": 52, -97 + "exp": 9, -98 + "level_req": 2, -99 | }, -100 | "corn": { - - "name": "玉米", "seed_cost": 28, "grow_days": 6, - - "base_price": 95, "exp": 14, "level_req": 3, -101 + "name": "玉米", -102 + "seed_cost": 28, -103 + "grow_days": 6, -104 + "base_price": 95, -105 + "exp": 14, -106 + "level_req": 3, -107 | }, -108 | "pumpkin": { - - "name": "南瓜", "seed_cost": 45, "grow_days": 8, - - "base_price": 180, "exp": 22, "level_req": 4, -109 + "name": "南瓜", -110 + "seed_cost": 45, -111 + "grow_days": 8, -112 + "base_price": 180, -113 + "exp": 22, -114 + "level_req": 4, -115 | }, -116 | "strawberry": { - - "name": "草莓", "seed_cost": 70, "grow_days": 10, - - "base_price": 320, "exp": 35, "level_req": 5, -117 + "name": "草莓", -118 + "seed_cost": 70, -119 + "grow_days": 10, -120 + "base_price": 320, -121 + "exp": 35, -122 + "level_req": 5, -123 | }, --------------------------------------------------------------------------------- -149 | "chicken": { - - "name": "鸡", "cost": 90, "product": "鸡蛋", - - "base_price": 15, "cycle": 1, "feed_per_day": 1, - - "exp": 4, "level_req": 1, -150 + "name": "鸡", -151 + "cost": 90, -152 + "product": "鸡蛋", -153 + "base_price": 15, -154 + "cycle": 1, -155 + "feed_per_day": 1, -156 + "exp": 4, -157 + "level_req": 1, -158 | }, -159 | "sheep": { - - "name": "羊", "cost": 220, "product": "羊毛", - - "base_price": 55, "cycle": 2, "feed_per_day": 1, - - "exp": 9, "level_req": 2, -160 + "name": "羊", -161 + "cost": 220, -162 + "product": "羊毛", -163 + "base_price": 55, -164 + "cycle": 2, -165 + "feed_per_day": 1, -166 + "exp": 9, -167 + "level_req": 2, -168 | }, -169 | "cow": { - - "name": "牛", "cost": 480, "product": "牛奶", - - "base_price": 72, "cycle": 1, "feed_per_day": 2, - - "exp": 15, "level_req": 3, -170 + "name": "牛", -171 + "cost": 480, -172 + "product": "牛奶", -173 + "base_price": 72, -174 + "cycle": 1, -175 + "feed_per_day": 2, -176 + "exp": 15, -177 + "level_req": 3, -178 | }, --------------------------------------------------------------------------------- -190 | MAX_TICK_SECONDS = 600.0 - - SLEEP_COOLDOWN = 2.0 # 手动"休息"推进一天的冷却(秒) -191 + SLEEP_COOLDOWN = 2.0 # 手动"休息"推进一天的冷却(秒) -192 | - | - -unformatted: File would be reformatted - --> farm\engine.py:80:36 - | -79 | "day": self.state.day, - - "kind": kind, # info / good / bad / warn / action -80 + "kind": kind, # info / good / bad / warn / action -81 | "text": text, --------------------------------------------------------------------------------- -150 | "locked": self.state.level < info["level_req"], - - "profit_per_day": round( - - (price - feed_cost) / max(1, info["cycle"]), 2 - - ), -151 + "profit_per_day": round((price - feed_cost) / max(1, info["cycle"]), 2), -152 | } --------------------------------------------------------------------------------- -368 | if plot.crop is not None: - - raise GameError("plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}") -369 + raise GameError( -370 + "plot_busy", f"{plot.index + 1} 号地已经种了{config.CROPS[plot.crop]['name']}" -371 + ) -372 | --------------------------------------------------------------------------------- -428 | lucky = "(丰收!)" if quantity > 1 else "" - - self.add_log( - - f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good" - - ) -429 + self.add_log(f"收获 {quantity} 份{info['name']}{lucky},+{earned} 金币", "good") -430 | levelups = self._gain_exp(info["exp"]) --------------------------------------------------------------------------------- -643 | if plot.is_ripe: - - events.append(f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟") -644 + events.append( -645 + f"{plot.index + 1} 号地的{config.CROPS[plot.crop]['name']}已成熟" -646 + ) -647 | else: - | - -unformatted: File would be reformatted - --> farm\models.py:26:29 - | -25 | def __init__(self, index: int) -> None: - - self.index = index # 0-based 下标,对外展示用 index+1 -26 + self.index = index # 0-based 下标,对外展示用 index+1 -27 | self.crop: Optional[str] = None - - self.growth: float = 0.0 # 已生长天数 - - self.moisture: int = 0 # 剩余可支撑天数 -28 + self.growth: float = 0.0 # 已生长天数 -29 + self.moisture: int = 0 # 剩余可支撑天数 -30 | self.planted_day: int = 0 --------------------------------------------------------------------------------- -104 | self.kind = kind - - self.fed = False # 今天是否已喂食 - - self.progress = 0 # 距下次产出的累计天数 - - self.starve = 0 # 连续未进食天数 - - self.pending = 0 # 待收集的产出数量 -105 + self.fed = False # 今天是否已喂食 -106 + self.progress = 0 # 距下次产出的累计天数 -107 + self.starve = 0 # 连续未进食天数 -108 + self.pending = 0 # 待收集的产出数量 -109 | --------------------------------------------------------------------------------- -210 | return -1 - - return int(config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH ** self.plots_bought)) -211 + return int(config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH**self.plots_bought)) -212 | --------------------------------------------------------------------------------- -231 | land = sum( - - config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH ** i) - - for i in range(self.plots_bought) -232 + config.PLOT_BASE_PRICE * (config.PLOT_PRICE_GROWTH**i) for i in range(self.plots_bought) -233 | ) --------------------------------------------------------------------------------- -296 | plots.extend(Plot(i) for i in range(len(plots), count)) - - state.plots = plots[:config.MAX_PLOTS] -297 + state.plots = plots[: config.MAX_PLOTS] -298 | for i, plot in enumerate(state.plots): - | - -unformatted: File would be reformatted - --> farm\net.py:46:14 - | -45 | - - def open_url(target: Union[str, urllib.request.Request], timeout: float = 6.0, - - host: Optional[str] = None) -> Any: -46 + def open_url( -47 + target: Union[str, urllib.request.Request], timeout: float = 6.0, host: Optional[str] = None -48 + ) -> Any: -49 | """打开一个 URL 或 Request,回环地址自动绕过代理。 - | - -unformatted: File would be reformatted - --> farm\server\__init__.py:25:19 - | -24 | _LAZY = { - - "API_VERSION", "SERVER_NAME", "ServerConfig", "ServerHandle", - - "build_app", "create_app", "rules_text", "brief", - - "Action", "Param", "ACTIONS", "run_action", "describe_actions", "action_names", - - "FarmSession", "SessionManager", "FarmClock", "validate_farm_id", "DEFAULT_FARM", - - "Client", "ConnectionHub", "Publisher", - - "DaemonManager", "pid_alive", - - "DASHBOARD_HTML", "dashboard_text", -25 + "API_VERSION", -26 + "SERVER_NAME", -27 + "ServerConfig", -28 + "ServerHandle", -29 + "build_app", -30 + "create_app", -31 + "rules_text", -32 + "brief", -33 + "Action", -34 + "Param", -35 + "ACTIONS", -36 + "run_action", -37 + "describe_actions", -38 + "action_names", -39 + "FarmSession", -40 + "SessionManager", -41 + "FarmClock", -42 + "validate_farm_id", -43 + "DEFAULT_FARM", -44 + "Client", -45 + "ConnectionHub", -46 + "Publisher", -47 + "DaemonManager", -48 + "pid_alive", -49 + "DASHBOARD_HTML", -50 + "dashboard_text", -51 | } --------------------------------------------------------------------------------- -55 | - - def __getattr__(name: str) -> Any: # PEP 562:模块级惰性属性 -56 + def __getattr__(name: str) -> Any: # PEP 562:模块级惰性属性 -57 | if name not in _LAZY: --------------------------------------------------------------------------------- -64 | module = import_module("farm.server.webui") - - elif name in ("Action", "Param", "ACTIONS", "run_action", "describe_actions", - - "action_names"): -65 + elif name in ("Action", "Param", "ACTIONS", "run_action", "describe_actions", "action_names"): -66 | module = import_module("farm.server.actions") - - elif name in ("FarmSession", "SessionManager", "FarmClock", "validate_farm_id", - - "DEFAULT_FARM"): -67 + elif name in ("FarmSession", "SessionManager", "FarmClock", "validate_farm_id", "DEFAULT_FARM"): -68 | module = import_module("farm.server.session") - | - -unformatted: File would be reformatted - --> farm\server\__main__.py:23:25 - | -22 | ) - - parser.add_argument("--host", default="127.0.0.1", - - help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0") -23 + parser.add_argument( -24 + "--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0" -25 + ) -26 | parser.add_argument("--port", type=int, default=8765, help="监听端口,默认 8765") -27 | parser.add_argument("--token", default=None, help="访问令牌,设置后所有接口都要带") - - parser.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS, - - help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒") -28 + parser.add_argument( -29 + "--tick", -30 + type=float, -31 + default=config.DEFAULT_TICK_SECONDS, -32 + help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒", -33 + ) -34 | parser.add_argument("--save-dir", default=None, help="存档目录,默认 <项目根>/saves") - - parser.add_argument("--save", dest="save_file", default=None, - - help="兼容旧参数:指定 default 牧场的存档文件") -35 + parser.add_argument( -36 + "--save", dest="save_file", default=None, help="兼容旧参数:指定 default 牧场的存档文件" -37 + ) -38 | parser.add_argument("--max-sessions", type=int, default=64, help="同时在线的牧场上限") - - parser.add_argument("--idle-timeout", type=float, default=1800.0, - - help="无人访问多少秒后把牧场移出内存(存档保留),默认 1800") -39 + parser.add_argument( -40 + "--idle-timeout", -41 + type=float, -42 + default=1800.0, -43 + help="无人访问多少秒后把牧场移出内存(存档保留),默认 1800", -44 + ) -45 | parser.add_argument("--world", default="SimpleFarm 公共农场", help="世界名称") - - parser.add_argument("--log-level", default="info", - - choices=["critical", "error", "warning", "info", "debug"]) -46 + parser.add_argument( -47 + "--log-level", default="info", choices=["critical", "error", "warning", "info", "debug"] -48 + ) -49 | parser.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求") - - parser.add_argument("--no-remote-control", action="store_true", - - help="禁止通过接口停机") - - parser.add_argument("--daemon", action="store_true", - - help="转到后台运行(等价于 python main.py serve --daemon)") - - parser.add_argument("--pid-file", default=None, - - help="把本进程 pid 写到该文件(守护进程模式会自动指定)") -50 + parser.add_argument("--no-remote-control", action="store_true", help="禁止通过接口停机") -51 + parser.add_argument( -52 + "--daemon", action="store_true", help="转到后台运行(等价于 python main.py serve --daemon)" -53 + ) -54 + parser.add_argument( -55 + "--pid-file", default=None, help="把本进程 pid 写到该文件(守护进程模式会自动指定)" -56 + ) -57 | parser.add_argument("--log-file", default=DEFAULT_LOG_FILE, help=argparse.SUPPRESS) --------------------------------------------------------------------------------- -82 | manager = DaemonManager( - - host=args.host, port=args.port, token=args.token, - - tick=clamp_tick(args.tick), save_dir=save_dir, -83 + host=args.host, -84 + port=args.port, -85 + token=args.token, -86 + tick=clamp_tick(args.tick), -87 + save_dir=save_dir, -88 | pid_file=args.pid_file or DEFAULT_PID_FILE, --------------------------------------------------------------------------------- -101 | import websockets # noqa: F401 只是探测,uvicorn 需要它来做 WS 协议升级 -102 + -103 | ws_ok = True --------------------------------------------------------------------------------- -153 | if not ws_ok: - - print(" ⚠ 未检测到 websockets 库,/ws 实时通道可能不可用;" - - "网页端会自动降级为轮询。安装:pip install 'uvicorn[standard]'") -154 + print( -155 + " ⚠ 未检测到 websockets 库,/ws 实时通道可能不可用;" -156 + "网页端会自动降级为轮询。安装:pip install 'uvicorn[standard]'" -157 + ) -158 | if cfg.token: --------------------------------------------------------------------------------- -173 | if not getattr(server, "started", False): - - print(f"启动失败:端口 {cfg.port} 可能已被占用。" - - f"换一个端口试试:--port {cfg.port + 1}", file=sys.stderr) -174 + print( -175 + f"启动失败:端口 {cfg.port} 可能已被占用。换一个端口试试:--port {cfg.port + 1}", -176 + file=sys.stderr, -177 + ) -178 | return 3 - | - -unformatted: File would be reformatted - --> farm\server\actions.py:27:18 - | -26 | - - def __init__(self, name: str, kind: str, required: bool = True, - - default: Any = None, desc: str = "") -> None: -27 + def __init__( -28 + self, name: str, kind: str, required: bool = True, default: Any = None, desc: str = "" -29 + ) -> None: -30 | self.name = name - - self.kind = kind # "int" / "str" / "bool" -31 + self.kind = kind # "int" / "str" / "bool" -32 | self.required = required --------------------------------------------------------------------------------- -76 | - - def __init__(self, name: str, summary: str, params: List[Param], - - handler: Callable[[FarmEngine, Dict[str, Any]], Dict[str, Any]], - - realtime: bool = True) -> None: -77 + def __init__( -78 + self, -79 + name: str, -80 + summary: str, -81 + params: List[Param], -82 + handler: Callable[[FarmEngine, Dict[str, Any]], Dict[str, Any]], -83 + realtime: bool = True, -84 + ) -> None: -85 | self.name = name --------------------------------------------------------------------------------- -150 | - - _register(Action("plant", "在指定地块播种", [ - - Param("plot", "int", True, None, "地块编号,从 1 开始"), - - Param("crop", "str", True, None, "作物标识,如 wheat"), - - ], lambda e, p: e.plant(p["plot"], p["crop"]))) -151 + _register( -152 + Action( -153 + "plant", -154 + "在指定地块播种", -155 + [ -156 + Param("plot", "int", True, None, "地块编号,从 1 开始"), -157 + Param("crop", "str", True, None, "作物标识,如 wheat"), -158 + ], -159 + lambda e, p: e.plant(p["plot"], p["crop"]), -160 + ) -161 + ) -162 | - - _register(Action("water", "给指定地块浇水(补满水分)", [ - - Param("plot", "int", True, None, "地块编号,从 1 开始"), - - ], lambda e, p: e.water(p["plot"]))) -163 + _register( -164 + Action( -165 + "water", -166 + "给指定地块浇水(补满水分)", -167 + [ -168 + Param("plot", "int", True, None, "地块编号,从 1 开始"), -169 + ], -170 + lambda e, p: e.water(p["plot"]), -171 + ) -172 + ) -173 | - - _register(Action("harvest", "收获指定地块的成熟作物", [ - - Param("plot", "int", True, None, "地块编号,从 1 开始"), - - ], lambda e, p: e.harvest(p["plot"]))) -174 + _register( -175 + Action( -176 + "harvest", -177 + "收获指定地块的成熟作物", -178 + [ -179 + Param("plot", "int", True, None, "地块编号,从 1 开始"), -180 + ], -181 + lambda e, p: e.harvest(p["plot"]), -182 + ) -183 + ) -184 | -185 | _register(Action("harvest_all", "一键收获所有成熟作物", [], _harvest_all)) -186 | - - _register(Action("buy_animal", "购买牲畜", [ - - Param("kind", "str", True, None, "牲畜标识:chicken / sheep / cow"), - - ], lambda e, p: e.buy_animal(p["kind"]))) -187 + _register( -188 + Action( -189 + "buy_animal", -190 + "购买牲畜", -191 + [ -192 + Param("kind", "str", True, None, "牲畜标识:chicken / sheep / cow"), -193 + ], -194 + lambda e, p: e.buy_animal(p["kind"]), -195 + ) -196 + ) -197 | - - _register(Action("feed", "喂食指定牲畜", [ - - Param("animal_id", "int", True, None, "牲畜编号(state.animals[].id)"), - - Param("quantity", "int", False, None, "喂食份数,默认按需求;多喂无益"), - - ], lambda e, p: e.feed(p["animal_id"], p.get("quantity")))) -198 + _register( -199 + Action( -200 + "feed", -201 + "喂食指定牲畜", -202 + [ -203 + Param("animal_id", "int", True, None, "牲畜编号(state.animals[].id)"), -204 + Param("quantity", "int", False, None, "喂食份数,默认按需求;多喂无益"), -205 + ], -206 + lambda e, p: e.feed(p["animal_id"], p.get("quantity")), -207 + ) -208 + ) -209 | - - _register(Action("collect", "收取牲畜的产出并自动出售", [ - - Param("animal_id", "int", True, None, "牲畜编号"), - - ], lambda e, p: e.collect(p["animal_id"]))) -210 + _register( -211 + Action( -212 + "collect", -213 + "收取牲畜的产出并自动出售", -214 + [ -215 + Param("animal_id", "int", True, None, "牲畜编号"), -216 + ], -217 + lambda e, p: e.collect(p["animal_id"]), -218 + ) -219 + ) -220 | - - _register(Action("buy_feed", "购买饲料", [ - - Param("quantity", "int", False, config.FEED_BULK, f"份数,默认 {config.FEED_BULK}"), - - ], lambda e, p: e.buy_feed(p.get("quantity", config.FEED_BULK)))) -221 + _register( -222 + Action( -223 + "buy_feed", -224 + "购买饲料", -225 + [ -226 + Param("quantity", "int", False, config.FEED_BULK, f"份数,默认 {config.FEED_BULK}"), -227 + ], -228 + lambda e, p: e.buy_feed(p.get("quantity", config.FEED_BULK)), -229 + ) -230 + ) -231 | --------------------------------------------------------------------------------- -235 | - - _register(Action("pause", "暂停或恢复自动推进时间", [ - - Param("paused", "bool", False, None, "true 暂停 / false 恢复;省略则反转当前状态"), - - ], _pause)) -236 + _register( -237 + Action( -238 + "pause", -239 + "暂停或恢复自动推进时间", -240 + [ -241 + Param("paused", "bool", False, None, "true 暂停 / false 恢复;省略则反转当前状态"), -242 + ], -243 + _pause, -244 + ) -245 + ) -246 | --------------------------------------------------------------------------------- -259 | - - def run_action(engine: FarmEngine, name: Any, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: -260 + def run_action( -261 + engine: FarmEngine, name: Any, params: Optional[Dict[str, Any]] = None -262 + ) -> Dict[str, Any]: -263 | """校验参数并执行动作。 - | - -unformatted: File would be reformatted - --> farm\server\app.py:128:9 - | -127 | f"2. 行动点:播种/浇水/喂食各消耗 1 点,收获与收取产出免费。行动点用完只能等第二天" - - "(POST /api/actions {\"action\":\"sleep\"} 可主动推进一天,有 2 秒冷却)。\n" -128 + '(POST /api/actions {"action":"sleep"} 可主动推进一天,有 2 秒冷却)。\n' -129 | f"3. 水分:作物每天消耗 1 点水分(上限 {config.WATER_CAPACITY}),水分为 0 时停止生长。" --------------------------------------------------------------------------------- -141 | " GET /api/actions?farm=me → 拿到当前所有**合法**动作(含开销与理由)\n" - - " POST /api/actions → {\"action\":\"<上一步里的 action>\"," - - "\"params\":{...}} 直接执行\n" -142 + ' POST /api/actions → {"action":"<上一步里的 action>",' -143 + '"params":{...}} 直接执行\n' -144 | " 响应里的 state 就是执行后的最新状态,无需再查一次。\n" -145 | "\n实时通道:ws://host/ws?farm=me ,服务器在状态变化时推送 state 消息;" - - "也可以直接在 WS 上发送 {\"type\":\"action\",\"action\":\"plant\"," - - "\"params\":{\"plot\":1,\"crop\":\"wheat\"},\"id\":1} 并等待 result 回执。\n" -146 + '也可以直接在 WS 上发送 {"type":"action","action":"plant",' -147 + '"params":{"plot":1,"crop":"wheat"},"id":1} 并等待 result 回执。\n' -148 | ) --------------------------------------------------------------------------------- -252 | session.engine.add_log(f"已存档到 {path}", "info") - - return {"ok": True, "action": "save", "path": path, - - "message": f"已存档到 {path}"} -253 + return {"ok": True, "action": "save", "path": path, "message": f"已存档到 {path}"} -254 | if key == "reset": -255 | session.reset() - - return {"ok": True, "action": "reset", - - "message": f"牧场「{session.farm_id}」已重置,重新开始经营"} -256 + return { -257 + "ok": True, -258 + "action": "reset", -259 + "message": f"牧场「{session.farm_id}」已重置,重新开始经营", -260 + } -261 | return actions_mod.run_action(session.engine, name, params) -262 | - - def run_and_wrap(session: FarmSession, name: Any, params: Any, - - full: bool = False) -> Dict[str, Any]: -263 + def run_and_wrap( -264 + session: FarmSession, name: Any, params: Any, full: bool = False -265 + ) -> Dict[str, Any]: -266 | """执行动作并拼装统一响应。HTTP 与 WS 共用。""" --------------------------------------------------------------------------------- -318 | "log": "GET /api/log?farm=NAME&limit=20&since=0", - - "execute": "POST /api/actions {\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}", - - "farms": "GET /api/farms | POST /api/farms {\"farm\":\"NAME\"}", -319 + "execute": 'POST /api/actions {"action":"plant","params":{"plot":1,"crop":"wheat"}}', -320 + "farms": 'GET /api/farms | POST /api/farms {"farm":"NAME"}', -321 | "realtime": "WS /ws?farm=NAME", --------------------------------------------------------------------------------- -409 | limit = _int(request.query_params.get("limit"), 20, 1, 200) - - since = _int(request.query_params.get("since"), 0, 0, 10 ** 9) -410 + since = _int(request.query_params.get("since"), 0, 0, 10**9) -411 | entries = session.engine.recent_log(limit=limit, since=since) --------------------------------------------------------------------------------- -540 | # 例如牧场数量达到上限:说清楚原因再关,别让客户端以为是网络问题 - - await websocket.send_json({"type": "error", "error": exc.code, - - "message": exc.message}) -541 + await websocket.send_json({"type": "error", "error": exc.code, "message": exc.message}) -542 | await websocket.close(code=1013, reason=exc.message[:100]) --------------------------------------------------------------------------------- -546 | - - await websocket.send_json({ - - "type": "welcome", - - "client_id": client.cid, - - "farm": farm, - - "server": { - - "name": SERVER_NAME, - - "version": API_VERSION, - - "world": cfg.world_name, - - "tick_seconds": session.engine.tick_seconds, - - }, - - "state": session.snapshot(), - - "catalog": session.engine.catalog(), - - "actions": session.engine.available_actions(), - - "log": session.engine.recent_log(limit=12), - - "online": hub.online(farm), - - }) -547 + await websocket.send_json( -548 + { -549 + "type": "welcome", -550 + "client_id": client.cid, -551 + "farm": farm, -552 + "server": { -553 + "name": SERVER_NAME, -554 + "version": API_VERSION, -555 + "world": cfg.world_name, -556 + "tick_seconds": session.engine.tick_seconds, -557 + }, -558 + "state": session.snapshot(), -559 + "catalog": session.engine.catalog(), -560 + "actions": session.engine.available_actions(), -561 + "log": session.engine.recent_log(limit=12), -562 + "online": hub.online(farm), -563 + } -564 + ) -565 | hub.broadcast( -566 | farm, - - {"type": "notice", "farm": farm, - - "text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})"}, -567 + { -568 + "type": "notice", -569 + "farm": farm, -570 + "text": f"{client.name or client.cid} 来到了牧场(在线 {hub.online(farm)})", -571 + }, -572 | exclude=client, --------------------------------------------------------------------------------- -594 | except json.JSONDecodeError as exc: - - await websocket.send_json({"type": "error", "error": "bad_json", - - "message": f"不是合法 JSON:{exc}"}) -595 + await websocket.send_json( -596 + {"type": "error", "error": "bad_json", "message": f"不是合法 JSON:{exc}"} -597 + ) -598 | return -599 | if not isinstance(data, dict): - - await websocket.send_json({"type": "error", "error": "bad_json", - - "message": "消息必须是 JSON 对象"}) -600 + await websocket.send_json( -601 + {"type": "error", "error": "bad_json", "message": "消息必须是 JSON 对象"} -602 + ) -603 | return --------------------------------------------------------------------------------- -612 | if kind in ("sync", "refresh", "state"): - - await websocket.send_json({ - - "type": "sync", - - "id": rid, - - "seq": next_seq(), - - "farm": session.farm_id, - - "state": session.snapshot(), - - "actions": session.engine.available_actions(), - - "log": session.engine.recent_log(limit=20), - - }) -613 + await websocket.send_json( -614 + { -615 + "type": "sync", -616 + "id": rid, -617 + "seq": next_seq(), -618 + "farm": session.farm_id, -619 + "state": session.snapshot(), -620 + "actions": session.engine.available_actions(), -621 + "log": session.engine.recent_log(limit=20), -622 + } -623 + ) -624 | return -625 | -626 | if kind == "help": - - await websocket.send_json({"type": "help", "id": rid, "rules": rules_text(), - - "actions": actions_mod.describe_actions()}) -627 + await websocket.send_json( -628 + { -629 + "type": "help", -630 + "id": rid, -631 + "rules": rules_text(), -632 + "actions": actions_mod.describe_actions(), -633 + } -634 + ) -635 | return --------------------------------------------------------------------------------- -640 | result = await asyncio.to_thread( - - execute_action, session, data.get("action"), -641 + execute_action, -642 + session, -643 + data.get("action"), -644 | data.get("params") or {}, -645 | ) -646 | except GameError as exc: -647 | counters["errors"] += 1 - - await websocket.send_json({"type": "result", "id": rid, "ok": False, - - "error": exc.code, "message": exc.message, - - "farm": session.farm_id}) -648 + await websocket.send_json( -649 + { -650 + "type": "result", -651 + "id": rid, -652 + "ok": False, -653 + "error": exc.code, -654 + "message": exc.message, -655 + "farm": session.farm_id, -656 + } -657 + ) -658 | return -659 | counters["actions"] += 1 -660 | session.touch(count_action=True) - - await websocket.send_json({ - - "type": "result", - - "id": rid, - - "ok": True, - - "farm": session.farm_id, - - "message": result.get("message", "完成"), - - "result": result, - - "state": brief(session, hub), - - # 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送 - - "snapshot": session.snapshot(), - - "log": session.engine.recent_log(limit=3), - - "seq": next_seq(), - - }) -661 + await websocket.send_json( -662 + { -663 + "type": "result", -664 + "id": rid, -665 + "ok": True, -666 + "farm": session.farm_id, -667 + "message": result.get("message", "完成"), -668 + "result": result, -669 + "state": brief(session, hub), -670 + # 回执里带上完整快照:客户端可以立刻刷新画面,不必等下一次推送 -671 + "snapshot": session.snapshot(), -672 + "log": session.engine.recent_log(limit=3), -673 + "seq": next_seq(), -674 + } -675 + ) -676 | return -677 | - - await websocket.send_json({"type": "error", "error": "unknown_message", - - "message": f"不认识的消息类型 {kind!r}", - - "hint": "可用:action / ping / sync / help"}) -678 + await websocket.send_json( -679 + { -680 + "type": "error", -681 + "error": "unknown_message", -682 + "message": f"不认识的消息类型 {kind!r}", -683 + "hint": "可用:action / ping / sync / help", -684 + } -685 + ) -686 | --------------------------------------------------------------------------------- -701 | session.farm_id, - - {"type": "notice", "farm": session.farm_id, - - "text": f"{client.name or client.cid} 离开了牧场(在线 {hub.online(session.farm_id)})"}, -702 + { -703 + "type": "notice", -704 + "farm": session.farm_id, -705 + "text": f"{client.name or client.cid} 离开了牧场(在线 {hub.online(session.farm_id)})", -706 + }, -707 | ) - | - -unformatted: File would be reformatted - --> farm\server\daemon.py:51:60 - | -50 | kernel32 = ctypes.windll.kernel32 - - handle = kernel32.OpenProcess(0x1000, False, pid) # QUERY_LIMITED_INFORMATION -51 + handle = kernel32.OpenProcess(0x1000, False, pid) # QUERY_LIMITED_INFORMATION -52 | if not handle: --------------------------------------------------------------------------------- -70 | - - def __init__(self, host: str = "127.0.0.1", port: int = 8765, - - token: Optional[str] = None, tick: float = 15.0, - - save_dir: str = "", pid_file: str = DEFAULT_PID_FILE, - - log_file: str = DEFAULT_LOG_FILE, python: Optional[str] = None) -> None: -71 + def __init__( -72 + self, -73 + host: str = "127.0.0.1", -74 + port: int = 8765, -75 + token: Optional[str] = None, -76 + tick: float = 15.0, -77 + save_dir: str = "", -78 + pid_file: str = DEFAULT_PID_FILE, -79 + log_file: str = DEFAULT_LOG_FILE, -80 + python: Optional[str] = None, -81 + ) -> None: -82 | self.host = host --------------------------------------------------------------------------------- -150 | sock.settimeout(0.6) - - return sock.connect_ex((self.host if self.host != "0.0.0.0" else "127.0.0.1", - - self.port)) == 0 -151 + return ( -152 + sock.connect_ex((self.host if self.host != "0.0.0.0" else "127.0.0.1", self.port)) -153 + == 0 -154 + ) -155 | -156 | # ------------------------------------------------------------ 启动 -157 | def command(self) -> List[str]: -158 | args = [ - - self.python, "-m", "farm.server", - - "--host", self.host, - - "--port", str(self.port), - - "--tick", str(self.tick), - - "--save-dir", self.save_dir, -159 + self.python, -160 + "-m", -161 + "farm.server", -162 + "--host", -163 + self.host, -164 + "--port", -165 + str(self.port), -166 + "--tick", -167 + str(self.tick), -168 + "--save-dir", -169 + self.save_dir, -170 | # 关键:pid 文件由**服务进程自己**写。 -171 | # Windows 上 venv 的 python.exe 可能是个转发器,父进程拿到的 pid -172 | # 未必是真正在监听端口的那个进程,写错 pid 会导致 stop 杀错人。 - - "--pid-file", self.pid_file, -173 + "--pid-file", -174 + self.pid_file, -175 | ] --------------------------------------------------------------------------------- -190 | os.makedirs(os.path.dirname(self.pid_file), exist_ok=True) - - self.clear_pid() # 清掉上一次留下的陈旧 pid -191 + self.clear_pid() # 清掉上一次留下的陈旧 pid -192 | env = dict(os.environ) --------------------------------------------------------------------------------- -245 | - - return False, ( - - f"服务器进程已启动但 {wait:.0f} 秒内没响应。请查看日志:{self.log_file}" - - ) -246 + return False, (f"服务器进程已启动但 {wait:.0f} 秒内没响应。请查看日志:{self.log_file}") -247 | --------------------------------------------------------------------------------- -277 | if force and os.name == "nt": - - subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], - - capture_output=True, check=False) -278 + subprocess.run( -279 + ["taskkill", "/F", "/T", "/PID", str(pid)], capture_output=True, check=False -280 + ) -281 | if self._wait_offline(pid, 3.0): --------------------------------------------------------------------------------- -308 | url += "?token=" + urllib.parse.quote(self.token) - - request = urllib.request.Request(url, data=b"{}", method="POST", - - headers={"Content-Type": "application/json"}) -309 + request = urllib.request.Request( -310 + url, data=b"{}", method="POST", headers={"Content-Type": "application/json"} -311 + ) -312 | try: - | - -unformatted: File would be reformatted - --> farm\server\hub.py:31:24 - | -30 | - - STATE_INTERVAL = 0.25 # 状态推送的最小间隔(秒),天然把高频操作合并成一次 - - QUEUE_SIZE = 32 # 单连接出口缓冲区;满了就丢最旧的(状态类消息可丢) -31 + STATE_INTERVAL = 0.25 # 状态推送的最小间隔(秒),天然把高频操作合并成一次 -32 + QUEUE_SIZE = 32 # 单连接出口缓冲区;满了就丢最旧的(状态类消息可丢) -33 | --------------------------------------------------------------------------------- -50 | - - __slots__ = ("cid", "farm_id", "loop", "queue", "name", "joined_at", "alive", - - "_sent", "_dropped") -51 + __slots__ = ( -52 + "cid", -53 + "farm_id", -54 + "loop", -55 + "queue", -56 + "name", -57 + "joined_at", -58 + "alive", -59 + "_sent", -60 + "_dropped", -61 + ) -62 | --------------------------------------------------------------------------------- -156 | # ------------------------------------------------------------ 广播 - - def broadcast(self, farm_id: str, message: Dict[str, Any], - - exclude: Optional[Client] = None) -> int: -157 + def broadcast( -158 + self, farm_id: str, message: Dict[str, Any], exclude: Optional[Client] = None -159 + ) -> int: -160 | sent = 0 --------------------------------------------------------------------------------- -231 | session.published_day = day - - self.hub.broadcast(session.farm_id, { - - "type": "day", - - "farm": session.farm_id, - - "day": day, - - "events": session.last_events, - - "at": now, - - }) -232 + self.hub.broadcast( -233 + session.farm_id, -234 + { -235 + "type": "day", -236 + "farm": session.farm_id, -237 + "day": day, -238 + "events": session.last_events, -239 + "at": now, -240 + }, -241 + ) -242 | self.hub.broadcast(session.farm_id, payload) --------------------------------------------------------------------------------- -248 | - - def build_state_message(session, seq: int, note: str = "", extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: -249 + def build_state_message( -250 + session, seq: int, note: str = "", extra: Optional[Dict[str, Any]] = None -251 + ) -> Dict[str, Any]: -252 | """构造给客户端的状态消息(HTTP 与 WS 共用同一份结构)。""" - | - -unformatted: File would be reformatted - --> farm\server\session.py:66:73 - | -65 | - - def __init__(self, farm_id: str, save_dir: str, tick_seconds: float, - - owner: str = "") -> None: -66 + def __init__(self, farm_id: str, save_dir: str, tick_seconds: float, owner: str = "") -> None: -67 | self.farm_id = farm_id --------------------------------------------------------------------------------- -182 | - - def __init__(self, save_dir: str, tick_seconds: float = config.DEFAULT_TICK_SECONDS, - - max_sessions: int = 64, idle_timeout: float = 1800.0) -> None: -183 + def __init__( -184 + self, -185 + save_dir: str, -186 + tick_seconds: float = config.DEFAULT_TICK_SECONDS, -187 + max_sessions: int = 64, -188 + idle_timeout: float = 1800.0, -189 + ) -> None: -190 | self.save_dir = save_dir --------------------------------------------------------------------------------- -209 | "too_many_farms", - - f"服务器牧场数量已达上限({self.max_sessions})," - - "请稍后再试或联系管理员扩容", -210 + f"服务器牧场数量已达上限({self.max_sessions}),请稍后再试或联系管理员扩容", -211 | status=503, --------------------------------------------------------------------------------- -258 | if session.engine.paused: - - continue # 暂停是人为操作,说明玩家还会回来 -259 + continue # 暂停是人为操作,说明玩家还会回来 -260 | if now - session.last_active < self.idle_timeout: --------------------------------------------------------------------------------- -264 | except OSError: - - continue # 存不下就别丢,宁可占着内存 -265 + continue # 存不下就别丢,宁可占着内存 -266 | with self._lock: - | - -unformatted: File would be reformatted - --> farm\server\webui.py:434:9 - | -433 | " -H 'Content-Type: application/json' \\\n" - - " -d '{\"action\":\"plant\",\"params\":{\"plot\":1,\"crop\":\"wheat\"}}'\n" -434 + ' -d \'{"action":"plant","params":{"plot":1,"crop":"wheat"}}\'\n' -435 | "\n" --------------------------------------------------------------------------------- -447 | "------\n" - - "POST /api/actions {\"action\":\"...\",\"params\":{...}} 通用入口\n" - - "POST /api/batch {\"actions\":[{...},{...}]} 批量\n" -448 + 'POST /api/actions {"action":"...","params":{...}} 通用入口\n' -449 + 'POST /api/batch {"actions":[{...},{...}]} 批量\n' -450 | "POST /api/plant|water|harvest|harvest_all|buy_animal|feed|\n" -451 | " collect|buy_feed|buy_plot|sleep|pause|save|reset 便捷入口\n" - - "POST /api/farms {\"farm\":\"名字\"} 创建/进入牧场\n" -452 + 'POST /api/farms {"farm":"名字"} 创建/进入牧场\n' -453 | "POST /api/admin/save 全部存档\n" - | - -unformatted: File would be reformatted - --> farm\tui.py:44:18 - | -43 | ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") - - CELL_WIDTH = 17 # 单块农田格子的显示宽度 -44 + CELL_WIDTH = 17 # 单块农田格子的显示宽度 -45 | CELL_INNER = CELL_WIDTH - 2 - - SIDE_WIDTH = 36 # 右侧栏宽度 -46 + SIDE_WIDTH = 36 # 右侧栏宽度 -47 | MIN_TERM_WIDTH = 76 - - MAX_VISIBLE_ANIMALS = 5 # 侧栏同时最多展示的牲畜数量 -48 + MAX_VISIBLE_ANIMALS = 5 # 侧栏同时最多展示的牲畜数量 -49 | --------------------------------------------------------------------------------- -135 | # ---------------------------------------------------------------- 绘制工具 - - def draw_box(title: str, body: List[str], width: int, accent: Optional[str] = None, - - use_color: bool = True) -> List[str]: -136 + def draw_box( -137 + title: str, body: List[str], width: int, accent: Optional[str] = None, use_color: bool = True -138 + ) -> List[str]: -139 | """画一个带标题的方框,返回行列表(宽度精确等于 ``width``)。""" --------------------------------------------------------------------------------- -168 | - - def __init__(self, engine: Any, save_path: Optional[str] = None, - - use_color: bool = True, autosave: bool = True, - - server_label: str = "") -> None: -169 + def __init__( -170 + self, -171 + engine: Any, -172 + save_path: Optional[str] = None, -173 + use_color: bool = True, -174 + autosave: bool = True, -175 + server_label: str = "", -176 + ) -> None: -177 | # engine 既可以是本地 FarmEngine,也可以是远程 RemoteFarm——只要接口一致 --------------------------------------------------------------------------------- -185 | self.running = True - - self.mode = "main" # main / help / select_crop / select_animal / confirm_reset -186 + self.mode = "main" # main / help / select_crop / select_animal / confirm_reset -187 | self.plot_cursor = 0 --------------------------------------------------------------------------------- -211 | try: - - self.engine.save(self.save_path) # 本地引擎写文件;远程引擎让服务器落盘 -212 + self.engine.save(self.save_path) # 本地引擎写文件;远程引擎让服务器落盘 -213 | except Exception: --------------------------------------------------------------------------------- -620 | filled_count = max(0, min(10, int(round(ratio * 10)))) - - progress = color("█" * filled_count + "░" * (10 - filled_count), - - C_GREEN if plot["is_ripe"] else C_CYAN, self.use_color) -621 + progress = color( -622 + "█" * filled_count + "░" * (10 - filled_count), -623 + C_GREEN if plot["is_ripe"] else C_CYAN, -624 + self.use_color, -625 + ) -626 | --------------------------------------------------------------------------------- -640 | body = [ - - color("│", border, self.use_color) + pad(line1, CELL_INNER) + color("│", border, self.use_color), - - color("│", border, self.use_color) + pad(line2, CELL_INNER) + color("│", border, self.use_color), -641 + color("│", border, self.use_color) -642 + + pad(line1, CELL_INNER) -643 + + color("│", border, self.use_color), -644 + color("│", border, self.use_color) -645 + + pad(line2, CELL_INNER) -646 + + color("│", border, self.use_color), -647 | ] --------------------------------------------------------------------------------- -658 | # 只渲染一个跟随光标的窗口,避免牲畜变多后把界面撑爆 - - start = max(0, min(max(0, len(animals) - MAX_VISIBLE_ANIMALS), - - self.animal_cursor - MAX_VISIBLE_ANIMALS // 2)) -659 + start = max( -660 + 0, -661 + min( -662 + max(0, len(animals) - MAX_VISIBLE_ANIMALS), -663 + self.animal_cursor - MAX_VISIBLE_ANIMALS // 2, -664 + ), -665 + ) -666 | for index in range(start, min(len(animals), start + MAX_VISIBLE_ANIMALS)): --------------------------------------------------------------------------------- -671 | if animal["pending"] > 0: - - status = color(f"{animal['product']}x{animal['pending']}", - - C_BOLD + C_GOLD, self.use_color) -672 + status = color( -673 + f"{animal['product']}x{animal['pending']}", C_BOLD + C_GOLD, self.use_color -674 + ) -675 | elif animal["is_sick"]: --------------------------------------------------------------------------------- -683 | - - head = marker + color(f" #{animal['id']} {animal['name']}", - - C_WHITE, self.use_color) -684 + head = marker + color(f" #{animal['id']} {animal['name']}", C_WHITE, self.use_color) -685 | rows.append(pad(head, 13) + status) --------------------------------------------------------------------------------- -689 | rows.append(color(f" …还有 {hidden} 只(Tab 切换)", C_DIM, self.use_color)) - - pasture = draw_box(f"牧场 {snap['animal_count']}/{snap['max_animals']}", rows, - - side_width, accent=C_ORANGE, use_color=self.use_color) -690 + pasture = draw_box( -691 + f"牧场 {snap['animal_count']}/{snap['max_animals']}", -692 + rows, -693 + side_width, -694 + accent=C_ORANGE, -695 + use_color=self.use_color, -696 + ) -697 | --------------------------------------------------------------------------------- -719 | ) - - market = draw_box("市场行情", market_rows, side_width, - - accent=C_CYAN, use_color=self.use_color) -720 + market = draw_box( -721 + "市场行情", market_rows, side_width, accent=C_CYAN, use_color=self.use_color -722 + ) -723 | --------------------------------------------------------------------------------- -780 | rows = [ - - color(" 玩法目标:靠种地和养殖把净资产做到 10000 金币,解锁全部 5 个里程碑。", C_WHITE, self.use_color), -781 + color( -782 + " 玩法目标:靠种地和养殖把净资产做到 10000 金币,解锁全部 5 个里程碑。", -783 + C_WHITE, -784 + self.use_color, -785 + ), -786 | "", -787 | color(" 时间", C_BOLD + C_GOLD, self.use_color) - - + color(" 每 15 秒自动推进一天,作物成长、牲畜产奶、市场价格都在这一天结算。", C_GRAY, self.use_color), -788 + + color( -789 + " 每 15 秒自动推进一天,作物成长、牲畜产奶、市场价格都在这一天结算。", -790 + C_GRAY, -791 + self.use_color, -792 + ), -793 | color(" 水分", C_BOLD + C_GOLD, self.use_color) - - + color(" 作物每天消耗 1 点水分,水为 0 就停止生长。按 W 浇水恢复到 3 点。", C_GRAY, self.use_color), -794 + + color( -795 + " 作物每天消耗 1 点水分,水为 0 就停止生长。按 W 浇水恢复到 3 点。", -796 + C_GRAY, -797 + self.use_color, -798 + ), -799 | color(" 行动点", C_BOLD + C_GOLD, self.use_color) - - + color(" 每天 10 点,播种/浇水/喂食各消耗 1 点,收获和收取免费。", C_GRAY, self.use_color), -800 + + color( -801 + " 每天 10 点,播种/浇水/喂食各消耗 1 点,收获和收取免费。", C_GRAY, self.use_color -802 + ), -803 | color(" 牲畜", C_BOLD + C_GOLD, self.use_color) - - + color(" 必须每天喂食才会产出;连续 3 天没吃东西会离开农场。", C_GRAY, self.use_color), -804 + + color( -805 + " 必须每天喂食才会产出;连续 3 天没吃东西会离开农场。", C_GRAY, self.use_color -806 + ), -807 | color(" 市场", C_BOLD + C_GOLD, self.use_color) -808 | + color(" 作物和畜产品价格每天波动 ±18%,高点卖出更赚。", C_GRAY, self.use_color), -809 | "", -810 | color(" 快捷键", C_BOLD + C_GOLD, self.use_color) - - + color(" P 播种 / W 浇水 / H 收获 / E 一键收获 / F 喂食 / C 收取产出", C_GRAY, self.use_color), -811 + + color( -812 + " P 播种 / W 浇水 / H 收获 / E 一键收获 / F 喂食 / C 收取产出", -813 + C_GRAY, -814 + self.use_color, -815 + ), -816 | color(" ", C_GRAY, self.use_color) - - + color(" B 买动物 / L 开垦土地 / K 买饲料 / N 休息 / 空格 暂停 / S 存档 / R 重置", C_GRAY, self.use_color), -817 + + color( -818 + " B 买动物 / L 开垦土地 / K 买饲料 / N 休息 / 空格 暂停 / S 存档 / R 重置", -819 + C_GRAY, -820 + self.use_color, -821 + ), -822 | "", -823 | color(" AI 接入", C_BOLD + C_GOLD, self.use_color) - - + color(" 游戏跑在服务器进程里,HTTP 接口与 WebSocket 通道同时开放,", - - C_GRAY, self.use_color), -824 + + color( -825 + " 游戏跑在服务器进程里,HTTP 接口与 WebSocket 通道同时开放,", -826 + C_GRAY, -827 + self.use_color, -828 + ), -829 | color(" ", C_GRAY, self.use_color) - - + color(" AI 与你在同一个农场里操作(见 README「给 AI 的接口」)。", - - C_GRAY, self.use_color), -830 + + color( -831 + " AI 与你在同一个农场里操作(见 README「给 AI 的接口」)。", C_GRAY, self.use_color -832 + ), -833 | "", - | - -unformatted: File would be reformatted - --> main.py:101:26 - | -100 | p_serve.add_argument("--daemon", action="store_true", help="转到后台运行(守护进程)") - - p_serve.add_argument("--host", default="127.0.0.1", - - help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0") -101 + p_serve.add_argument( -102 + "--host", default="127.0.0.1", help="监听地址,默认 127.0.0.1;局域网联机用 0.0.0.0" -103 + ) -104 | p_serve.add_argument("--port", type=int, default=8765, help="监听端口,默认 8765") -105 | p_serve.add_argument("--token", default=None, help="访问令牌,设置后接口需要携带") - - p_serve.add_argument("--tick", type=float, default=config.DEFAULT_TICK_SECONDS, - - help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒") -106 + p_serve.add_argument( -107 + "--tick", -108 + type=float, -109 + default=config.DEFAULT_TICK_SECONDS, -110 + help=f"每多少秒推进一天,默认 {config.DEFAULT_TICK_SECONDS:.0f} 秒", -111 + ) -112 | p_serve.add_argument("--save-dir", default=None, help="存档目录,默认 <项目>/saves") -113 | p_serve.add_argument("--world", default="SimpleFarm 公共农场", help="世界名称") -114 | p_serve.add_argument("--max-sessions", type=int, default=64, help="同时在线的牧场上限") - - p_serve.add_argument("--idle-timeout", type=float, default=1800.0, - - help="无人访问多少秒后把牧场移出内存(存档保留)") -115 + p_serve.add_argument( -116 + "--idle-timeout", -117 + type=float, -118 + default=1800.0, -119 + help="无人访问多少秒后把牧场移出内存(存档保留)", -120 + ) -121 | p_serve.add_argument("--no-remote-control", action="store_true", help="禁止接口停机") -122 | p_serve.add_argument("--access-log", action="store_true", help="打印每个 HTTP 请求") - - p_serve.add_argument("--log-level", default="info", - - choices=["critical", "error", "warning", "info", "debug"]) -123 + p_serve.add_argument( -124 + "--log-level", default="info", choices=["critical", "error", "warning", "info", "debug"] -125 + ) -126 | p_serve.add_argument("--log-file", default=None, help="守护进程日志路径") --------------------------------------------------------------------------------- -151 | p_play.add_argument("--no-color", action="store_true") - - p_play.add_argument("--no-realtime", action="store_true", - - help="不用 WebSocket,改走 HTTP 轮询") -152 + p_play.add_argument("--no-realtime", action="store_true", help="不用 WebSocket,改走 HTTP 轮询") -153 | --------------------------------------------------------------------------------- -161 | p_do.add_argument("action", help="动作名:plant / water / harvest / ...") - - p_do.add_argument("params", nargs="?", default="{}", - - help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\'') -162 + p_do.add_argument( -163 + "params", nargs="?", default="{}", help='JSON 参数,例如 \'{"plot":1,"crop":"wheat"}\'' -164 + ) -165 | _add_client_args(p_do) --------------------------------------------------------------------------------- -214 | return RemoteFarm( - - endpoint=args.url, farm=getattr(args, "farm", "default"), -215 + endpoint=args.url, -216 + farm=getattr(args, "farm", "default"), -217 | token=getattr(args, "token", None), -218 | name=getattr(args, "name", "") or "cli", - - realtime=realtime, autostart=True, -219 + realtime=realtime, -220 + autostart=True, -221 | ) --------------------------------------------------------------------------------- -235 | argv = [ - - "--host", args.host, "--port", str(args.port), - - "--tick", str(args.tick), "--world", args.world, - - "--max-sessions", str(args.max_sessions), - - "--idle-timeout", str(args.idle_timeout), - - "--log-level", args.log_level, -236 + "--host", -237 + args.host, -238 + "--port", -239 + str(args.port), -240 + "--tick", -241 + str(args.tick), -242 + "--world", -243 + args.world, -244 + "--max-sessions", -245 + str(args.max_sessions), -246 + "--idle-timeout", -247 + str(args.idle_timeout), -248 + "--log-level", -249 + args.log_level, -250 | ] --------------------------------------------------------------------------------- -307 | for row in world["leaderboard"][:5]: - - print(f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}" - - f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}") -308 + print( -309 + f" #{row['rank']} {row['farm']:<14} 净资产 {row['net_worth']:>8.0f}" -310 + f" · 第 {row['day']} 天 · Lv.{row['level']} · 在线 {row['online']}" -311 + ) -312 | --------------------------------------------------------------------------------- -342 | label = f"{client.farm} @ {client.host}" - - app = TuiApp(client, save_path=None, use_color=not args.no_color, - - autosave=True, server_label=label) -343 + app = TuiApp( -344 + client, save_path=None, use_color=not args.no_color, autosave=True, server_label=label -345 + ) -346 | try: --------------------------------------------------------------------------------- -392 | except json.JSONDecodeError as exc: - - return die(f"参数不是合法 JSON:{exc}", '例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'') -393 + return die( -394 + f"参数不是合法 JSON:{exc}", -395 + '例子:python main.py do plant \'{"plot":1,"crop":"wheat"}\'', -396 + ) -397 | if not isinstance(params, dict): --------------------------------------------------------------------------------- -403 | except GameError as exc: - - return die(f"{args.action} 失败({exc.code}):{exc.message}", - - "看看现在能做什么:python main.py actions") -404 + return die( -405 + f"{args.action} 失败({exc.code}):{exc.message}", -406 + "看看现在能做什么:python main.py actions", -407 + ) -408 | - - print(ok("✓ ") + str(result.get("message", "完成")) - - + dim(f" ({time.time() - started:.2f}s)")) - - extra = {k: v for k, v in result.items() - - if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list))} -409 + print(ok("✓ ") + str(result.get("message", "完成")) + dim(f" ({time.time() - started:.2f}s)")) -410 + extra = { -411 + k: v -412 + for k, v in result.items() -413 + if k not in ("ok", "message", "state", "log", "action") and not isinstance(v, (dict, list)) -414 + } -415 | if extra: -416 | print(dim(" " + " ".join(f"{k}={v}" for k, v in extra.items()))) -417 | state = result.get("state") or {} -418 | if state: - - print(f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " - - f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}") -419 + print( -420 + f" 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " -421 + f"行动点 {state.get('ap')}/{state.get('max_ap')} · 净资产 {state.get('net_worth')}" -422 + ) -423 | return 0 --------------------------------------------------------------------------------- -432 | except json.JSONDecodeError as exc: - - return die(f"actions 不是合法 JSON:{exc}", - - '例子:python main.py batch \'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'') -433 + return die( -434 + f"actions 不是合法 JSON:{exc}", -435 + '例子:python main.py batch \'[{"action":"harvest_all"},{"action":"buy_feed","params":{"quantity":5}}]\'', -436 + ) -437 | if not isinstance(items, list): --------------------------------------------------------------------------------- -453 | items = client.available_actions() - - print(f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " - - f"行动点 {state.get('ap')}/{state.get('max_ap')}") -454 + print( -455 + f"牧场 {client.farm} · 第 {state.get('day')} 天 · 金币 {state.get('gold')} · " -456 + f"行动点 {state.get('ap')}/{state.get('max_ap')}" -457 + ) -458 | if not items: --------------------------------------------------------------------------------- -465 | cost_text = " ".join(f"{k}:{v}" for k, v in cost.items()) or "免费" - - print(f" [{index:>2}] {item['action']:<12} {json.dumps(item['params'], ensure_ascii=False):<42}" - - f" ({cost_text})") -466 + print( -467 + f" [{index:>2}] {item['action']:<12} {json.dumps(item['params'], ensure_ascii=False):<42}" -468 + f" ({cost_text})" -469 + ) -470 | print(dim(f" {item.get('reason', '')}")) --------------------------------------------------------------------------------- -481 | print(f"牧场 {snap.get('farm')}") - - print(f"第 {snap.get('day')} 天" - - + (f" · {snap.get('seconds_to_tick')}s 后进入下一天" - - if snap.get("seconds_to_tick") is not None else " · 已暂停")) - - print(f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} " - - f"({snap.get('exp')}/{snap.get('exp_needed')})") -482 + print( -483 + f"第 {snap.get('day')} 天" -484 + + ( -485 + f" · {snap.get('seconds_to_tick')}s 后进入下一天" -486 + if snap.get("seconds_to_tick") is not None -487 + else " · 已暂停" -488 + ) -489 + ) -490 + print( -491 + f"金币 {snap.get('gold')} 等级 Lv.{snap.get('level')} " -492 + f"({snap.get('exp')}/{snap.get('exp_needed')})" -493 + ) -494 | print(f"行动点 {snap.get('ap')}/{snap.get('max_ap')} 饲料 {snap.get('feed')}") --------------------------------------------------------------------------------- -500 | status = "可收获" if plot["is_ripe"] else f"还需{plot['days_left']}天" - - print(f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}" - - f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}") -501 + print( -502 + f" {plot['position']:>2} 号地 {plot['crop_name']:<6} 生长 {plot['growth']}" -503 + f"/{plot['grow_days']} 水分 {plot['moisture']} · {status}" -504 + ) -505 | for animal in snap.get("animals", []): - - note = (f"{animal['product']} x{animal['pending']} 待收" if animal["pending"] - - else ("已喂食" if animal["fed"] else "未喂食")) -506 + note = ( -507 + f"{animal['product']} x{animal['pending']} 待收" -508 + if animal["pending"] -509 + else ("已喂食" if animal["fed"] else "未喂食") -510 + ) -511 | print(f" #{animal['id']} {animal['name']:<4} {note}") --------------------------------------------------------------------------------- -537 | online = snap.get("online") - - head = (f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | " - - f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | " - - f"净资产 {snap.get('net_worth')}") -538 + head = ( -539 + f"第 {snap.get('day')} 天 | 金币 {snap.get('gold')} | " -540 + f"Lv.{snap.get('level')} | 行动点 {snap.get('ap')}/{snap.get('max_ap')} | " -541 + f"净资产 {snap.get('net_worth')}" -542 + ) -543 | if online is not None: --------------------------------------------------------------------------------- -572 | world = client.world() - - print(f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · " - - f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个") -573 + print( -574 + f"世界:{world.get('world')} · 已运行 {world.get('uptime', 0):.0f} 秒 · " -575 + f"牧场 {world.get('farms')} 个 · 连接 {world.get('clients')} 个" -576 + ) -577 | print(f"时钟:每 {world.get('tick_seconds')} 秒推进一天\n") -578 | print(f" {'#':>2} {'牧场':<14} {'净资产':>9} {'金币':>7} {'天':>4} {'等级':>4} {'在线':>4}") -579 | for row in world.get("leaderboard", []): - - print(f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} " - - f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}") -580 + print( -581 + f" {row['rank']:>2} {row['farm']:<14} {row['net_worth']:>9.0f} " -582 + f"{row['gold']:>7} {row['day']:>4} {row['level']:>4} {row['online']:>4}" -583 + ) -584 | return 0 --------------------------------------------------------------------------------- -660 | health = info.get("health") or {} - - print(ok("✓ 服务器正在运行") - - + f" http://{info['host']}:{info['port']}/ " - - + dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}")) -661 + print( -662 + ok("✓ 服务器正在运行") -663 + + f" http://{info['host']}:{info['port']}/ " -664 + + dim(f"牧场 {health.get('farms', 0)} 个 · 在线 {health.get('clients', 0)}") -665 + ) -666 | else: --------------------------------------------------------------------------------- -677 | return handler(args) - - except SystemExit as exc: # make_client 里的友好退出 -678 + except SystemExit as exc: # make_client 里的友好退出 -679 | return int(exc.code or 0) - | - -unformatted: File would be reformatted - --> tests\test_engine.py:19:26 - | -18 | - - from farm import config # noqa: E402 -19 + from farm import config # noqa: E402 -20 | from farm.engine import FarmEngine, GameError # noqa: E402 - - from farm.models import GameState, Plot # noqa: E402 -21 + from farm.models import GameState, Plot # noqa: E402 -22 | from farm.storage import load_state, save_state # noqa: E402 --------------------------------------------------------------------------------- -192 | def test_buy_plot_without_gold(self) -> None: - - self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步 -193 + self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步 -194 | self.engine.state.gold = 0 --------------------------------------------------------------------------------- -199 | def test_buy_plot_blocked_at_level_cap(self) -> None: - - self.engine.state.gold = 10 ** 6 -200 + self.engine.state.gold = 10**6 -201 | with self.assertRaises(GameError) as ctx: --------------------------------------------------------------------------------- -264 | def test_pasture_limit(self) -> None: - - self.engine.state.gold = 10 ** 6 -265 + self.engine.state.gold = 10**6 -266 | for _ in range(config.MAX_ANIMALS): --------------------------------------------------------------------------------- -282 | def test_buy_feed_validation(self) -> None: - - self.engine.state.gold = 10 ** 6 -283 + self.engine.state.gold = 10**6 -284 | with self.assertRaises(GameError): --------------------------------------------------------------------------------- -352 | self.engine.buy_animal("chicken") - - self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言 -353 + self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言 -354 | self.engine.state.feed = 7 --------------------------------------------------------------------------------- -404 | self.assertEqual(state.level, 1) - - self.assertIsNone(state.plots[0].crop) # 非法作物被清空 - - self.assertEqual(state.animals, []) # 非法动物被丢弃 - - self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值 -405 + self.assertIsNone(state.plots[0].crop) # 非法作物被清空 -406 + self.assertEqual(state.animals, []) # 非法动物被丢弃 -407 + self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值 -408 | self.assertGreaterEqual(len(state.plots), config.BASE_PLOTS) -409 | -410 | # ============================================================ 并发 -411 | def test_concurrent_operations_are_safe(self) -> None: - - self.engine.state.gold = 10 ** 6 -412 + self.engine.state.gold = 10**6 -413 | errors = [] - | - -unformatted: File would be reformatted - --> tests\test_server.py:35:22 - | -34 | HAS_FASTAPI = True - - except ImportError: # pragma: no cover - 取决于环境 -35 + except ImportError: # pragma: no cover - 取决于环境 -36 | HAS_FASTAPI = False --------------------------------------------------------------------------------- -41 | HAS_UVICORN = True - - except ImportError: # pragma: no cover -42 + except ImportError: # pragma: no cover -43 | HAS_UVICORN = False --------------------------------------------------------------------------------- -233 | self.tmp = tempfile.mkdtemp(prefix="farm-api-") - - self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, - - tick_seconds=60) -234 + self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, tick_seconds=60) -235 | self.app = create_app(self.cfg) --------------------------------------------------------------------------------- -278 | self.assertIn("已种下", data["message"]) - - self.assertEqual(data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"]) -279 + self.assertEqual( -280 + data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"] -281 + ) -282 | --------------------------------------------------------------------------------- -287 | """{"action":"plant","plot":1,"crop":"wheat"} 这种平铺写法也要能用。""" - - data = self.client.post("/api/actions?farm=flat", - - json={"action": "plant", "plot": 1, "crop": "wheat"}).json() -288 + data = self.client.post( -289 + "/api/actions?farm=flat", json={"action": "plant", "plot": 1, "crop": "wheat"} -290 + ).json() -291 | self.assertTrue(data["ok"]) --------------------------------------------------------------------------------- -298 | def test_action_error_shape(self) -> None: - - response = self.client.post("/api/actions?farm=err", - - json={"action": "plant", "params": {"plot": 1, "crop": "nope"}}) -299 + response = self.client.post( -300 + "/api/actions?farm=err", json={"action": "plant", "params": {"plot": 1, "crop": "nope"}} -301 + ) -302 | self.assertEqual(response.status_code, 400) --------------------------------------------------------------------------------- -308 | def test_out_of_range_plot(self) -> None: - - response = self.client.post("/api/actions?farm=err", - - json={"action": "water", "params": {"plot": 999}}) -309 + response = self.client.post( -310 + "/api/actions?farm=err", json={"action": "water", "params": {"plot": 999}} -311 + ) -312 | self.assertEqual(response.status_code, 400) -313 | self.assertEqual(response.json()["error"], "no_such_plot") -314 | -315 | def test_bad_json_body(self) -> None: - - response = self.client.post("/api/actions?farm=err", content=b"{oops", - - headers={"Content-Type": "application/json"}) -316 + response = self.client.post( -317 + "/api/actions?farm=err", content=b"{oops", headers={"Content-Type": "application/json"} -318 + ) -319 | self.assertEqual(response.status_code, 400) --------------------------------------------------------------------------------- -324 | names = {item["action"] for item in data["actions"]} - - self.assertIn("plant", names) # 有空地 + 有钱 → 可播种 - - self.assertNotIn("harvest", names) # 还没种东西 → 不可收获 -325 + self.assertIn("plant", names) # 有空地 + 有钱 → 可播种 -326 + self.assertNotIn("harvest", names) # 还没种东西 → 不可收获 -327 | -328 | def test_batch_endpoint(self) -> None: - - items = {"actions": [ - - {"action": "buy_feed", "params": {"quantity": 5}}, - - {"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, - - {"action": "harvest", "params": {"plot": 1}}, # 必然失败 - - ]} -329 + items = { -330 + "actions": [ -331 + {"action": "buy_feed", "params": {"quantity": 5}}, -332 + {"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, -333 + {"action": "harvest", "params": {"plot": 1}}, # 必然失败 -334 + ] -335 + } -336 | data = self.client.post("/api/batch?farm=multi", json=items).json() --------------------------------------------------------------------------------- -341 | def test_log_entries_and_since(self) -> None: - - self.client.post("/api/actions?farm=logging", - - json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}) -342 + self.client.post( -343 + "/api/actions?farm=logging", -344 + json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, -345 + ) -346 | entries = self.client.get("/api/log?farm=logging").json()["entries"] --------------------------------------------------------------------------------- -355 | def test_farms_list_and_isolation(self) -> None: - - self.client.post("/api/actions?farm=alice", - - json={"action": "buy_feed", "params": {"quantity": 10}}) -356 + self.client.post( -357 + "/api/actions?farm=alice", json={"action": "buy_feed", "params": {"quantity": 10}} -358 + ) -359 | self.client.post("/api/actions?farm=bob", json={"action": "save"}) -360 | rows = self.client.get("/api/farms").json()["farms"] -361 | by_name = {row["farm"]: row for row in rows} -362 | self.assertIn("alice", by_name) - - self.assertEqual(by_name["alice"]["gold"], - - config.START_GOLD - 10 * config.FEED_UNIT_PRICE) -363 + self.assertEqual(by_name["alice"]["gold"], config.START_GOLD - 10 * config.FEED_UNIT_PRICE) -364 | --------------------------------------------------------------------------------- -381 | def test_admin_save(self) -> None: - - self.client.post("/api/actions?farm=saver", - - json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}) -382 + self.client.post( -383 + "/api/actions?farm=saver", -384 + json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}}, -385 + ) -386 | data = self.client.post("/api/admin/save").json() --------------------------------------------------------------------------------- -432 | def test_action_blocked_without_token(self) -> None: - - response = self.client.post("/api/actions", - - json={"action": "buy_plot"}) -433 + response = self.client.post("/api/actions", json={"action": "buy_plot"}) -434 | self.assertEqual(response.status_code, 401) --------------------------------------------------------------------------------- -448 | cls.url = f"http://127.0.0.1:{cls.port}" - - app = create_app(ServerConfig(host="127.0.0.1", port=cls.port, save_dir=cls.tmp, - - tick_seconds=600, autosave_interval=3600)) -449 + app = create_app( -450 + ServerConfig( -451 + host="127.0.0.1", -452 + port=cls.port, -453 + save_dir=cls.tmp, -454 + tick_seconds=600, -455 + autosave_interval=3600, -456 + ) -457 + ) -458 | import uvicorn -459 | - - cls.server = uvicorn.Server(uvicorn.Config( - - app, host="127.0.0.1", port=cls.port, log_level="warning", - - access_log=False, lifespan="on", - - )) -460 + cls.server = uvicorn.Server( -461 + uvicorn.Config( -462 + app, -463 + host="127.0.0.1", -464 + port=cls.port, -465 + log_level="warning", -466 + access_log=False, -467 + lifespan="on", -468 + ) -469 + ) -470 | cls.thread = threading.Thread(target=cls.server.run, name="live-server", daemon=True) --------------------------------------------------------------------------------- -479 | time.sleep(0.2) - - else: # pragma: no cover -480 + else: # pragma: no cover -481 | raise unittest.SkipTest("服务器没能在 20 秒内起来") --------------------------------------------------------------------------------- -491 | - - return RemoteFarm(endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, - - autostart=True) -492 + return RemoteFarm( -493 + endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, autostart=True -494 + ) -495 | --------------------------------------------------------------------------------- -506 | with self.assertRaises(GameError) as ctx: - - client.plant(1, "carrot") # 地已经占用了 -507 + client.plant(1, "carrot") # 地已经占用了 -508 | self.assertEqual(ctx.exception.code, "plot_busy") -509 | -510 | actions = client.available_actions() -511 | names = {item["action"] for item in actions} - - self.assertIn("plant", names) # 还有空地可以种 - - self.assertNotIn("harvest", names) # 小麦还没成熟 -512 + self.assertIn("plant", names) # 还有空地可以种 -513 + self.assertNotIn("harvest", names) # 小麦还没成熟 -514 | - - client.water(1) # 播种后水分 2/3,可以补满 -515 + client.water(1) # 播种后水分 2/3,可以补满 -516 | self.assertEqual(client.state.plots[0].moisture, config.WATER_CAPACITY) -517 | with self.assertRaises(GameError) as ctx: - - client.water(1) # 水分已满,再浇就是错的 -518 + client.water(1) # 水分已满,再浇就是错的 -519 | self.assertEqual(ctx.exception.code, "already_watered") --------------------------------------------------------------------------------- -541 | time.sleep(0.1) - - self.assertEqual(watcher.state.plots[0].crop, "carrot", - - "实时通道应该把另一方的操作推过来") -542 + self.assertEqual( -543 + watcher.state.plots[0].crop, "carrot", "实时通道应该把另一方的操作推过来" -544 + ) -545 | self.assertEqual(watcher.mode, "ws") - | - -unformatted: File would be reformatted - --> tools\preview_tui.py:30:30 - | -29 | from farm.engine import FarmEngine # noqa: E402 - - from farm.tui import TuiApp # noqa: E402 -30 + from farm.tui import TuiApp # noqa: E402 -31 | --------------------------------------------------------------------------------- -39 | client = RemoteFarm(endpoint=url, farm=farm, token=token, realtime=False) - - app = TuiApp(client, use_color=True, autosave=False, - - server_label=f"{client.farm} @ {client.host}") -40 + app = TuiApp( -41 + client, use_color=True, autosave=False, server_label=f"{client.farm} @ {client.host}" -42 + ) -43 | return client, app --------------------------------------------------------------------------------- -103 | parser.add_argument("--color", action="store_true", help="保留 ANSI 颜色") - - parser.add_argument("--url", default=None, - - help="连到真实服务器渲染(例如 http://127.0.0.1:8765)") -104 + parser.add_argument( -105 + "--url", default=None, help="连到真实服务器渲染(例如 http://127.0.0.1:8765)" -106 + ) -107 | parser.add_argument("--farm", default="default", help="--url 模式下要看的牧场") --------------------------------------------------------------------------------- -116 | state = engine.snapshot() - - print(f"# 来自真实服务器 {args.url} · 牧场 {args.farm} · 第 {state.get('day')} 天 · " - - f"金币 {state.get('gold')} · 在线 {state.get('online')}\n") -117 + print( -118 + f"# 来自真实服务器 {args.url} · 牧场 {args.farm} · 第 {state.get('day')} 天 · " -119 + f"金币 {state.get('gold')} · 在线 {state.get('online')}\n" -120 + ) -121 | else: - | - -unformatted: File would be reformatted - --> tools\ws_demo.py:41:15 - | -40 | state = welcome.get("state", {}) - - print(f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · " - - f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个") -41 + print( -42 + f"← welcome:牧场 {welcome['farm']} · 第 {state.get('day')} 天 · " -43 + f"金币 {state.get('gold')} · 候选动作 {len(welcome.get('actions', []))} 个" -44 + ) -45 | --------------------------------------------------------------------------------- -65 | if kind == "result": - - print(f"← result(id={message.get('id')}):{message.get('message')} " - - f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]") -66 + print( -67 + f"← result(id={message.get('id')}):{message.get('message')} " -68 + f"[通过 WebSocket 提交的动作,ok={message.get('ok')}]" -69 + ) -70 | elif kind == "state": -71 | snapshot = message.get("state", {}) - - print(f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · " - - f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · " - - f"在线 {message.get('online')}") -72 + print( -73 + f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · " -74 + f"金币 {snapshot.get('gold')} · 行动点 {snapshot.get('ap')} · " -75 + f"在线 {message.get('online')}" -76 + ) -77 | elif kind == "day": - | - -23 files would be reformatted, 4 files already formatted diff --git a/_sync.log b/_sync.log deleted file mode 100644 index 19c63c5..0000000 --- a/_sync.log +++ /dev/null @@ -1,33 +0,0 @@ -Resolved 28 packages in 7ms -Downloading ruff (10.1MiB) - Downloaded ruff -Prepared 6 packages in 4m 29s -warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance. - If the cache and target directories are on different filesystems, hardlinking may not be supported. - If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning. -Installed 24 packages in 12.11s - + annotated-doc==0.0.5 - + annotated-types==0.8.0 - + anyio==4.15.1 - + certifi==2026.7.22 - + click==8.5.0 - + fastapi==0.141.1 - + h11==0.16.0 - + httpcore==1.0.9 - + httptools==0.8.0 - + httpx==0.28.1 - + idna==3.20 - + nodeenv==1.10.0 - + pydantic==2.13.5 - + pydantic-core==2.46.5 - + pyright==1.1.414 - + python-dotenv==1.2.3 - + pyyaml==6.0.3 - + ruff==0.16.8 - + starlette==1.6.0 - + typing-extensions==4.16.0 - + typing-inspection==0.4.4 - + uvicorn==0.53.0 - + watchfiles==1.2.0 - + websockets==17.1 -EXIT=0 diff --git a/_uvlock.log b/_uvlock.log deleted file mode 100644 index ba2ca8a..0000000 --- a/_uvlock.log +++ /dev/null @@ -1,294 +0,0 @@ -DEBUG Found workspace root: `D:\SimpleFarm` -DEBUG Adding root workspace member: `D:\SimpleFarm` -DEBUG Found workspace configuration at `D:\SimpleFarm\pyproject.toml` -DEBUG Searching for user configuration in: `C:\Users\Administrator\AppData\Roaming\uv\uv.toml` -DEBUG uv 0.12.14 (ce3bd7931 2026-09-14 x86_64-pc-windows-msvc) -DEBUG Found project root: `D:\SimpleFarm` -DEBUG Reading Python requests from version file at `D:\SimpleFarm\.python-version` -DEBUG Using Python request `3.13` from version file at `.python-version` -DEBUG Checking for Python environment at: `.venv` -DEBUG The project environment's Python version satisfies the request: `Python 3.13` -DEBUG Using request connect timeout of 10s and read timeout of 45s -DEBUG Found static `pyproject.toml` for: simplefarm @ file:///D:/SimpleFarm -DEBUG Solving with installed Python version: 3.13.15 -DEBUG Solving with target Python version: >=3.10 -DEBUG Adding direct dependency: simplefarm* -DEBUG Adding direct dependency: simplefarm:dev* -DEBUG Searching for a compatible version of simplefarm @ file:///D:/SimpleFarm (*) -DEBUG Adding direct dependency: fastapi>=0.110 -DEBUG Adding direct dependency: uvicorn[standard]>=0.27 -DEBUG Searching for a compatible version of simplefarm @ file:///D:/SimpleFarm (*) -DEBUG Adding direct dependency: simplefarm:dev==2.0.0 -DEBUG Searching for a compatible version of simplefarm @ file:///D:/SimpleFarm (==2.0.0) -DEBUG Adding direct dependency: httpx>=0.27 -DEBUG Adding direct dependency: pyright>=1.1.390 -DEBUG Adding direct dependency: ruff>=0.8.0 -DEBUG Found fresh response for: https://pypi.org/simple/fastapi/ -DEBUG Found fresh response for: https://pypi.org/simple/uvicorn/ -DEBUG Searching for a compatible version of fastapi (>=0.110) -DEBUG Selecting: fastapi==0.141.1 [compatible] (fastapi-0.141.1-py3-none-any.whl) -DEBUG Found fresh response for: https://pypi.org/simple/httpx/ -DEBUG Found fresh response for: https://pypi.org/simple/pyright/ -DEBUG Found fresh response for: https://pypi.org/simple/ruff/ -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl.metadata -DEBUG Adding transitive dependency for fastapi==0.141.1: annotated-doc>=0.0.2 -DEBUG Adding transitive dependency for fastapi==0.141.1: pydantic>=2.9.0 -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl.metadata -DEBUG Adding transitive dependency for fastapi==0.141.1: starlette>=0.46.0 -DEBUG Adding transitive dependency for fastapi==0.141.1: typing-extensions>=4.8.0 -DEBUG Adding transitive dependency for fastapi==0.141.1: typing-inspection>=0.4.2 -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl.metadata -DEBUG Searching for a compatible version of uvicorn[standard] (>=0.27) -DEBUG Selecting: uvicorn==0.53.0 [compatible] (uvicorn-0.53.0-py3-none-any.whl) -DEBUG Adding transitive dependency for uvicorn==0.53.0: uvicorn==0.53.0 -DEBUG Adding transitive dependency for uvicorn==0.53.0: uvicorn[standard]==0.53.0 -DEBUG Searching for a compatible version of uvicorn (==0.53.0) -DEBUG Selecting: uvicorn==0.53.0 [compatible] (uvicorn-0.53.0-py3-none-any.whl) -DEBUG Adding transitive dependency for uvicorn==0.53.0: click>=7.0 -DEBUG Adding transitive dependency for uvicorn==0.53.0: h11>=0.8 -DEBUG Adding transitive dependency for uvicorn==0.53.0: typing-extensions{python_full_version < '3.11'}>=4.0 -DEBUG Searching for a compatible version of uvicorn[standard] (==0.53.0) -DEBUG Selecting: uvicorn==0.53.0 [compatible] (uvicorn-0.53.0-py3-none-any.whl) -DEBUG Adding transitive dependency for uvicorn==0.53.0: httptools>=0.8.0 -DEBUG Adding transitive dependency for uvicorn==0.53.0: python-dotenv>=0.13 -DEBUG Adding transitive dependency for uvicorn==0.53.0: pyyaml>=5.1 -DEBUG Adding transitive dependency for uvicorn==0.53.0: uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'}>=0.15.1 -DEBUG Adding transitive dependency for uvicorn==0.53.0: watchfiles>=0.20 -DEBUG Adding transitive dependency for uvicorn==0.53.0: websockets>=13.0 -DEBUG Searching for a compatible version of httpx (>=0.27) -DEBUG Selecting: httpx==0.28.1 [compatible] (httpx-0.28.1-py3-none-any.whl) -DEBUG Adding transitive dependency for httpx==0.28.1: anyio* -DEBUG Adding transitive dependency for httpx==0.28.1: certifi* -DEBUG Adding transitive dependency for httpx==0.28.1: httpcore>=1.dev0, <2.dev0 -DEBUG Adding transitive dependency for httpx==0.28.1: idna* -DEBUG Searching for a compatible version of pyright (>=1.1.390) -DEBUG Selecting: pyright==1.1.414 [compatible] (pyright-1.1.414-py3-none-any.whl) -DEBUG Adding transitive dependency for pyright==1.1.414: nodeenv>=1.6.0 -DEBUG Adding transitive dependency for pyright==1.1.414: typing-extensions>=4.1 -DEBUG Searching for a compatible version of ruff (>=0.8.0) -DEBUG Selecting: ruff==0.16.8 [compatible] (ruff-0.16.8-py3-none-linux_armv6l.whl) -DEBUG Found fresh response for: https://pypi.org/simple/annotated-doc/ -DEBUG Found fresh response for: https://pypi.org/simple/starlette/ -DEBUG Found fresh response for: https://pypi.org/simple/typing-inspection/ -DEBUG Found fresh response for: https://pypi.org/simple/pydantic/ -DEBUG Found fresh response for: https://pypi.org/simple/typing-extensions/ -DEBUG Searching for a compatible version of annotated-doc (>=0.0.2) -DEBUG Selecting: annotated-doc==0.0.5 [compatible] (annotated_doc-0.0.5-py3-none-any.whl) -DEBUG Found fresh response for: https://pypi.org/simple/h11/ -DEBUG Found fresh response for: https://pypi.org/simple/click/ -DEBUG Found fresh response for: https://pypi.org/simple/python-dotenv/ -DEBUG Found fresh response for: https://pypi.org/simple/httptools/ -DEBUG Found fresh response for: https://pypi.org/simple/pyyaml/ -DEBUG Found fresh response for: https://pypi.org/simple/uvloop/ -DEBUG Found fresh response for: https://pypi.org/simple/watchfiles/ -DEBUG Found fresh response for: https://pypi.org/simple/anyio/ -DEBUG Found fresh response for: https://pypi.org/simple/certifi/ -DEBUG Found fresh response for: https://pypi.org/simple/websockets/ -DEBUG Found fresh response for: https://pypi.org/simple/idna/ -DEBUG Found fresh response for: https://pypi.org/simple/httpcore/ -DEBUG Found fresh response for: https://pypi.org/simple/nodeenv/ -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl.metadata -DEBUG Searching for a compatible version of pydantic (>=2.9.0) -DEBUG Selecting: pydantic==2.13.5 [compatible] (pydantic-2.13.5-py3-none-any.whl) -DEBUG Adding transitive dependency for pydantic==2.13.5: annotated-types>=0.6.0 -DEBUG Adding transitive dependency for pydantic==2.13.5: pydantic-core>=2.46.5, <2.46.5+ -DEBUG Adding transitive dependency for pydantic==2.13.5: typing-extensions>=4.14.1 -DEBUG Adding transitive dependency for pydantic==2.13.5: typing-inspection>=0.4.2 -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl.metadata -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl.metadata -DEBUG Found fresh response for: https://pypi.org/simple/annotated-types/ -DEBUG No cache entry for: https://pypi.org/simple/pydantic-core/ -DEBUG Sending fresh GET request for: https://pypi.org/simple/pydantic-core/ -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl.metadata -DEBUG Searching for a compatible version of pydantic-core (>=2.46.5, <2.46.5+) -DEBUG Selecting: pydantic-core==2.46.5 [compatible] (pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl) -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl.metadata -DEBUG Adding transitive dependency for pydantic-core==2.46.5: typing-extensions>=4.14.1 -DEBUG Searching for a compatible version of starlette (>=0.46.0) -DEBUG Selecting: starlette==1.6.0 [compatible] (starlette-1.6.0-py3-none-any.whl) -DEBUG Adding transitive dependency for starlette==1.6.0: anyio>=3.6.2, <5 -DEBUG Adding transitive dependency for starlette==1.6.0: typing-extensions{python_full_version < '3.13'}>=4.10.0 -DEBUG Searching for a compatible version of typing-extensions (>=4.14.1) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.11'} (>=4.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions==4.16.0 -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions{python_full_version < '3.11'}==4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.11'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.13'} (>=4.10.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions==4.16.0 -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions{python_full_version < '3.13'}==4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.13'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-inspection (>=0.4.2) -DEBUG Selecting: typing-inspection==0.4.4 [compatible] (typing_inspection-0.4.4-py3-none-any.whl) -DEBUG Adding transitive dependency for typing-inspection==0.4.4: typing-extensions>=4.15.0 -DEBUG Searching for a compatible version of click (>=7.0) -DEBUG Selecting: click==8.5.0 [compatible] (click-8.5.0-py3-none-any.whl) -DEBUG Searching for a compatible version of h11 (>=0.8) -DEBUG Selecting: h11==0.16.0 [compatible] (h11-0.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of httptools (>=0.8.0) -DEBUG Selecting: httptools==0.8.0 [compatible] (httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of python-dotenv (>=0.13) -DEBUG Selecting: python-dotenv==1.2.3 [compatible] (python_dotenv-1.2.3-py3-none-any.whl) -DEBUG Searching for a compatible version of pyyaml (>=5.1) -DEBUG Selecting: pyyaml==6.0.3 [compatible] (pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl) -DEBUG Searching for a compatible version of uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'} (>=0.15.1) -DEBUG Selecting: uvloop==0.22.1 [compatible] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Adding transitive dependency for uvloop==0.22.1: uvloop==0.22.1 -DEBUG Adding transitive dependency for uvloop==0.22.1: uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'}==0.22.1 -DEBUG Searching for a compatible version of uvloop (==0.22.1) -DEBUG Selecting: uvloop==0.22.1 [compatible] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'} (==0.22.1) -DEBUG Selecting: uvloop==0.22.1 [compatible] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of watchfiles (>=0.20) -DEBUG Selecting: watchfiles==1.2.0 [compatible] (watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl) -DEBUG Adding transitive dependency for watchfiles==1.2.0: anyio>=3.0.0 -DEBUG Searching for a compatible version of websockets (>=13.0) -DEBUG Forking Python requirement `>=3.10` on `>=3.11` for websockets==17.1 (split `python_full_version == '3.10.*'`, split `python_full_version >= '3.11'`) -DEBUG Narrowed `requires-python` bound to: >=3.10, <3.11 -DEBUG Narrowed `requires-python` bound to: >=3.11 -DEBUG Solving split (markers: python_full_version >= '3.11') (requires-python: RequiresPython { specifiers: VersionSpecifiers([VersionSpecifier { operator: GreaterThanEqual, version: "3.11" }]), range: RequiresPythonRange(LowerBound(Included("3.11")), UpperBound(Unbounded)) }) -DEBUG Searching for a compatible version of websockets (>=13.0) -DEBUG Selecting: websockets==17.1 [compatible] (websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl) -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/7d/ad/66a74d42fb537bd44056483eae6cbb7ebb10b742c300a0bf8cee427556d4/websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl.metadata -DEBUG Searching for a compatible version of anyio (>=3.6.2, <5) -DEBUG Selecting: anyio==4.15.1 [compatible] (anyio-4.15.1-py3-none-any.whl) -DEBUG Adding transitive dependency for anyio==4.15.1: idna>=2.8 -DEBUG Adding transitive dependency for anyio==4.15.1: typing-extensions{python_full_version < '3.15'}>=4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.15'} (>=4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions==4.16.0 -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions{python_full_version < '3.15'}==4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.15'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [compatible] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of certifi (*) -DEBUG Selecting: certifi==2026.7.22 [compatible] (certifi-2026.7.22-py3-none-any.whl) -DEBUG Searching for a compatible version of httpcore (>=1.dev0, <2.dev0) -DEBUG Selecting: httpcore==1.0.9 [compatible] (httpcore-1.0.9-py3-none-any.whl) -DEBUG Adding transitive dependency for httpcore==1.0.9: certifi* -DEBUG Adding transitive dependency for httpcore==1.0.9: h11>=0.16 -DEBUG Searching for a compatible version of idna (>=2.8) -DEBUG Selecting: idna==3.20 [compatible] (idna-3.20-py3-none-any.whl) -DEBUG Searching for a compatible version of nodeenv (>=1.6.0) -DEBUG Selecting: nodeenv==1.10.0 [compatible] (nodeenv-1.10.0-py2.py3-none-any.whl) -DEBUG Searching for a compatible version of annotated-types (>=0.6.0) -DEBUG Selecting: annotated-types==0.8.0 [compatible] (annotated_types-0.8.0-py3-none-any.whl) -DEBUG Tried 26 versions: annotated-doc 1, annotated-types 1, anyio 1, certifi 1, click 1, fastapi 1, h11 1, httpcore 1, httptools 1, httpx 1, idna 1, nodeenv 1, pydantic 1, pydantic-core 1, pyright 1, python-dotenv 1, pyyaml 1, ruff 1, simplefarm 1, starlette 1, typing-extensions 1, typing-inspection 1, uvicorn 1, uvloop 1, watchfiles 1, websockets 1 -DEBUG split `python_full_version >= '3.11'` resolution took 0.005s -DEBUG Solving split (markers: python_full_version == '3.10.*') (requires-python: RequiresPython { specifiers: VersionSpecifiers([VersionSpecifier { operator: GreaterThanEqual, version: "3.10" }, VersionSpecifier { operator: LessThan, version: "3.11" }]), range: RequiresPythonRange(LowerBound(Included("3.10")), UpperBound(Excluded("3.11"))) }) -DEBUG Searching for a compatible version of websockets (>=13.0) -DEBUG No compatible version found for: Python -DEBUG Recording unit propagation conflict of Python from incompatibility of (websockets) -DEBUG Searching for a compatible version of typing-extensions (>=4.8.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.11'} (>=4.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.11'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'} (>=0.15.1) -DEBUG Selecting: uvloop==0.22.1 [preference] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of uvloop (==0.22.1) -DEBUG Selecting: uvloop==0.22.1 [preference] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of uvloop{platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'} (==0.22.1) -DEBUG Selecting: uvloop==0.22.1 [preference] (uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of httpx (>=0.27) -DEBUG Selecting: httpx==0.28.1 [preference] (httpx-0.28.1-py3-none-any.whl) -DEBUG Searching for a compatible version of pyright (>=1.1.390) -DEBUG Selecting: pyright==1.1.414 [preference] (pyright-1.1.414-py3-none-any.whl) -DEBUG Searching for a compatible version of ruff (>=0.8.0) -DEBUG Selecting: ruff==0.16.8 [preference] (ruff-0.16.8-py3-none-linux_armv6l.whl) -DEBUG Searching for a compatible version of annotated-doc (>=0.0.2) -DEBUG Selecting: annotated-doc==0.0.5 [preference] (annotated_doc-0.0.5-py3-none-any.whl) -DEBUG Searching for a compatible version of pydantic (>=2.9.0) -DEBUG Selecting: pydantic==2.13.5 [preference] (pydantic-2.13.5-py3-none-any.whl) -DEBUG Searching for a compatible version of pydantic-core (>=2.46.5, <2.46.5+) -DEBUG Selecting: pydantic-core==2.46.5 [preference] (pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl) -DEBUG Searching for a compatible version of starlette (>=0.46.0) -DEBUG Selecting: starlette==1.6.0 [preference] (starlette-1.6.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.13'} (>=4.10.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.13'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of typing-inspection (>=0.4.2) -DEBUG Selecting: typing-inspection==0.4.4 [preference] (typing_inspection-0.4.4-py3-none-any.whl) -DEBUG Searching for a compatible version of click (>=7.0) -DEBUG Selecting: click==8.5.0 [preference] (click-8.5.0-py3-none-any.whl) -DEBUG Searching for a compatible version of h11 (>=0.8) -DEBUG Selecting: h11==0.16.0 [preference] (h11-0.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of httptools (>=0.8.0) -DEBUG Selecting: httptools==0.8.0 [preference] (httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Searching for a compatible version of python-dotenv (>=0.13) -DEBUG Selecting: python-dotenv==1.2.3 [preference] (python_dotenv-1.2.3-py3-none-any.whl) -DEBUG Searching for a compatible version of pyyaml (>=5.1) -DEBUG Selecting: pyyaml==6.0.3 [preference] (pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl) -DEBUG Searching for a compatible version of watchfiles (>=0.20) -DEBUG Selecting: watchfiles==1.2.0 [preference] (watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl) -DEBUG Searching for a compatible version of websockets (>=13.0, <=17.0.1) -DEBUG Recording unit propagation conflict of Python from incompatibility of (websockets) -DEBUG Searching for a compatible version of websockets (>=13.0, <=17.0) -DEBUG Recording unit propagation conflict of Python from incompatibility of (websockets) -DEBUG Searching for a compatible version of websockets (>=13.0, <=16.1.1) -DEBUG Selecting: websockets==16.1.1 [compatible] (websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl) -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/08/e7/d1671fb984f9dd844e1da5288070c7c23c9eaba3082d3871aae19c3ab8b9/websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl.metadata -DEBUG Searching for a compatible version of anyio (>=3.6.2, <5) -DEBUG Selecting: anyio==4.15.1 [preference] (anyio-4.15.1-py3-none-any.whl) -DEBUG Adding transitive dependency for anyio==4.15.1: exceptiongroup{python_full_version < '3.11'}>=1.0.2 -DEBUG Adding transitive dependency for anyio==4.15.1: idna>=2.8 -DEBUG Adding transitive dependency for anyio==4.15.1: typing-extensions{python_full_version < '3.15'}>=4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.15'} (>=4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions==4.16.0 -DEBUG Adding transitive dependency for typing-extensions==4.16.0: typing-extensions{python_full_version < '3.15'}==4.16.0 -DEBUG Searching for a compatible version of typing-extensions{python_full_version < '3.15'} (==4.16.0) -DEBUG Selecting: typing-extensions==4.16.0 [preference] (typing_extensions-4.16.0-py3-none-any.whl) -DEBUG Searching for a compatible version of certifi (*) -DEBUG Selecting: certifi==2026.7.22 [preference] (certifi-2026.7.22-py3-none-any.whl) -DEBUG Searching for a compatible version of httpcore (>=1.dev0, <2.dev0) -DEBUG Selecting: httpcore==1.0.9 [preference] (httpcore-1.0.9-py3-none-any.whl) -DEBUG Adding transitive dependency for httpcore==1.0.9: certifi* -DEBUG Adding transitive dependency for httpcore==1.0.9: h11>=0.16 -DEBUG Searching for a compatible version of idna (>=2.8) -DEBUG Selecting: idna==3.20 [preference] (idna-3.20-py3-none-any.whl) -DEBUG Searching for a compatible version of nodeenv (>=1.6.0) -DEBUG Selecting: nodeenv==1.10.0 [preference] (nodeenv-1.10.0-py2.py3-none-any.whl) -DEBUG Searching for a compatible version of annotated-types (>=0.6.0) -DEBUG Selecting: annotated-types==0.8.0 [preference] (annotated_types-0.8.0-py3-none-any.whl) -DEBUG No cache entry for: https://pypi.org/simple/exceptiongroup/ -DEBUG Sending fresh GET request for: https://pypi.org/simple/exceptiongroup/ -DEBUG Searching for a compatible version of exceptiongroup{python_full_version < '3.11'} (>=1.0.2) -DEBUG Selecting: exceptiongroup==1.3.1 [compatible] (exceptiongroup-1.3.1-py3-none-any.whl) -DEBUG Adding transitive dependency for exceptiongroup==1.3.1: exceptiongroup==1.3.1 -DEBUG Adding transitive dependency for exceptiongroup==1.3.1: exceptiongroup{python_full_version < '3.11'}==1.3.1 -DEBUG Searching for a compatible version of exceptiongroup (==1.3.1) -DEBUG Selecting: exceptiongroup==1.3.1 [compatible] (exceptiongroup-1.3.1-py3-none-any.whl) -DEBUG Found fresh response for: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl.metadata -DEBUG Adding transitive dependency for exceptiongroup==1.3.1: typing-extensions{python_full_version < '3.13'}>=4.6.0 -DEBUG Searching for a compatible version of exceptiongroup{python_full_version < '3.11'} (==1.3.1) -DEBUG Selecting: exceptiongroup==1.3.1 [compatible] (exceptiongroup-1.3.1-py3-none-any.whl) -DEBUG Adding transitive dependency for exceptiongroup==1.3.1: typing-extensions{python_full_version < '3.13'}>=4.6.0 -DEBUG Tried 27 versions: annotated-doc 1, annotated-types 1, anyio 1, certifi 1, click 1, exceptiongroup 1, fastapi 1, h11 1, httpcore 1, httptools 1, httpx 1, idna 1, nodeenv 1, pydantic 1, pydantic-core 1, pyright 1, python-dotenv 1, pyyaml 1, ruff 1, simplefarm 1, starlette 1, typing-extensions 1, typing-inspection 1, uvicorn 1, uvloop 1, watchfiles 1, websockets 1 -DEBUG split `python_full_version == '3.10.*'` resolution took 0.141s -INFO Solved your requirements for 2 environments -DEBUG Distinct solution for split (markers: python_full_version >= '3.11') with 26 package(s) -DEBUG Distinct solution for split (markers: python_full_version == '3.10.*') with 27 package(s) -Resolved 28 packages in 48.80s -EXIT=0