From 8f2adbfbc47b4564b3bb34ee1b2196972faa3442 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Mon, 26 Aug 2024 17:01:56 +0300 Subject: [PATCH] style: enable flake8-type-checking and move imports to TYPE_CHECKING --- .flake8 | 2 ++ .pre-commit-config.yaml | 2 ++ .../create_report_apps/create_report.py | 18 ++++++---- .../karkas_blocks/standard/chats/main.py | 12 ++++--- .../standard/config/miniapp_ui.py | 10 +++--- .../karkas_blocks/standard/help/main.py | 5 +-- .../standard/miniapp/dash_telegram_auth.py | 8 +++-- .../karkas_blocks/standard/statistics/main.py | 16 +++++---- .../karkas_blocks/standard/users/main.py | 12 ++++--- .../karkas_blocks/standard/welcome/main.py | 9 ++--- .../welcome/verifications_methods/base.py | 11 +++--- .../welcome/verifications_methods/simple.py | 7 ++-- .../welcome/verifications_methods/utils.py | 9 +++-- src/karkas_core/karkas_core/lib.py | 34 ++++++++----------- .../modules_system/loaders/base.py | 8 +++-- .../modules_system/public_api/public_api.py | 10 +++--- .../karkas_core/modules_system/safe/policy.py | 8 +++-- src/karkas_core/karkas_core/singleton.py | 14 +++++--- 18 files changed, 115 insertions(+), 80 deletions(-) diff --git a/.flake8 b/.flake8 index 4e2b181..fb9ec7c 100644 --- a/.flake8 +++ b/.flake8 @@ -4,3 +4,5 @@ per-file-ignores = max-line-length = 88 count = true extend-ignore = E203,E701 + +extend-select = TC010,TC200 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7167028..6babcf4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,8 @@ repos: rev: 7.1.0 # sync:flake8:poetry.lock hooks: - id: flake8 + additional_dependencies: + - flake8-type-checking - repo: https://github.com/PyCQA/bandit rev: 1.7.9 # sync:bandit:poetry.lock hooks: diff --git a/src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py b/src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py index 9eaf223..22b1855 100644 --- a/src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py +++ b/src/karkas_blocks/karkas_blocks/external/create_report_apps/create_report.py @@ -1,6 +1,7 @@ +from typing import TYPE_CHECKING + from aiogram import Bot, Router from aiogram.enums import ParseMode -from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import ( BufferedInputFile, @@ -14,6 +15,9 @@ from karkas_core.modules_system.public_api import Utils, get_fsm_context from .report import Report +if TYPE_CHECKING: + from aiogram.fsm.context import FSMContext + router = Router() @@ -65,7 +69,7 @@ app_info_message = """Укажите название и версию прило @router.message(ReportState.input_system_info) -async def system_entered(message: Message, state: FSMContext): +async def system_entered(message: Message, state: "FSMContext"): await state.update_data(system=message.text) await message.answer( text=app_info_message, @@ -80,28 +84,28 @@ step_by_step_message = ( @router.message(ReportState.input_app_name) -async def app_name_entered(message: Message, state: FSMContext): +async def app_name_entered(message: Message, state: "FSMContext"): await state.update_data(app=message.text) await message.answer(text=step_by_step_message) await state.set_state(ReportState.input_problem_step_by_step) @router.message(ReportState.input_problem_step_by_step) -async def problem_step_by_step_entered(message: Message, state: FSMContext): +async def problem_step_by_step_entered(message: Message, state: "FSMContext"): await state.update_data(problem_step_by_step=message.text) await message.answer(text="Опиши, что произошло (фактический результат).") await state.set_state(ReportState.input_actual_result) @router.message(ReportState.input_actual_result) -async def actual_result_entered(message: Message, state: FSMContext): +async def actual_result_entered(message: Message, state: "FSMContext"): await state.update_data(actual=message.text) await message.answer(text="Опиши ожидаемый результат.") await state.set_state(ReportState.input_expected_result) @router.message(ReportState.input_expected_result) -async def expected_result_entered(message: Message, state: FSMContext): +async def expected_result_entered(message: Message, state: "FSMContext"): await state.update_data(expected=message.text) await message.answer( text="Если есть дополнительная информация, то напиши ее.", @@ -116,7 +120,7 @@ async def expected_result_entered(message: Message, state: FSMContext): @router.message(ReportState.input_additional_info) -async def additional_info_entered(message: Message, state: FSMContext): +async def additional_info_entered(message: Message, state: "FSMContext"): if message.text == "Дополнительной информации нет": additional_info = "" else: diff --git a/src/karkas_blocks/karkas_blocks/standard/chats/main.py b/src/karkas_blocks/karkas_blocks/standard/chats/main.py index f0b8a5b..1a52807 100644 --- a/src/karkas_blocks/karkas_blocks/standard/chats/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/chats/main.py @@ -1,12 +1,14 @@ -from typing import Any, Awaitable, Callable, Dict +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware -from aiogram.types import Chat, TelegramObject from .db.tables import ChatInfo +if TYPE_CHECKING: + from aiogram.types import Chat, TelegramObject -async def update_chat_info(chat: Chat): + +async def update_chat_info(chat: "Chat"): chat_name = chat.title if chat.type != "private" else "" await ChatInfo.insert( @@ -24,8 +26,8 @@ async def update_chat_info(chat: Chat): class ChatsMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], - event: TelegramObject, + handler: Callable[["TelegramObject", Dict[str, Any]], Awaitable[Any]], + event: "TelegramObject", data: Dict[str, Any], ) -> Any: chat = event.chat diff --git a/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py b/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py index 799be60..420bd8f 100644 --- a/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py +++ b/src/karkas_blocks/karkas_blocks/standard/config/miniapp_ui.py @@ -1,8 +1,6 @@ import asyncio from typing import TYPE_CHECKING -from .config_manager import ConfigManager - try: import dash_bootstrap_components as dbc import flask @@ -17,8 +15,10 @@ from karkas_core.modules_system.public_api import get_module if TYPE_CHECKING: from karkas_blocks.standard.roles import Roles as IRoles + from .config_manager import ConfigManager -def create_control(key: str, config: ConfigManager): + +def create_control(key: str, config: "ConfigManager"): value = config.get(key) meta = config.get_meta(key) @@ -85,7 +85,7 @@ def create_control(key: str, config: ConfigManager): return dbc.Row(row, className="mb-3 mx-1") -def build_settings_tree(config: ConfigManager): +def build_settings_tree(config: "ConfigManager"): tree = {} for key, value in config._metadata.items(): @@ -134,7 +134,7 @@ def create_settings_components(tree, level=0): return components -def get_miniapp_blueprint(config: ConfigManager, prefix: str): +def get_miniapp_blueprint(config: "ConfigManager", prefix: str): Roles: "type[IRoles]" = get_module("standard.roles", "Roles") roles = Roles() diff --git a/src/karkas_blocks/karkas_blocks/standard/help/main.py b/src/karkas_blocks/karkas_blocks/standard/help/main.py index 4bf124f..5299298 100644 --- a/src/karkas_blocks/karkas_blocks/standard/help/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/help/main.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING from aiogram import Router from aiogram.filters import Command -from aiogram.types import Message from karkas_core.modules_system.public_api import ( get_metainfo, @@ -12,6 +11,8 @@ from karkas_core.modules_system.public_api import ( ) if TYPE_CHECKING: + from aiogram.types import Message + from karkas_blocks.standard.config import IConfig config: "IConfig" = get_module("standard.config", "config") @@ -47,7 +48,7 @@ def format_commands(commands_dict): return "\n".join(formatted_commands) -async def help(message: Message): +async def help(message: "Message"): commands = "" version = "" diff --git a/src/karkas_blocks/karkas_blocks/standard/miniapp/dash_telegram_auth.py b/src/karkas_blocks/karkas_blocks/standard/miniapp/dash_telegram_auth.py index 2d5a928..3e84cb7 100644 --- a/src/karkas_blocks/karkas_blocks/standard/miniapp/dash_telegram_auth.py +++ b/src/karkas_blocks/karkas_blocks/standard/miniapp/dash_telegram_auth.py @@ -1,9 +1,13 @@ +from typing import TYPE_CHECKING + import flask from aiogram.utils.web_app import safe_parse_webapp_init_data -from dash import Dash from dash_extensions.enrich import Input, Output from flask import request +if TYPE_CHECKING: + from dash import Dash + # TODO: добавить прокидывание BASE_PATH, т.к. это параметр из настроек WEBAPP_LOADER_TEMPLATE = """ @@ -112,7 +116,7 @@ def get_auth_server(bot_token: str): return server -def setup_auth_clientcallbacks(app: Dash): +def setup_auth_clientcallbacks(app: "Dash"): app.clientside_callback( """ function(n_intervals) { diff --git a/src/karkas_blocks/karkas_blocks/standard/statistics/main.py b/src/karkas_blocks/karkas_blocks/standard/statistics/main.py index 7e85deb..e0cd72c 100644 --- a/src/karkas_blocks/karkas_blocks/standard/statistics/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/statistics/main.py @@ -1,12 +1,14 @@ -from typing import Any, Awaitable, Callable, Dict +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware -from aiogram.types import Message, TelegramObject from .db.tables import ChatStats, Messages, UserStats +if TYPE_CHECKING: + from aiogram.types import Message, TelegramObject -async def update_chat_stats(event: Message): + +async def update_chat_stats(event: "Message"): await ChatStats.insert( ChatStats(chat_id=event.chat.id, date=event.date, messages_count=1) ).on_conflict( @@ -18,7 +20,7 @@ async def update_chat_stats(event: Message): ).run() -async def update_user_stats(event: Message): +async def update_user_stats(event: "Message"): await UserStats.insert( UserStats( key=f"{event.chat.id}-{event.from_user.id}", @@ -36,7 +38,7 @@ async def update_user_stats(event: Message): ).run() -async def save_messages(event: Message): +async def save_messages(event: "Message"): await Messages.insert( Messages( key=f"{event.chat.id}-{event.message_id}", @@ -54,8 +56,8 @@ async def save_messages(event: Message): class StatisticsMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], - event: TelegramObject, + handler: Callable[["TelegramObject", Dict[str, Any]], Awaitable[Any]], + event: "TelegramObject", data: Dict[str, Any], ) -> Any: diff --git a/src/karkas_blocks/karkas_blocks/standard/users/main.py b/src/karkas_blocks/karkas_blocks/standard/users/main.py index 6c88781..e74b1ba 100644 --- a/src/karkas_blocks/karkas_blocks/standard/users/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/users/main.py @@ -1,12 +1,14 @@ -from typing import Any, Awaitable, Callable, Dict +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict from aiogram import BaseMiddleware -from aiogram.types import TelegramObject, User from .db.tables import UserInfo +if TYPE_CHECKING: + from aiogram.types import TelegramObject, User -async def update_user_info(user: User): + +async def update_user_info(user: "User"): if user.last_name is None: user_name = user.first_name else: @@ -26,8 +28,8 @@ async def update_user_info(user: User): class UsersMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], - event: TelegramObject, + handler: Callable[["TelegramObject", Dict[str, Any]], Awaitable[Any]], + event: "TelegramObject", data: Dict[str, Any], ) -> Any: user = event.from_user diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/main.py b/src/karkas_blocks/karkas_blocks/standard/welcome/main.py index bac7446..2f4acba 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/main.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/main.py @@ -7,7 +7,6 @@ from aiogram import Bot, Router, types from aiogram.enums import ChatMemberStatus, ParseMode from aiogram.exceptions import TelegramBadRequest from aiogram.filters import JOIN_TRANSITION, LEAVE_TRANSITION, ChatMemberUpdatedFilter -from aiogram.types import ChatMemberUpdated, PollAnswer from karkas_core.modules_system.public_api import get_module, log, register_router @@ -22,6 +21,8 @@ from .verifications_methods.simple import ( from .verifications_methods.utils import user_mention if TYPE_CHECKING: + from aiogram.types import ChatMemberUpdated, PollAnswer + from karkas_blocks.standard.config import IConfig from karkas_blocks.standard.filters import ChatIDFilter as IChatIDFilter @@ -78,7 +79,7 @@ verification_tasks = MultiKeyDict() last_success = {} -async def new_member_handler(event: ChatMemberUpdated, bot: Bot): +async def new_member_handler(event: "ChatMemberUpdated", bot: Bot): # НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED) if event.new_chat_member.status == ChatMemberStatus.MEMBER: task = task_manager.build_random_task(event, bot) @@ -87,7 +88,7 @@ async def new_member_handler(event: ChatMemberUpdated, bot: Bot): verification_tasks.add(task, keys) -async def left_member_handler(event: ChatMemberUpdated, bot: Bot): +async def left_member_handler(event: "ChatMemberUpdated", bot: Bot): user_id = event.from_user.id chat_id = event.chat.id @@ -144,7 +145,7 @@ async def success_end(task: BaseTask): last_success[task.from_chat_id] = message.message_id -async def handle_poll_verification(answer: PollAnswer, bot: Bot): +async def handle_poll_verification(answer: "PollAnswer", bot: Bot): key = key_from_poll(answer.poll_id) if not verification_tasks.exists(key): return diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py index afc3853..f9be39a 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/base.py @@ -1,21 +1,24 @@ import asyncio from functools import wraps +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.exceptions import TelegramBadRequest from aiogram.filters.callback_data import CallbackData -from aiogram.types import ChatMemberUpdated from karkas_core.modules_system.public_api import log from .utils import mute_user, unmute_user +if TYPE_CHECKING: + from aiogram import Bot + from aiogram.types import ChatMemberUpdated + class BaseTask: def __init__( self, - event: ChatMemberUpdated, - bot: Bot, + event: "ChatMemberUpdated", + bot: "Bot", timeout_func=None, attempt_number=1, max_attempts=1, diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py index f396822..e82f6b8 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/simple.py @@ -1,6 +1,6 @@ from string import Template +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.enums import ParseMode, PollType from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup @@ -8,6 +8,9 @@ from ..utils import get_plural_form, key_from_poll, key_from_user_chat from .base import BaseTask, VerificationCallback, mute_while_task from .utils import user_mention +if TYPE_CHECKING: + from aiogram import Bot + class SimpleBaseTask(BaseTask): pass @@ -120,7 +123,7 @@ class SimplePollTask(SimpleVariantsBaseTask): def __init__( self, event: ChatMemberUpdated, - bot: Bot, + bot: "Bot", timeout_func=None, attempt_number=1, max_attempts=1, diff --git a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py index f544735..0365f6f 100644 --- a/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py +++ b/src/karkas_blocks/karkas_blocks/standard/welcome/verifications_methods/utils.py @@ -1,9 +1,12 @@ import time +from typing import TYPE_CHECKING -from aiogram import Bot from aiogram.enums import ParseMode from aiogram.types import ChatPermissions, User +if TYPE_CHECKING: + from aiogram import Bot + def user_mention(user: User, mode=ParseMode.HTML): if mode == ParseMode.HTML: @@ -14,7 +17,7 @@ def user_mention(user: User, mode=ParseMode.HTML): raise ValueError(f"Unknown parse mode {mode}") -async def mute_user(chat_id, user_id, until, bot: Bot): +async def mute_user(chat_id, user_id, until, bot: "Bot"): end_time = until + int(time.time()) await bot.restrict_chat_member( chat_id, @@ -40,7 +43,7 @@ async def mute_user(chat_id, user_id, until, bot: Bot): ) -async def unmute_user(chat_id, user_id, bot: Bot): +async def unmute_user(chat_id, user_id, bot: "Bot"): await bot.restrict_chat_member( chat_id, user_id, diff --git a/src/karkas_core/karkas_core/lib.py b/src/karkas_core/karkas_core/lib.py index 7cc7acb..6608524 100644 --- a/src/karkas_core/karkas_core/lib.py +++ b/src/karkas_core/karkas_core/lib.py @@ -1,10 +1,14 @@ import importlib import os import traceback +from typing import TYPE_CHECKING -from aiogram import Bot, Dispatcher from aiogram.types import Update +if TYPE_CHECKING: + from aiogram import Bot, Dispatcher + from fastapi import FastAPI, Request + def get_module_directory(module_name): spec = importlib.util.find_spec(module_name) @@ -16,23 +20,15 @@ def get_module_directory(module_name): return os.path.dirname(module_path) -try: - from fastapi import FastAPI, Request +async def register_bot_webhook(app: "FastAPI", bot: "Bot", dp: "Dispatcher"): + async def handle_webhook(request: "Request"): + try: + update = Update.model_validate(await request.json(), context={"bot": bot}) + await dp.feed_update(bot, update) + except Exception: + traceback.print_exc() + return {"ok": False} - async def register_bot_webhook(app: FastAPI, bot: Bot, dp: Dispatcher): - async def handle_webhook(request: Request): - try: - update = Update.model_validate( - await request.json(), context={"bot": bot} - ) - await dp.feed_update(bot, update) - except Exception: - traceback.print_exc() - return {"ok": False} + return {"ok": True} - return {"ok": True} - - app.post("/webhook")(handle_webhook) - -except ImportError: - pass + app.post("/webhook")(handle_webhook) diff --git a/src/karkas_core/karkas_core/modules_system/loaders/base.py b/src/karkas_core/karkas_core/modules_system/loaders/base.py index ebcb89e..9696931 100644 --- a/src/karkas_core/karkas_core/modules_system/loaders/base.py +++ b/src/karkas_core/karkas_core/modules_system/loaders/base.py @@ -1,9 +1,11 @@ -import types from dataclasses import dataclass -from typing import Dict, List, Optional, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Union from dataclasses_json import dataclass_json +if TYPE_CHECKING: + import types + @dataclass_json @dataclass @@ -39,5 +41,5 @@ class AbstractLoader: def info(self) -> ModuleInfo: raise NotImplementedError - def load(self) -> types.ModuleType: + def load(self) -> "types.ModuleType": raise NotImplementedError diff --git a/src/karkas_core/karkas_core/modules_system/public_api/public_api.py b/src/karkas_core/karkas_core/modules_system/public_api/public_api.py index 53e6185..6b0869a 100644 --- a/src/karkas_core/karkas_core/modules_system/public_api/public_api.py +++ b/src/karkas_core/karkas_core/modules_system/public_api/public_api.py @@ -1,8 +1,7 @@ import inspect import types -from typing import Any, Tuple, Union +from typing import TYPE_CHECKING, Any, Tuple, Union -from aiogram import BaseMiddleware, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.storage.base import StorageKey @@ -10,18 +9,21 @@ from aiogram.fsm.storage.base import StorageKey from karkas_core.modules_system.loaders.base import DependencyInfo from karkas_core.singleton import Singleton +if TYPE_CHECKING: + from aiogram import BaseMiddleware, Router + async def set_chat_menu_button(menu_button): app = Singleton() await app.bot.set_chat_menu_button(menu_button=menu_button) -def register_router(router: Router): +def register_router(router: "Router"): app = Singleton() app.storage["_routers"].append(router) -def register_outer_message_middleware(middleware: BaseMiddleware): +def register_outer_message_middleware(middleware: "BaseMiddleware"): app = Singleton() app.storage["_outer_message_middlewares"].append(middleware) diff --git a/src/karkas_core/karkas_core/modules_system/safe/policy.py b/src/karkas_core/karkas_core/modules_system/safe/policy.py index 8dfc14b..58c11ac 100644 --- a/src/karkas_core/karkas_core/modules_system/safe/policy.py +++ b/src/karkas_core/karkas_core/modules_system/safe/policy.py @@ -1,6 +1,5 @@ import types -from _ast import AnnAssign -from typing import Any +from typing import TYPE_CHECKING, Any from aiogram import Bot from RestrictedPython import ( @@ -20,6 +19,9 @@ from RestrictedPython.Guards import ( # guarded_setattr,; full_write_guard, from karkas_core.logger import log from karkas_core.modules_system.safe.zope_guards import extra_safe_builtins +if TYPE_CHECKING: + from _ast import AnnAssign + class RestrictedPythonPolicy(RestrictingNodeTransformer): def visit_AsyncFunctionDef(self, node): @@ -50,7 +52,7 @@ class RestrictedPythonPolicy(RestrictingNodeTransformer): return self.node_contents_visit(node) """ - def visit_AnnAssign(self, node: AnnAssign) -> Any: + def visit_AnnAssign(self, node: "AnnAssign") -> Any: # missing in RestrictingNodeTransformer # this doesn't need the logic that is in visit_Assign # because it doesn't have a "targets" attribute, diff --git a/src/karkas_core/karkas_core/singleton.py b/src/karkas_core/karkas_core/singleton.py index 3a0f1f4..8f86585 100644 --- a/src/karkas_core/karkas_core/singleton.py +++ b/src/karkas_core/karkas_core/singleton.py @@ -1,7 +1,11 @@ -from aiogram import Bot, Dispatcher +from typing import TYPE_CHECKING + from aiogram.fsm.storage.memory import MemoryStorage -from karkas_core.modules_system import ModulesManager +if TYPE_CHECKING: + from aiogram import Bot, Dispatcher + + from karkas_core.modules_system import ModulesManager class SingletonMeta(type): @@ -15,9 +19,9 @@ class SingletonMeta(type): class Singleton(metaclass=SingletonMeta): - bot: Bot - dp: Dispatcher = None - modules_manager: ModulesManager = None + bot: "Bot" + dp: "Dispatcher" = None + modules_manager: "ModulesManager" = None storage = { "_fsm_storage": MemoryStorage(), "_routers": [],