Exemplo n.º 1
0
    async def shutdown(self):
        # run shutdown
        cors = [shutdown() for shutdown in self.shutdown_funcs]
        if cors:
            try:
                await asyncio.gather(*cors)
            except Exception as e:
                logger.opt(colors=True, exception=e).error(
                    "<r><bg #f8bbd0>Error when running shutdown function. "
                    "Ignored!</bg #f8bbd0></r>")

        for task in self.connections:
            if not task.done():
                task.cancel()
        await asyncio.sleep(0.1)

        tasks = [
            t for t in asyncio.all_tasks() if t is not asyncio.current_task()
        ]
        if tasks and not self.force_exit:
            logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
        while tasks and not self.force_exit:
            await asyncio.sleep(0.1)
            tasks = [
                t for t in asyncio.all_tasks()
                if t is not asyncio.current_task()
            ]

        for task in tasks:
            task.cancel()

        await asyncio.gather(*tasks, return_exceptions=True)

        loop = asyncio.get_event_loop()
        loop.stop()
Exemplo n.º 2
0
async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event,
                       state: dict) -> Union[None, NoReturn]:
    if Matcher.expire_time and datetime.now() > Matcher.expire_time:
        raise _ExceptionContainer([ExpiredException])

    try:
        if not await Matcher.check_perm(
                bot, event) or not await Matcher.check_rule(bot, event, state):
            return
    except Exception as e:
        logger.opt(colors=True, exception=e).error(
            f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>")
        return

    logger.info(f"Event will be handled by {Matcher}")

    matcher = Matcher()
    # TODO: BeforeMatcherRun
    try:
        logger.debug(f"Running matcher {matcher}")
        await matcher.run(bot, event, state)
    except Exception as e:
        logger.opt(colors=True, exception=e).error(
            f"<r><bg #f8bbd0>Running matcher {matcher} failed.</bg #f8bbd0></r>"
        )

    exceptions = []
    if Matcher.temp:
        exceptions.append(ExpiredException)
    if Matcher.block:
        exceptions.append(StopPropagation)
    if exceptions:
        raise _ExceptionContainer(exceptions)
Exemplo n.º 3
0
    async def handle_message(self, message: bytes):
        data: dict = json.loads(message)

        if not data:
            return

        # 判断消息类型,生成不同的 Event
        try:
            conversation_type = data["conversationType"]
            if conversation_type == ConversationType.private:
                event = PrivateMessageEvent.parse_obj(data)
            elif conversation_type == ConversationType.group:
                event = GroupMessageEvent.parse_obj(data)
            else:
                raise ValueError("Unsupported conversation type")
        except Exception as e:
            log("ERROR", "Event Parser Error", e)
            return

        try:
            await handle_event(self, event)
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f"<r><bg #f8bbd0>Failed to handle event. Raw: {escape_tag(str(data))}</bg #f8bbd0></r>"
            )
        return
Exemplo n.º 4
0
def init(*, _env_file: Optional[str] = None, **kwargs: Any) -> None:
    """初始化 NoneBot 以及 全局 {ref}`nonebot.drivers.Driver` 对象。

    NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。

    也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。

    参数:
        _env_file: 配置文件名,默认从 `.env.{env_name}` 中读取配置
        kwargs: 任意变量,将会存储到 {ref}`nonebot.drivers.Driver.config` 对象里

    用法:
        ```python
        nonebot.init(database=Database(...))
        ```
    """
    global _driver
    if not _driver:
        logger.success("NoneBot is initializing...")
        env = Env()
        config = Config(
            **kwargs,
            _common_config=env.dict(),
            _env_file=_env_file or f".env.{env.environment}",
        )

        default_filter.level = config.log_level
        logger.opt(colors=True).info(
            f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>")
        logger.opt(colors=True).debug(
            f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}")

        DriverClass: Type[Driver] = _resolve_combine_expr(config.driver)
        _driver = DriverClass(env, config)
Exemplo n.º 5
0
def load_plugin(module_path: str) -> Optional[Plugin]:
    """
    :说明:
      使用 ``importlib`` 加载单个插件,可以是本地插件或是通过 ``pip`` 安装的插件。
    :参数:
      * ``module_path: str``: 插件名称 ``path.to.your.plugin``
    :返回:
      - ``Optional[Plugin]``
    """
    try:
        _tmp_matchers.clear()
        if module_path in plugins:
            return plugins[module_path]
        elif module_path in sys.modules:
            logger.warning(
                f"Module {module_path} has been loaded by other plugins! Ignored"
            )
            return
        module = importlib.import_module(module_path)
        for m in _tmp_matchers:
            m.module = module_path
        plugin = Plugin(module_path, module, _tmp_matchers.copy())
        plugins[module_path] = plugin
        logger.opt(
            colors=True).info(f'Succeeded to import "<y>{module_path}</y>"')
        return plugin
    except Exception as e:
        logger.opt(colors=True, exception=e).error(
            f'<r><bg #f8bbd0>Failed to import "{module_path}"</bg #f8bbd0></r>')
        return None
Exemplo n.º 6
0
    def load_plugin(self, name: str) -> Optional[Plugin]:
        """加载指定插件。

        对于独立插件,可以使用完整插件模块名或者插件名称。

        参数:
            name: 插件名称。
        """

        try:
            if name in self.plugins:
                module = importlib.import_module(name)
            elif name in self._third_party_plugin_names:
                module = importlib.import_module(
                    self._third_party_plugin_names[name])
            elif name in self._searched_plugin_names:
                module = importlib.import_module(
                    self._path_to_module_name(
                        self._searched_plugin_names[name]))
            else:
                raise RuntimeError(
                    f"Plugin not found: {name}! Check your plugin name")

            logger.opt(colors=True).success(
                f'Succeeded to import "<y>{escape_tag(name)}</y>"')
            plugin = getattr(module, "__plugin__", None)
            if plugin is None:
                raise RuntimeError(
                    f"Module {module.__name__} is not loaded as a plugin! "
                    "Make sure not to import it before loading.")
            return plugin
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f'<r><bg #f8bbd0>Failed to import "{escape_tag(name)}"</bg #f8bbd0></r>'
            )
Exemplo n.º 7
0
def load_plugins(*plugin_dir: str) -> Set[Plugin]:
    loaded_plugins = set()
    for module_info in pkgutil.iter_modules(plugin_dir):
        _tmp_matchers.clear()
        name = module_info.name
        if name.startswith("_"):
            continue

        spec = module_info.module_finder.find_spec(name)
        if spec.name in plugins:
            continue
        elif spec.name in sys.modules:
            logger.warning(
                f"Module {spec.name} has been loaded by other plugin! Ignored")
            continue

        try:
            module = _load(spec)

            for m in _tmp_matchers:
                m.module = name
            plugin = Plugin(name, module, _tmp_matchers.copy())
            plugins[name] = plugin
            loaded_plugins.add(plugin)
            logger.opt(colors=True).info(f'Succeeded to import "<y>{name}</y>"')
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f'<r><bg #f8bbd0>Failed to import "{name}"</bg #f8bbd0></r>')
    return loaded_plugins
Exemplo n.º 8
0
    async def handle_message(self, message: bytes):
        """
        :说明:

          处理事件并转换为 `Event <#class-event>`_
        """
        data: dict = json.loads(message)
        if data.get("type") == "url_verification":
            return

        try:
            header = data["header"]
            event_type = header["event_type"]
            if data.get("event"):
                if data["event"].get("message"):
                    event_type += f".{data['event']['message']['chat_type']}"

            models = get_event_model(event_type)
            for model in models:
                try:
                    event = model.parse_obj(data)
                    break
                except Exception as e:
                    log("DEBUG", "Event Parser Error", e)
            else:
                event = Event.parse_obj(data)

            _check_at_me(self, event)
            _check_nickname(self, event)

            await handle_event(self, event)
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f"<r><bg #f8bbd0>Failed to handle event. Raw: {escape_tag(str(data))}</bg #f8bbd0></r>"
            )
Exemplo n.º 9
0
async def _check_matcher(priority: int, Matcher: Type[Matcher], bot: "Bot",
                         event: "Event", state: T_State) -> None:
    if Matcher.expire_time and datetime.now() > Matcher.expire_time:
        try:
            matchers[priority].remove(Matcher)
        except Exception:
            pass
        return

    try:
        if not await Matcher.check_perm(
                bot, event) or not await Matcher.check_rule(bot, event, state):
            return
    except Exception as e:
        logger.opt(colors=True, exception=e).error(
            f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
        )
        return

    if Matcher.temp:
        try:
            matchers[priority].remove(Matcher)
        except Exception:
            pass

    await _run_matcher(Matcher, bot, event, state)
Exemplo n.º 10
0
def _load_plugin(manager: PluginManager, plugin_name: str) -> Optional[Plugin]:
    if plugin_name.startswith("_"):
        return None

    _tmp_matchers.set(set())
    _export.set(Export())

    if plugin_name in plugins:
        return None

    try:
        module = manager.load_plugin(plugin_name)

        for m in _tmp_matchers.get():
            m.module = plugin_name
        plugin = Plugin(plugin_name, module, _tmp_matchers.get(),
                        _export.get())
        plugins[plugin_name] = plugin
        logger.opt(
            colors=True).info(f'Succeeded to import "<y>{plugin_name}</y>"')
        return plugin
    except Exception as e:
        logger.opt(colors=True, exception=e).error(
            f'<r><bg #f8bbd0>Failed to import "{plugin_name}"</bg #f8bbd0></r>'
        )
        return None
Exemplo n.º 11
0
 def run(self, *args, **kwargs):
     """
     启动驱动框架
     """
     logger.opt(colors=True).debug(
         f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
     )
Exemplo n.º 12
0
    async def handle_message(self, message: dict):
        """
        :说明:

          调用 `_check_reply <#async-check-reply-bot-event>`_, `_check_at_me <#check-at-me-bot-event>`_, `_check_nickname <#check-nickname-bot-event>`_ 处理事件并转换为 `Event <#class-event>`_
        """
        if not message:
            return

        if "post_type" not in message:
            ResultStore.add_result(message)
            return

        try:
            event = Event(message)

            # Check whether user is calling me
            await _check_reply(self, event)
            _check_at_me(self, event)
            _check_nickname(self, event)

            await handle_event(self, event)
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>"
            )
Exemplo n.º 13
0
 def _load_plugin(module_path: str) -> Optional[Plugin]:
     try:
         _tmp_matchers.set(set())
         _export.set(Export())
         if module_path in plugins:
             return plugins[module_path]
         elif module_path in sys.modules:
             logger.warning(
                 f"Module {module_path} has been loaded by other plugins! Ignored"
             )
             return None
         module = importlib.import_module(module_path)
         for m in _tmp_matchers.get():
             m.module = module_path
         plugin = Plugin(module_path, module, _tmp_matchers.get(),
                         _export.get())
         plugins[module_path] = plugin
         logger.opt(colors=True).info(
             f'Succeeded to import "<y>{module_path}</y>"')
         return plugin
     except Exception as e:
         logger.opt(colors=True, exception=e).error(
             f'<r><bg #f8bbd0>Failed to import "{module_path}"</bg #f8bbd0></r>'
         )
         return None
Exemplo n.º 14
0
    def _load_plugin(module_info) -> Optional[Plugin]:
        _tmp_matchers.set(set())
        _export.set(Export())
        name = module_info.name
        if name.startswith("_"):
            return None

        spec = module_info.module_finder.find_spec(name, None)
        if not spec:
            logger.warning(
                f"Module {name} cannot be loaded! Check module name first.")
        elif spec.name in plugins:
            return None
        elif spec.name in sys.modules:
            logger.warning(
                f"Module {spec.name} has been loaded by other plugin! Ignored")
            return None

        try:
            module = _load(spec)

            for m in _tmp_matchers.get():
                m.module = name
            plugin = Plugin(name, module, _tmp_matchers.get(), _export.get())
            plugins[name] = plugin
            logger.opt(
                colors=True).info(f'Succeeded to import "<y>{name}</y>"')
            return plugin
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f'<r><bg #f8bbd0>Failed to import "{name}"</bg #f8bbd0></r>')
            return None
Exemplo n.º 15
0
 async def _run_hook(bot: "Bot") -> None:
     coros = list(map(lambda x: x(bot), self._ws_disconnection_hook))
     if coros:
         try:
             await asyncio.gather(*coros)
         except Exception as e:
             logger.opt(colors=True, exception=e).error(
                 "<r><bg #f8bbd0>Error when running WebSocketDisConnection hook. "
                 "Running cancelled!</bg #f8bbd0></r>")
Exemplo n.º 16
0
def check_available_themes() -> None:
    for theme in config.AVAILABLE_USER_CONFIG[1:]:
        try:
            import_theme(theme)
        except:
            logger.error(f'Arcaea theme fail to import {theme}')
        else:
            logger.opt(colors=True).info(
                f'Arcaea theme <b><u>{theme}</u></b> successfully import')
Exemplo n.º 17
0
    async def call_api(self, api: str, **data: Any) -> Any:
        """
        :说明:

          调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用

        :参数:

          * ``api: str``: API 名称
          * ``self_id: Optional[str]``: 指定调用 API 的机器人
          * ``**data``: API 数据

        :示例:

        .. code-block:: python

            await bot.call_api("send_msg", message="hello world")
            await bot.send_msg(message="hello world")
        """
        coros = list(map(lambda x: x(self, api, data), self._calling_api_hook))
        if coros:
            try:
                logger.debug("Running CallingAPI hooks...")
                await asyncio.gather(*coros)
            except Exception as e:
                logger.opt(colors=True, exception=e).error(
                    "<r><bg #f8bbd0>Error when running CallingAPI hook. "
                    "Running cancelled!</bg #f8bbd0></r>")

        exception = None
        result = None

        try:
            if "self_id" in data and data["self_id"]:
                bot = self.driver.bots[str(data["self_id"])]
                result = await bot._call_api(api, **data)
            else:
                result = await self._call_api(api, **data)
        except Exception as e:
            exception = e

        coros = list(
            map(lambda x: x(self, exception, api, data, result),
                self._called_api_hook))
        if coros:
            try:
                logger.debug("Running CalledAPI hooks...")
                await asyncio.gather(*coros)
            except Exception as e:
                logger.opt(colors=True, exception=e).error(
                    "<r><bg #f8bbd0>Error when running CalledAPI hook. "
                    "Running cancelled!</bg #f8bbd0></r>")

        if exception:
            raise exception
        return result
Exemplo n.º 18
0
 async def _checker():
     while cls.active:
         try:
             await _connection_ensure()
         except Exception as e:
             logger.opt(colors=True).warning(
                 'Failed to create mirai connection to '
                 f'<y>{qq}</y>, reason: <r>{e}</r>. '
                 'Will retry after 3 seconds')
         await asyncio.sleep(3)
Exemplo n.º 19
0
    async def _handle_ws_reverse(
        self,
        adapter: str,
        websocket: FastAPIWebSocket,
        x_self_id: str = Header(None),
        auth: Optional[str] = Depends(get_auth_bearer)):
        ws = WebSocket(websocket)

        access_token = self.config.access_token
        if access_token and access_token != auth:
            logger.warning("Authorization Header is invalid"
                           if auth else "Missing Authorization Header")
            await ws.close(code=status.WS_1008_POLICY_VIOLATION)
            return

        if not x_self_id:
            logger.warning(f"Missing X-Self-ID Header")
            await ws.close(code=status.WS_1008_POLICY_VIOLATION)
            return

        if x_self_id in self._clients:
            logger.warning(f"Connection Conflict: self_id {x_self_id}")
            await ws.close(code=status.WS_1008_POLICY_VIOLATION)
            return

        # Create Bot Object
        if adapter in self._adapters:
            BotClass = self._adapters[adapter]
            bot = BotClass(self,
                           "websocket",
                           self.config,
                           x_self_id,
                           websocket=ws)
        else:
            logger.warning("Unknown adapter")
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                                detail="adapter not found")

        await ws.accept()
        self._clients[x_self_id] = bot
        logger.opt(colors=True).info(
            f"WebSocket Connection from <y>{adapter.upper()} "
            f"Bot {x_self_id}</y> Accepted!")

        try:
            while not ws.closed:
                data = await ws.receive()

                if not data:
                    continue

                asyncio.create_task(bot.handle_message(data))
        finally:
            del self._clients[x_self_id]
Exemplo n.º 20
0
 async def wrapper(*args, **kwargs):
     try:
         data = await function(*args, **kwargs)
     except httpx.HTTPError:
         raise exception.NetworkError('mirai')
     logger.opt(colors=True).debug('<b>Mirai API returned data:</b> '
                                   f'<y>{escape_tag(str(data))}</y>')
     if isinstance(data, dict):
         if data.get('code', 0) != 0:
             raise ActionFailed(**data)
     return data
Exemplo n.º 21
0
 def register_adapter(cls, name: str, adapter: Type[Bot]):
     """
     :说明:
       注册一个协议适配器
     :参数:
       * ``name: str``: 适配器名称,用于在连接时进行识别
       * ``adapter: Type[Bot]``: 适配器 Class
     """
     cls._adapters[name] = adapter
     logger.opt(
         colors=True).debug(f'Succeeded to load adapter "<y>{name}</y>"')
Exemplo n.º 22
0
 async def _build_request(
         setup: HTTPPollingSetup) -> Optional[HTTPRequest]:
     url = httpx.URL(setup.url)
     if not url.netloc:
         logger.opt(colors=True).error(
             f"<r><bg #f8bbd0>Error parsing url {escape_tag(str(url))}</bg #f8bbd0></r>"
         )
         return
     return HTTPRequest(
         setup.http_version, url.scheme, url.path, url.query, {
             **setup.headers, "host": url.netloc.decode("ascii")
         }, setup.method, setup.body)
Exemplo n.º 23
0
    async def startup(self):
        # run startup
        cors = [startup() for startup in self.startup_funcs]
        if cors:
            try:
                await asyncio.gather(*cors)
            except Exception as e:
                logger.opt(colors=True, exception=e).error(
                    "<r><bg #f8bbd0>Error when running startup function. "
                    "Ignored!</bg #f8bbd0></r>")

        logger.info("Application startup completed.")
Exemplo n.º 24
0
def init(*, _env_file: Optional[str] = None, **kwargs):
    """
    :说明:

      初始化 NoneBot 以及 全局 Driver 对象。

      NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。

      你也可以传入自定义的 _env_file 来指定 NoneBot 从该文件读取配置。

    :参数:

      * ``_env_file: Optional[str]``: 配置文件名,默认从 .env.{env_name} 中读取配置
      * ``**kwargs``: 任意变量,将会存储到 Config 对象里

    :返回:

      - ``None``

    :用法:

    .. code-block:: python

        nonebot.init(database=Database(...))

    """
    global _driver
    if not _driver:
        logger.info("NoneBot is initializing...")
        env = Env()
        logger.opt(
            colors=True).info(f"Current <y><b>Env: {env.environment}</b></y>")
        config = Config(**kwargs,
                        _env_file=_env_file or f".env.{env.environment}")

        default_filter.level = "DEBUG" if config.debug else "INFO"
        logger.opt(colors=True).debug(
            f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}")

        DriverClass: Type[Driver] = getattr(
            importlib.import_module(config.driver), "Driver")
        _driver = DriverClass(env, config)

        # register build-in adapters
        _driver.register_adapter("cqhttp", CQBot)

        # load nonebot test frontend if debug
        if config.debug and nonebot_test:
            logger.debug("Loading nonebot test frontend...")
            nonebot_test.init()

    if scheduler:
        _driver.on_startup(_start_scheduler)
Exemplo n.º 25
0
 async def _build_request(
         setup: HTTPPollingSetup) -> Optional[HTTPRequest]:
     url = URL(setup.url)
     if not url.is_absolute() or not url.host:
         logger.opt(colors=True).error(
             f"<r><bg #f8bbd0>Error parsing url {escape_tag(str(url))}</bg #f8bbd0></r>"
         )
         return
     host = f"{url.host}:{url.port}" if url.port else url.host
     return HTTPRequest(setup.http_version, url.scheme, url.path,
                        url.raw_query_string.encode("latin-1"), {
                            **setup.headers, "host": host
                        }, setup.method, setup.body)
Exemplo n.º 26
0
    async def handle_message(self, body: dict):
        message = MessageModel.parse_obj(body)
        if not message:
            return

        try:
            event = Event(message)
            await handle_event(self, event)
        except Exception as e:
            logger.opt(colors=True, exception=e).error(
                f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>"
            )
        return
Exemplo n.º 27
0
 async def _check(Matcher: Type[Matcher], bot: Bot, event: Event,
                  state: dict) -> Optional[Type[Matcher]]:
     try:
         if (not Matcher.expire_time or datetime.now() <=
                 Matcher.expire_time) and await Matcher.check_perm(
                     bot, event) and await Matcher.check_rule(
                         bot, event, state):
             return Matcher
     except Exception as e:
         logger.opt(colors=True, exception=e).error(
             f"<r><bg #f8bbd0>Rule check failed for {Matcher}.</bg #f8bbd0></r>"
         )
     return None
Exemplo n.º 28
0
def init():
    driver = get_driver()
    if isinstance(driver, Driver):
        register_router_fastapi(driver, socket_app)
    else:
        logger.warning(f"Driver {driver.type} not supported")
        return
    host = str(driver.config.host)
    port = driver.config.port
    if host in ["0.0.0.0", "127.0.0.1"]:
        host = "localhost"
    logger.opt(colors=True).info(
        f"Nonebot test frontend will be running at: "
        f"<b><u>http://{host}:{port}{URL_BASE}</u></b>")
Exemplo n.º 29
0
async def _(bot: Bot, event: Event, state: dict) -> None:
    user = str(event.user_id)
    group = str(event.group_id)
    message = str(event.message)
    message_id = str(event.id)

    if group == "None":
        saveMessage(message_id, message, user)
    else:
        saveMessage(message_id, message, user, group)

    logger.opt(colors=True).info(
        f"GROUP[<yellow>{group}</yellow>]: USER(<blue>{user}</blue>) > Message: (<green>{message}</green>) Saved successfully"
    )
Exemplo n.º 30
0
def init(*, _env_file: Optional[str] = None, **kwargs):
    """
    :说明:

      初始化 NoneBot 以及 全局 Driver 对象。

      NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。

      你也可以传入自定义的 _env_file 来指定 NoneBot 从该文件读取配置。

    :参数:

      * ``_env_file: Optional[str]``: 配置文件名,默认从 .env.{env_name} 中读取配置
      * ``**kwargs``: 任意变量,将会存储到 Config 对象里

    :返回:

      - ``None``

    :用法:

    .. code-block:: python

        nonebot.init(database=Database(...))

    """
    global _driver
    if not _driver:
        logger.success("NoneBot is initializing...")
        env = Env()
        config = Config(**kwargs,
                        _common_config=env.dict(),
                        _env_file=_env_file or f".env.{env.environment}")

        default_filter.level = (
            "DEBUG" if config.debug else
            "INFO") if config.log_level is None else config.log_level
        logger.opt(colors=True).info(
            f"Current <y><b>Env: {escape_tag(env.environment)}</b></y>")
        logger.opt(colors=True).debug(
            f"Loaded <y><b>Config</b></y>: {escape_tag(str(config.dict()))}")

        modulename, _, cls = config.driver.partition(":")
        module = importlib.import_module(modulename)
        instance = module
        for attr_str in (cls or "Driver").split("."):
            instance = getattr(instance, attr_str)
        DriverClass: Type[Driver] = instance  # type: ignore
        _driver = DriverClass(env, config)