"""
快捷导入:

- `CommandGroup` -> {ref}`nonebot.command.group.CommandGroup`

此模块定义了一个内部细节可能会频繁变更而不在文档公开的 `Command` 类型，用户如果需要使用应该自行考虑不兼容更新产生的风险。
"""
import re
import asyncio
import warnings
from datetime import datetime, timedelta
from functools import partial
from typing import (NoReturn, Tuple, Union, Iterable, Any, Optional, List, Dict,
                    Awaitable, Pattern, Type)

from aiocqhttp import Event as CQEvent
from aiocqhttp.message import Message

from nonebot import NoneBot
from nonebot.command.argfilter import ValidateError
from nonebot.helpers import context_id, send, render_expression
from nonebot.log import logger
from nonebot.session import BaseSession
from nonebot.typing import (CommandName_T, CommandArgs_T, CommandHandler_T,
                            Message_T, PermissionPolicy_T, State_T, Filter_T, Patterns_T)
from nonebot.permission import check_permission

# key: context id
# value: CommandSession object
_sessions = {}  # type: Dict[str, "CommandSession"]

class CommandInterrupt(Exception):
    """INTERNAL API"""
    pass

class _YieldException(CommandInterrupt):
    """
    Raised by command.run(), session indicating that the waiting session
    will resume and current execution path should return immediately.
    """
    pass

class _PauseException(CommandInterrupt):
    """
    Raised by session.pause() indicating that the command session
    should be paused to ask the user for some arguments.
    """
    pass

class _FinishException(CommandInterrupt):
    """
    Raised by session.finish() indicating that the command session
    should be stopped and removed.
    """

    def __init__(self, result: bool = True):
        """
        :param result: succeeded to call the command
        """
        self.result = result

class SwitchException(CommandInterrupt):
    """
    INTERNAL API

    Raised by session.switch() indicating that the command session
    should be stopped and replaced with a new one (going through
    handle_message() again).

    Since the new message will go through handle_message() again,
    the later function should be notified. So this exception is
    intended to be propagated to handle_message().
    """

    def __init__(self, new_message: Message):
        """
        :param new_message: new message which should be placed in event
        """
        self.new_message = new_message

class Command:
    """
    INTERNAL API

    Note ... (Ellipsis) is a valid value for field expire_timeout and run_timeout.
    However I cannot type it until Python 3.10. see bugs.python.org/issue41810.
    """
    __slots__ = ('name', 'func', 'only_to_me', 'privileged', 'args_parser_func',
                 'permission', 'expire_timeout', 'run_timeout', 'session_class')

    def __init__(self, *, name: CommandName_T, func: CommandHandler_T,
                 only_to_me: bool, privileged: bool,
                 permission: PermissionPolicy_T,
                 expire_timeout: Optional[timedelta],
                 run_timeout: Optional[timedelta],
                 session_class: Optional[Type['CommandSession']]):
        self.name = name
        self.func = func
        self.only_to_me = only_to_me
        self.privileged = privileged
        self.args_parser_func: Optional[CommandHandler_T] = None
        self.permission = permission  # includes EllipsisType
        self.expire_timeout = expire_timeout  # includes EllipsisType
        self.run_timeout = run_timeout  # includes EllipsisType
        self.session_class = session_class

    async def run(self,
                  session: 'CommandSession',
                  *,
                  check_perm: bool = True,
                  dry: bool = False) -> bool:
        """
        Run the command in a given session.

        :param session: CommandSession object
        :param check_perm: should check permission before running
        :param dry: just check any prerequisite, without actually running
        :return: the command is finished (or can be run, given dry == True)
        """
        has_perm = await self._check_perm(session) if check_perm else True
        if self.func and has_perm:
            if dry:
                return True

            if session.current_arg_filters is not None and \
                    session.current_key is not None:
                # argument-level filters are given, use them
                arg = session.current_arg
                config = session.bot.config
                for f in session.current_arg_filters:
                    try:
                        res = f(arg)
                        if isinstance(res, Awaitable):
                            res = await res
                        arg = res
                    except ValidateError as e:
                        # validation failed
                        if config.MAX_VALIDATION_FAILURES > 0:
                            # should check number of validation failures
                            session.state['__validation_failure_num'] = \
                                session.state.get(
                                    '__validation_failure_num', 0) + 1

                            if session.state['__validation_failure_num'] >= \
                                    config.MAX_VALIDATION_FAILURES:
                                # noinspection PyProtectedMember
                                session.finish(
                                    render_expression(
                                        config.
                                        TOO_MANY_VALIDATION_FAILURES_EXPRESSION
                                    ), **session._current_send_kwargs)

                        failure_message = e.message
                        if failure_message is None:
                            failure_message = render_expression(
                                config.DEFAULT_VALIDATION_FAILURE_EXPRESSION)
                        # noinspection PyProtectedMember
                        session.pause(failure_message,
                                      **session._current_send_kwargs)

                # passed all filters
                session.state[session.current_key] = arg
            else:
                # fallback to command-level args_parser_func
                if self.args_parser_func:
                    await self.args_parser_func(session)
                if session.current_key is not None and \
                        session.current_key not in session.state:
                    # args_parser_func didn't set state, here we set it
                    session.state[session.current_key] = session.current_arg
            if session.waiting:
                session._future.set_result(True)
                raise _YieldException()
            else:
                await self.func(session)
            return True
        return False

    async def _check_perm(self, session: 'CommandSession') -> bool:
        """
        Check if the session has sufficient permission to
        call the command.

        :param session: CommandSession object
        :return: the session has the permission
        """
        return await check_permission(session.bot, session.event,
            self.permission if self.permission is not ...
                else session.bot.config.DEFAULT_COMMAND_PERMISSION)

    def args_parser(self, parser_func: CommandHandler_T) -> CommandHandler_T:
        """
        Decorator to register a function as the arguments parser of
        the corresponding command.
        """
        self.args_parser_func = parser_func
        return parser_func

    def __repr__(self):
        return f'<Command, name={self.name.__repr__()}>'

    def __str__(self):
        return self.__repr__()

class CommandManager:
    """全局命令管理器。

    版本: 1.6.0+
    """
    _commands = {}  # type: Dict[CommandName_T, Command]
    _aliases = {}  # type: Dict[str, Command]
    _switches = {}  # type: Dict[Command, bool]
    _patterns = {}  # type: Dict[Pattern[str], Command]

    def __init__(self):
        # TODO: don't copy
        # https://github.com/nonebot/nonebot/projects/2#card-69531150
        self.commands: Dict[CommandName_T, Command] = CommandManager._commands.copy()
        """{anno}`Dict[nonebot.typing.CommandName_T, Command]` 命令字典。"""
        self.aliases: Dict[str, Command] = CommandManager._aliases.copy()
        """命令别名字典。"""
        self.switches: Dict[Command, bool] = CommandManager._switches.copy()
        """命令开关状态字典。"""
        self.patterns: Dict[Pattern[str], Command] = CommandManager._patterns.copy()
        """
        命令正则匹配字典。
        版本: 1.7.0+
        """

    @classmethod
    def add_command(cls, cmd_name: CommandName_T, cmd: Command) -> None:
        """注册一个 `Command` 对象。

        参数:
            cmd_name (nonebot.typing.CommandName_T): 命令名称
            cmd: 命令对象

        用法:
            ```python
            cmd = Command(...)
            CommandManager.add_command(name, cmd)
            ```
        """
        if cmd_name in cls._commands:
            warnings.warn(f"Command {cmd_name} already exists")
            return
        cls._switches[cmd] = True
        cls._commands[cmd_name] = cmd

    @classmethod
    def reload_command(cls, cmd_name: CommandName_T, cmd: Command) -> None:
        """更新一个已存在的命令。

        参数:
            cmd_name (nonebot.typing.CommandName_T): 命令名称
            cmd: 命令对象

        用法:
            ```python
            cmd = Command(...)
            CommandManager.reload_command(name, cmd)
            ```
        """
        if cmd_name not in cls._commands:
            warnings.warn(
                f"Command {cmd_name} does not exist. Please use add_command instead"
            )
            return

        cmd_ = cls._commands[cmd_name]
        if cmd_ in cls._switches:
            del cls._switches[cmd_]

        cls._switches[cmd] = True
        cls._commands[cmd_name] = cmd

    @classmethod
    def remove_command(cls, cmd_name: CommandName_T) -> bool:
        """移除一个已存在的命令。

        参数:
            cmd_name (nonebot.typing.CommandName_T): 命令名称

        返回:
            bool: 是否成功移除命令

        用法:
            ```python
            CommandManager.remove_command(("test", ))
            ```
        """
        if cmd_name in cls._commands:
            cmd = cls._commands[cmd_name]
            for alias in list(
                    filter(lambda x: cls._aliases[x] == cmd,
                           cls._aliases.keys())):
                del cls._aliases[alias]
            del cls._commands[cmd_name]
            if cmd in cls._switches:
                del cls._switches[cmd]
            return True
        return False

    @classmethod
    def switch_command_global(cls,
                              cmd_name: CommandName_T,
                              state: Optional[bool] = None) -> None:
        """根据 `state` 更改 command 的全局状态。

        参数:
            cmd_name (nonebot.typing.CommandName_T): 命令名称
            state:
                - `None(default)`: 切换状态，即 开 -> 关、关 -> 开
                - `bool`: 切换至指定状态，`True` -> 开、`False` -> 关

        用法:
            ```python
            from nonebot import message_preprocessor

            # 全局关闭命令test, 对所有消息生效
            CommandManager.switch_command_global(("test", ), state=False)

            @message_preprocessor
            async def processor(bot: NoneBot, event: CQEvent, plugin_manager: PluginManager):
                plugin_manager.cmd_manager.switch_command_global(("test", ), state=False)
            ```
        """
        cmd = cls._commands[cmd_name]
        cls._switches[cmd] = not cls._switches[cmd] if state is None else bool(
            state)

    @classmethod
    def add_aliases(cls, aliases: Union[Iterable[str], str], cmd: Command) -> None:
        """为 `Command` 添加命令别名。

        参数:
            aliases: 命令别名列表

        用法:
            ```python
            cmd = Command(...)
            CommandManager.add_aliases({"别名", "test"}, cmd)
            ```
        """
        if isinstance(aliases, str):
            aliases = (aliases,)
        for alias in aliases:
            if not isinstance(alias, str):
                warnings.warn(f"Alias {alias} is not a string! Ignored")
                return
            elif alias in cls._aliases:
                warnings.warn(f"Alias {alias} already exists")
                return
            cls._aliases[alias] = cmd

    @classmethod
    def add_patterns(cls, patterns: Patterns_T, cmd: Command) -> None:
        """Register command alias(es)

        Args:
            patterns (Union[Iterable[Pattern], Pattern, Iterable[str], str]): Command patterns
            cmd (Command): Matched command
        """
        if isinstance(patterns, (str, Pattern)):
            patterns = (patterns,)
        for pattern in patterns:
            if isinstance(pattern, str):
                pattern = re.compile(pattern)
            if not isinstance(pattern, Pattern):
                warnings.warn(
                    f"Pattern {pattern} is not a regex or string! Ignored")
                continue
            elif pattern in cls._patterns:
                warnings.warn(f"Pattern {pattern} already exists")
                continue
            cls._patterns[pattern] = cmd

    def _find_command(self, name: Union[str,
                                        CommandName_T]) -> Optional[Command]:
        cmd_name = (name,) if isinstance(name, str) else name
        if not cmd_name:
            return None

        cmd = {
            name: cmd
            for name, cmd in self.commands.items()
            if self.switches.get(cmd, True)
        }.get(cmd_name)
        return cmd

    def parse_command(
            self, bot: NoneBot,
            cmd_string: str) -> Tuple[Optional[Command], Optional[str]]:
        logger.debug(f'Parsing command: {repr(cmd_string)}')

        matched_start = None
        for start in bot.config.COMMAND_START:
            # loop through COMMAND_START to find the longest matched start
            curr_matched_start = None
            if isinstance(start, str):
                if cmd_string.startswith(start):
                    curr_matched_start = start
            elif isinstance(start, Pattern):
                m = start.search(cmd_string)
                if m and m.start(0) == 0:
                    curr_matched_start = m.group(0)

            if curr_matched_start is not None and \
                    (matched_start is None or
                    len(curr_matched_start) > len(matched_start)):
                # a longer start, use it
                matched_start = curr_matched_start

        if matched_start is None:
            # it's not a command
            logger.debug('It\'s not a command')
            return None, None

        logger.debug(f'Matched command start: '
                     f'{matched_start}{"(empty)" if not matched_start else ""}')
        full_command = cmd_string[len(matched_start):].lstrip()

        if not full_command:
            # command is empty
            return None, None

        cmd_name_text, *cmd_remained = full_command.split(maxsplit=1)

        cmd_name = None
        for sep in bot.config.COMMAND_SEP:
            # loop through COMMAND_SEP to find the most optimized split
            curr_cmd_name = None
            if isinstance(sep, str):
                curr_cmd_name = tuple(cmd_name_text.split(sep))
            elif isinstance(sep, Pattern):
                curr_cmd_name = tuple(sep.split(cmd_name_text))

            if curr_cmd_name is not None and \
                    (not cmd_name or len(curr_cmd_name) > len(cmd_name)):
                # a more optimized split, use it
                cmd_name = curr_cmd_name

        if not cmd_name:
            cmd_name = (cmd_name_text,)
        logger.debug(f'Split command name: {cmd_name}')

        cmd = self._find_command(cmd_name)  # type: ignore
        if not cmd:
            logger.debug(f'Command {cmd_name} not found. Try to match aliases')
            cmd = {
                name: cmd
                for name, cmd in self.aliases.items()
                if self.switches.get(cmd, True)
            }.get(cmd_name_text)

        if not cmd:
            logger.debug(f'Alias {cmd_name} not found. Try to match patterns')
            patterns = {
                pattern: cmd
                for pattern, cmd in self.patterns.items()
                if self.switches.get(cmd, True)
            }
            for pattern in patterns:
                if pattern.search(full_command):
                    cmd = patterns[pattern]
                    logger.debug(
                        f'Pattern {pattern} of command {cmd.name} matched, function: {cmd.func}'
                    )
                    # if command matched by regex, it will use the full_command as the current_arg of the session
                    return cmd, full_command

        if not cmd:
            return None, None

        logger.debug(f'Command {cmd.name} found, function: {cmd.func}')
        return cmd, ''.join(cmd_remained)

    def switch_command(self,
                       cmd_name: CommandName_T,
                       state: Optional[bool] = None) -> None:
        """根据 `state` 更改 command 的状态。仅对当前消息有效。

        参数:
            cmd_name (nonebot.typing.CommandName_T): 命令名称
            state:
                - `None(default)`: 切换状态，即 开 -> 关、关 -> 开
                - `bool`: 切换至指定状态，`True` -> 开、`False` -> 关

        用法:
            ```python
            from nonebot import message_preprocessor

            # 关闭命令test, 仅对当前消息生效
            @message_preprocessor
            async def processor(bot: NoneBot, event: CQEvent, plugin_manager: PluginManager):
                plugin_manager.cmd_manager.switch_command(("test", ), state=False)
            ```
        """
        cmd = self.commands[cmd_name]
        self.switches[cmd] = not self.switches[cmd] if state is None else bool(
            state)

class CommandSession(BaseSession):
    """
    继承自 `BaseSession` 类，表示命令 Session。

    参数:
        bot:
        event:
        cmd:
        current_arg:
        args (Optional[nonebot.typing.CommandArgs_T]):
    """

    __slots__ = ('cmd', 'current_key', 'current_arg_filters',
                 '_current_send_kwargs', 'current_arg', '_current_arg_text',
                 '_current_arg_images', '_state', '_last_interaction',
                 '_running', '_run_future', '_future')

    def __init__(self,
                 bot: NoneBot,
                 event: CQEvent,
                 cmd: Command,
                 *,
                 current_arg: Optional[str] = '',
                 args: Optional[CommandArgs_T] = None):
        super().__init__(bot, event)
        self.cmd = cmd  # Command object

        # unique key of the argument that is currently requesting (asking)
        self.current_key: Optional[str] = None
        """命令会话当前正在询问用户的参数的键（或称参数的名字）。第一次运行会话时，该属性为 `None`。"""

        # initialize current argument filters
        self.current_arg_filters: Optional[List[Filter_T]] = None

        self._current_send_kwargs: Dict[str, Any] = {}

        # initialize current argument
        self.current_arg: Optional[str] = ''  # with potential CQ codes
        """命令会话当前参数。实际上是 酷 Q 收到的消息去掉命令名的剩下部分，因此可能存在 CQ 码。"""
        self._current_arg_text = None
        self._current_arg_images = None
        self.refresh(event, current_arg=current_arg)  # fill the above

        self._run_future = partial(asyncio.run_coroutine_threadsafe,
                                   loop=bot.loop)

        self._state: State_T = {}
        if args:
            self._state.update(args)

        self._last_interaction = None  # last interaction time of this session
        self._running = False
        self._future: Optional[asyncio.Future] = None

    @property
    def state(self) -> State_T:
        """
        {anno}`nonebot.typing.State_T`
        命令会话的状态数据（包括已获得的所有参数）。

        属性本身只读，但属性中的内容可读写。

        版本: 1.2.0+

        用法:
            ```python
            if not session.state.get('initialized'):
                # ... 初始化工作
                session.state['initialized'] = True
            ```

            在命令处理函数的开头进行**每次命令调用只应该执行一次的初始化操作**。
        """
        return self._state

    @property
    def running(self) -> bool:
        """INTERNAL API"""
        return self._running

    @running.setter
    def running(self, value) -> None:
        """INTERNAL API"""
        if self._running is True and value is False:
            # change status from running to not running, record the time
            self._last_interaction = datetime.now()
        self._running = value

    @property
    def waiting(self) -> bool:
        """INTERNAL API"""
        return self._future is not None and not self._future.done()

    @property
    def expire_timeout(self) -> Optional[timedelta]:
        """INTERNAL API"""
        if self.cmd.expire_timeout is not ...:
            return self.cmd.expire_timeout
        return self.bot.config.SESSION_EXPIRE_TIMEOUT

    @property
    def run_timeout(self) -> Optional[timedelta]:
        """INTERNAL API"""
        if self.cmd.run_timeout is not ...:
            return self.cmd.run_timeout
        return self.bot.config.SESSION_RUN_TIMEOUT

    @property
    def is_valid(self) -> bool:
        """
        INTERNAL API

        Check whether the session has expired or not.
        """
        tm = self.expire_timeout
        if tm and self._last_interaction and \
            datetime.now() - self._last_interaction > tm:
            return False
        return True

    @property
    def is_first_run(self) -> bool:
        """命令会话是否第一次运行。"""
        return self._last_interaction is None

    @property
    def current_arg_text(self) -> str:
        """`current_arg` 属性的纯文本部分（不包含 CQ 码），各部分使用空格连接。"""
        if self._current_arg_text is None:
            self._current_arg_text = Message(
                self.current_arg).extract_plain_text()
        return self._current_arg_text

    @property
    def current_arg_images(self) -> List[str]:
        """`current_arg` 属性中所有图片的 URL 的列表，如果参数中没有图片，则为 `[]`。"""
        if self._current_arg_images is None:
            self._current_arg_images = [
                s.data['url']
                for s in Message(self.current_arg)
                if s.type == 'image' and 'url' in s.data
            ]
        return self._current_arg_images

    @property
    def argv(self) -> List[str]:
        """
        命令参数列表，类似于 `sys.argv`，本质上是 `session.state.get('argv', [])`，**需要搭配 `on_command(..., shell_like=True)` 使用**。

        用法:
            ```python
            @on_command('some_cmd', shell_like=True)
            async def _(session: CommandSession):
                argv = session.argv
            ```
        """
        return self.state.get('argv', [])

    def refresh(self,
                event: CQEvent,
                *,
                current_arg: Optional[str] = '') -> None:
        """
        INTERNAL API

        Refill the session with a new message event.

        :param event: new message event
        :param current_arg: new command argument as a string
        """
        self.event = event
        self.current_arg = current_arg
        self._current_arg_text = None
        self._current_arg_images = None

    def get(self,
            key: str,
            *,
            prompt: Optional[Message_T] = None,
            arg_filters: Optional[List[Filter_T]] = None,
            **kwargs) -> Any:
        r"""
        从 `state` 属性获取参数，如果参数不存在，则暂停当前会话，向用户发送提示，并等待用户的新一轮交互。

        如果需要暂停当前会话，则命令处理器中，此函数调用之后的语句将不会被执行（除非捕获了此函数抛出的特殊异常）。

        注意，一旦传入 `arg_filters` 参数（参数过滤器），则等用户再次输入时，_command_func._`args_parser` 所注册的参数解析函数将不会被运行，而会在对 `current_arg` 依次运行过滤器之后直接将其放入 `state` 属性中。

        :::tip
        推荐使用 `aget` 方法。
        :::

        参数:
            key: 参数的键
            prompt (Optional[nonebot.typing.Message_T]): 提示的消息内容
            arg_filters (Optional[List[nonebot.typing.Filter_T]]) {version}`1.2.0+`: 用于处理和验证用户输入的参数的过滤器
            kwargs: 其它传入 `BaseSession.send()` 的命名参数

        返回:
            Any: 参数的值

        用法:
            ```python
            location = session.get('location', prompt='请输入要查询的地区')
            ```

            获取位置信息，如果当前还不知道，则询问用户。

            ```python
            from nonebot.command.argfilter import extractors, validators

            time = session.get(
                'time', prompt='你需要我在什么时间提醒你呢？',
                arg_filters=[
                    extractors.extract_text,  # 取纯文本部分
                    controllers.handle_cancellation(session),  # 处理用户可能的取消指令
                    str.strip,  # 去掉两边空白字符
                    # 正则匹配输入格式
                    validators.match_regex(r'^\d{4}-\d{2}-\d{2}$', '格式不对啦，请重新输入')
                ]
            )
            ```

            获取时间信息，如果当前还不知道，则询问用户，等待用户输入之后，会依次运行 `arg_filters` 参数中的过滤器，以确保参数内容和格式符合要求。
        """
        if key in self.state:
            return self.state[key]

        self.current_key = key
        self.current_arg_filters = arg_filters
        self._current_send_kwargs = kwargs
        self.pause(prompt, **kwargs)

    async def aget(self,
                   key: str = ...,
                   *,
                   prompt: Optional[Message_T] = None,
                   arg_filters: Optional[List[Filter_T]] = None,
                   force_update: bool = ...,
                   **kwargs) -> Any:
        r"""
        从 `state` 属性获取参数，如果参数不存在，则异步地暂停当前会话，向用户发送提示，并等待用户的进一步交互。

        当用户再次输入时，不会重新运行命令处理器，而是回到此函数调用之处继续执行。

        注意，一旦传入 `arg_filters` 参数（参数过滤器），则等用户再次输入时，_command_func._`args_parser` 所注册的参数解析函数将不会被运行，而会在对 `current_arg` 依次运行过滤器之后直接将其放入 `state` 属性中。

        版本: 1.8.0+

        参数:
            key: 参数的键，若不传入则使用默认键值
            prompt (Optional[nonebot.typing.Message_T]): 提示的消息内容
            arg_filters (Optional[List[nonebot.typing.Filter_T]]): 用于处理和验证用户输入的参数的过滤器
            force_update: 是否强制获取用户新的输入，若是，则会忽略已有的当前参数，若 `key` 不传入则为真，否则默认为假
            kwargs: 其它传入 `BaseSession.send()` 的命名参数

        返回:
            Any: 参数的值

        用法:
            ```python
            from nonebot.command.argfilter import extractors, validators

            note = await session.aget(
                'note', prompt='你需要我提醒你什么呢',
                arg_filters=[
                    extractors.extract_text,  # 取纯文本部分
                    controllers.handle_cancellation(session),  # 处理用户可能的取消指令
                    str.strip  # 去掉两边空白字符
                ]
            )

            time = await session.aget(
                'time', prompt='你需要我在什么时间提醒你呢？',
                arg_filters=[
                    extractors.extract_text,  # 取纯文本部分
                    controllers.handle_cancellation(session),  # 处理用户可能的取消指令
                    str.strip,  # 去掉两边空白字符
                    # 正则匹配输入格式
                    validators.match_regex(r'^\d{4}-\d{2}-\d{2}$', '格式不对啦，请重新输入')
                ]
            )
            ```

            连续获取多个参数，如果当前还不知道，则询问用户，等待用户输入之后，会依次运行 `arg_filters` 参数中的过滤器，以确保参数内容和格式符合要求。
        """
        if key is ...:
            key = '__default_argument'
            force_update = True
        if key in self.state:
            if force_update is True:
                del self.state[key]
            else:
                return self.state[key]

        self.current_key = key
        self.current_arg_filters = arg_filters
        self._current_send_kwargs = kwargs
        await self.apause(prompt, **kwargs)

        return self.state.get(key, self.current_arg)

    def pause(self, message: Optional[Message_T] = None, **kwargs) -> NoReturn:
        """
        暂停当前命令会话，并发送消息。此函数调用之后的语句将不会被执行（除非捕获了此函数抛出的特殊异常）。

        :::tip
        推荐使用 `apause` 方法。
        :::

        参数:
            message (Optional[nonebot.typing.Message_T]): 要发送的消息，若不传入则不发送
            kwargs: 其它传入 `BaseSession.send()` 的命名参数

        用法:
            ```python
            if session.is_first_run:
                session.pause('请发送要处理的图片，发送 done 结束')
            if session.current_arg_text.strip() == 'done':
                session.finish('处理完成')
            process_images(session.current_arg_images)
            session.pause('请继续发送要处理的图片，发送 done 结束')
            ```

            需要连续接收用户输入，并且过程中不需要改变 `current_key` 时，使用此函数暂停会话。
        """
        if message:
            self._run_future(self.send(message, **kwargs))
        self._raise(_PauseException())

    async def apause(self, message: Optional[Message_T] = None, **kwargs) -> None:
        """
        异步地暂停当前命令会话，并发送消息。

        当用户再次输入时，不会重新运行命令处理器，而是回到此函数调用之处继续执行。

        版本: 1.8.0+

        参数:
            message (Optional[nonebot.typing.Message_T]): 要发送的消息，若不传入则不发送
            kwargs: 其它传入 `BaseSession.send()` 的命名参数

        用法:
            ```python
            await session.apause('请发送要处理的图片，发送 done 结束')
            while True:
                if session.current_arg_text.strip() == 'done':
                    session.finish('处理完成')
                process_images(session.current_arg_images)
                await session.apause('请继续发送要处理的图片，发送 done 结束')
            ```

            需要连续接收用户输入，并且过程中不需要改变 `current_key` 时，使用此函数暂停会话。
        """
        if message:
            self._run_future(self.send(message, **kwargs))
        if self.waiting:
            return
        while True:
            try:
                self._future = asyncio.get_event_loop().create_future()
                self.running = False
                timeout_opt = self.expire_timeout
                timeout = timeout_opt.total_seconds() if timeout_opt else None
                await asyncio.wait_for(self._future, timeout)
                break
            except _PauseException:
                continue
            except (_FinishException, asyncio.TimeoutError):
                raise

    def finish(self, message: Optional[Message_T] = None, **kwargs) -> NoReturn:
        """
        结束当前命令会话，并发送消息。此函数调用之后的语句将不会被执行（除非捕获了此函数抛出的特殊异常）。

        调用此函数后，命令将被视为已经完成，当前命令会话将被移除。

        参数:
            message (Optional[nonebot.typing.Message_T]): 要发送的消息，若不传入则不发送
            kwargs: 其它传入 `BaseSession.send()` 的命名参数

        用法:
            ```python
            session.finish('感谢您的使用～')
            ```
        """
        if message:
            self._run_future(self.send(message, **kwargs))
        self._raise(_FinishException())

    def switch(self, new_message: Message_T) -> NoReturn:
        """
        结束当前会话，改变当前消息事件中的消息内容，然后重新处理消息事件。

        此函数可用于从一个命令中跳出，将用户输入的剩余部分作为新的消息来处理，例如可实现以下对话:

        ```
        用户: 帮我查下天气
        Bot: 你要查询哪里的天气呢？
        用户: 算了，帮我查下今天下午南京到上海的火车票吧
        Bot: 今天下午南京到上海的火车票有如下班次: blahblahblah
        ```

        这里进行到第三行时，命令期待的是一个地点，但实际发现消息的开头是「算了」，于是调用 `switch('帮我查下今天下午南京到上海的火车票吧')`，结束天气命令，将剩下来的内容作为新的消息来处理（触发火车票插件的自然语言处理器，进而调用火车票查询命令）。

        参数:
            new_message (nonebot.typing.Message_T): 要覆盖消息事件的新消息内容

        用法:
            ```python
            @my_cmd.args_parser
            async def _(session: CommandSession)
                if not session.is_first_run and session.current_arg.startswith('算了，'):
                    session.switch(session.current_arg[len('算了，'):])
            ```

            使用「算了」来取消当前命令，转而进入新的消息处理流程。这个例子比较简单，实际应用中可以使用更复杂的 NLP 技术来判断。
        """
        if self.is_first_run:
            # if calling this method during first run,
            # we think the command is not handled
            raise _FinishException(result=False)

        if not isinstance(new_message, Message):
            new_message = Message(new_message)
        self._raise(SwitchException(new_message))

    def _raise(self, e: Exception) -> NoReturn:
        """Raise an exception from the main execution path"""
        if self.waiting:
            self._future.set_exception(e)
            raise _YieldException
        raise e

async def handle_command(bot: NoneBot, event: CQEvent,
                         manager: CommandManager) -> Optional[bool]:
    """
    INTERNAL API

    Handle a message as a command.

    This function is typically called by "handle_message".

    :param bot: NoneBot instance
    :param event: message event
    :param manager: command manager
    :return: the message is handled as a command
    """
    cmd, current_arg = manager.parse_command(bot, str(event.message).lstrip())
    is_privileged_cmd = cmd and cmd.privileged
    if is_privileged_cmd and cmd.only_to_me and not event['to_me']:
        is_privileged_cmd = False
    disable_interaction = bool(is_privileged_cmd)

    if is_privileged_cmd:
        logger.debug(f'Command {cmd.name} is a privileged command')

    ctx_id = context_id(event)

    if not is_privileged_cmd:
        # wait for 1.5 seconds (at most) if the current session is running
        retry = 5
        while retry > 0 and \
                _sessions.get(ctx_id) and _sessions[ctx_id].running:
            retry -= 1
            await asyncio.sleep(0.3)

    check_perm = True
    session: Optional[CommandSession] = _sessions.get(
        ctx_id) if not is_privileged_cmd else None

    if session is not None:
        if session.running:
            logger.warning(f'There is a session of command '
                           f'{session.cmd.name} running, notify the user')
            asyncio.ensure_future(
                send(bot, event,
                     render_expression(bot.config.SESSION_RUNNING_EXPRESSION)))
            # pretend we are successful, so that NLP won't handle it
            return True

        if session.is_valid:
            logger.debug(f'Session of command {session.cmd.name} exists')
            # since it's in a session, the user must be talking to me
            event['to_me'] = True
            session.refresh(event, current_arg=str(event['message']))
            # there is no need to check permission for existing session
            check_perm = False
        else:
            # the session is expired, remove it
            logger.debug(f'Session of command {session.cmd.name} has expired')
            if ctx_id in _sessions:
                del _sessions[ctx_id]
            session = None

    if session is None:
        if not cmd:
            logger.debug('Not a known command, ignored')
            return False
        if cmd.only_to_me and not event['to_me']:
            logger.debug('Not to me, ignored')
            return False
        SessionClass = cmd.session_class or CommandSession
        session = SessionClass(bot, event, cmd, current_arg=current_arg)
        logger.debug(f'New session of command {session.cmd.name} created')

    assert isinstance(session, CommandSession)
    return await _real_run_command(session,
                                   ctx_id,
                                   check_perm=check_perm,
                                   disable_interaction=disable_interaction)

async def call_command(bot: NoneBot,
                       event: CQEvent,
                       name: Union[str, CommandName_T],
                       *,
                       current_arg: str = '',
                       args: Optional[CommandArgs_T] = None,
                       check_perm: bool = True,
                       disable_interaction: bool = False) -> Optional[bool]:
    """从内部直接调用命令。可用于在一个插件中直接调用另一个插件的命令。

    参数:
        bot: NoneBot 对象
        event: 事件对象
        name (Union[str, nonebot.typing.CommandName_T]): 要调用的命令名
        current_arg: 命令会话的当前输入参数
        args (Optional[nonebot.typing.CommandArgs_T]): 命令会话的（初始）参数（将会被并入命令会话的 `state` 属性）
        check_perm: 是否检查命令的权限，若否，则即使当前事件上下文并没有权限调用这里指定的命令，也仍然会调用成功
        disable_interaction: 是否禁用交互功能，若是，则该命令的会话不会覆盖任何当前已存在的命令会话，新创建的会话也不会保留

    返回:
        bool: 命令是否调用成功

    用法:
        ```python
        await call_command(bot, event, 'say', current_arg='[CQ:face,id=14]', check_perm=False)
        ```

        从内部调用 `say` 命令，且不检查权限。
    """
    cmd = CommandManager()._find_command(name)
    if not cmd:
        return False
    SessionClass = cmd.session_class or CommandSession
    session = SessionClass(bot, event, cmd, current_arg=current_arg, args=args)
    return await _real_run_command(session,
                                   context_id(session.event),
                                   check_perm=check_perm,
                                   disable_interaction=disable_interaction)

async def _real_run_command(session: CommandSession,
                            ctx_id: str,
                            disable_interaction: bool = False,
                            **kwargs) -> Optional[bool]:
    if not disable_interaction:
        # override session only when interaction is not disabled
        _sessions[ctx_id] = session
    try:
        logger.debug(f'Running command {session.cmd.name}')
        session.running = True
        future = asyncio.ensure_future(session.cmd.run(session, **kwargs))
        timeout_opt = session.run_timeout
        timeout = timeout_opt.total_seconds() if timeout_opt else None

        try:
            await asyncio.wait_for(future, timeout)
            handled = future.result()
        except asyncio.TimeoutError:
            handled = True
        except CommandInterrupt:
            raise
        except Exception as e:
            logger.error(f'An exception occurred while '
                         f'running command {session.cmd.name}:')
            logger.exception(e)
            handled = True
        raise _FinishException(handled)
    except _PauseException:
        session.running = False
        if disable_interaction:
            # if the command needs further interaction, we view it as failed
            return False
        logger.debug(f'Further interaction needed for '
                     f'command {session.cmd.name}')
        # return True because this step of the session is successful
        return True
    except _YieldException:
        return True
        # return True because this step of the session is successful
    except (_FinishException, SwitchException) as e:
        session.running = False
        logger.debug(f'Session of command {session.cmd.name} finished')
        if not disable_interaction and ctx_id in _sessions:
            # the command is finished, remove the session,
            # but if interaction is disabled during this command call,
            # we leave the _sessions untouched.
            del _sessions[ctx_id]

        if isinstance(e, _FinishException):
            return e.result
        elif isinstance(e, SwitchException):
            # we are guaranteed that the session is not first run here,
            # which means interaction is definitely enabled,
            # so we can safely touch _sessions here.
            if ctx_id in _sessions:
                # make sure there is no session waiting
                del _sessions[ctx_id]
            logger.debug(f'Session of command {session.cmd.name} switching, '
                         f'new message: {e.new_message}')
            raise e  # this is intended to be propagated to handle_message()

def kill_current_session(event: CQEvent) -> None:
    """强行移除当前已存在的任何命令会话，即使它正在运行。该函数可用于强制移除执行时间超过预期的命令，以保证新的消息不会被拒绝服务。

    参数:
        event: 事件对象

    用法:
        ```python
        @on_command('kill', privileged=True)
        async def _(session: CommandSession):
            kill_current_session(session.event)
        ```

        在特权命令 `kill` 中强行移除当前正在运行的会话。
    """
    ctx_id = context_id(event)
    if ctx_id in _sessions:
        if _sessions[ctx_id].waiting:
            _sessions[ctx_id]._future.set_exception(_FinishException())
        del _sessions[ctx_id]

from nonebot.command.group import CommandGroup

__all__ = [
    'CommandManager',
    'CommandSession',
    'call_command',
    'kill_current_session',
    'CommandGroup',
]

__autodoc__ = {
    "CommandInterrupt": False,
    "SwitchException": False,
    "Command": False,
    "CommandManager.add_patterns": False,
    "CommandManager.parse_command": False,
    "CommandSession.running": False,
    "CommandSession.waiting": False,
    "CommandSession.expire_timeout": False,
    "CommandSession.run_timeout": False,
    "CommandSession.is_valid": False,
    "CommandSession.refresh": False,
    "handle_command": False
}


from nicegui import ui

dict_of_tabs = {
    "Tab1": "Content of Tab1",
    "Tab2": "Content of Tab2",
}

# initialize tab_counter with the length of the dict_of_tabs
tab_counter = len(dict_of_tabs)

def add_tab():
    global tab_counter
    # use the tab_counter to create a new tab name
    new_tab_name = f'Tab{tab_counter + 1}'
    tab_counter += 1
    dict_of_tabs[new_tab_name] = f'Content of {new_tab_name}'
    tabs.refresh()

def delete_tab(title):
    if title in dict_of_tabs:
        del dict_of_tabs[title]
        tabs.refresh()

@ui.refreshable
def tabs():
    with ui.tabs().classes('w-full') as tabs:
        for title in dict_of_tabs:
            with ui.tab(name=title, label=''):
                with ui.row().classes('items-center justify-between'):
                    ui.label(title).classes('text-lg')
                    ui.button(icon='close', on_click=lambda t=title: delete_tab(t)).classes('m-0 p-0 text-black dark:text-white text-xs').props('flat')

        with ui.tab(name="+", label='').classes('p-0 m-0'):
            ui.button(icon='add', on_click=add_tab).props('outline').classes('h-full w-full m-0 text-black dark:text-white')

    with ui.tab_panels(tabs):
        for title in dict_of_tabs:
            with ui.tab_panel(title):
                ui.label(dict_of_tabs[title])
        with ui.tab_panel("+"):
            pass

tabs()

ui.run()


from flask import Flask, render_template
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import logging
from .common.filters import format_datetime

login_manager = LoginManager()
db = SQLAlchemy()
migrate = Migrate()

def create_app(settings_module):

    app = Flask(__name__, instance_relative_config=True)

    app.config.from_object(settings_module)

    if app.config.get('TESTING'):
        app.config.from_pyfile('config_testing.py', silent=True)
    else:
        app.config.from_pyfile('config.py')

    login_manager.init_app(app)
    login_manager.login_view = "auth.login"

    db.init_app(app)
    migrate.init_app(app, db)

    #Registro de los filtros
    register_filters(app)

    # Registro de los blueprints
    from .public import public_bp
    app.register_blueprint(public_bp)
    from .admin import admin_bp
    app.register_blueprint(admin_bp)
    from .auth import auth_bp
    app.register_blueprint(auth_bp)

    register_error_handlers(app)

    configure_logging(app)

    return app

def register_error_handlers(app):

    @app.errorhandler(500)
    def base_error_handler(e):
        return render_tempalte("500.html"), 500

    @app.errorhandler(404)
    def error_400_handler(e):
        print('llamando al error handler 400')
        return render_template("404.html"), 404

def configure_logging(app):

    # Eliminamos los posibles manejadores del logger por defecto (si existen)
    del app.logger.handlers[:]

    #Añadimos el logger por defecto a la lista de loggers
    loggers = [app.logger, ]
    handlers = []

    #Creamos un manejador para escribir los mensajes por consola
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG)
    console_handler.setFormatter(logging.Formatter('[%(asctime)s] - %(name)s - %(levelname)s - %(message)s'))
    handlers.append(console_handler)

    file_handler = logging.FileHandler("flask-blog.log")
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(logging.Formatter('[%(asctime)s] - %(name)s - %(levelname)s - %(message)s'))
    handlers.append(file_handler)

    #Añadimos cada uno de los handlers a cada uno de los loggers

    for l in loggers:
        for handler in handlers:
            l.addHandler(handler)
        l.propagate = False
        l.setLevel(logging.DEBUG)

def register_filters(app):
    app.jinja_env.filters['datetime'] = format_datetime


"""Base support for certificate storage backends.

Version Added:
    6.0
"""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import Generic, Iterator, Optional, TYPE_CHECKING, TypeVar

from typing_extensions import TypedDict

from reviewboard.certs.cert import CertDataFormat
from reviewboard.certs.errors import CertificateNotFoundError

if TYPE_CHECKING:
    from djblets.util.typing import StrOrPromise
    from reviewboard.certs.cert import (Certificate,
                                        CertificateBundle,
                                        CertificateFingerprints)
    from reviewboard.site.models import LocalSite, AnyOrAllLocalSites

logger = logging.getLogger(__name__)

class BaseStoredData(ABC):
    """Base class for a stored certificate-related data.

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The Local Site owning this stored certificate.
    #:
    #: Type:
    #:     reviewboard.site.models.LocalSite
    local_site: Optional[LocalSite]

    #: The storage backend managing this certificate.
    #:
    #: Type:
    #:     BaseCertificateStorageBackend
    storage: BaseCertificateStorageBackend

    #: A unique ID for this data in the storage backend.
    #:
    #: This should be considered an opaque value outside of the storage
    #: backend.
    #:
    #: Type:
    #:     str
    storage_id: Optional[str]

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        storage_id: Optional[str] = None,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Initialize the stored data.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        self.storage = storage
        self.storage_id = storage_id
        self.local_site = local_site

class BaseStoredCertificate(BaseStoredData):
    """Base class for a stored certificate.

    These represent a certificate stored in the backend, associating
    certificate data with backend-specific data such as a storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`get_cert_file_path`
    * :py:meth:`get_key_file_path`
    * :py:meth:`load_certificate`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The certificate data being stored.
    #:
    #: Type:
    #:     reviewboard.certs.cert.Certificate
    _certificate: Optional[Certificate]

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        certificate: Optional[Certificate] = None,
        storage_id: Optional[str] = None,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Initialize the stored certificate.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            certificate (reviewboard.certs.cert.Certificate, optional):
                The certificate data being stored.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)
        self._certificate = certificate

    @property
    def certificate(self) -> Certificate:
        """The Certificate data in storage.

        Type:
            reviewboard.certs.cert.Certificate

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the certificate from storage.

                Details are in the error message.
        """
        if self._certificate is None:
            self._certificate = self.load_certificate()

        return self._certificate

    @abstractmethod
    def get_cert_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> str:
        """Return the filesystem path to a certificate.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested certificate data file format.

        Returns:
            str:
            The resulting file path.
        """
        raise NotImplementedError

    @abstractmethod
    def get_key_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> Optional[str]:
        """Return the filesystem path to a certificate private key.

        Not all certificates will have an associated private key. If one is
        not available, this will return ``None``.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested private key data file format.

        Returns:
            str:
            The resulting file path, or ``None`` if a private key is not
            available.
        """
        raise NotImplementedError

    @abstractmethod
    def load_certificate(self) -> Certificate:
        """Load and return a Certificate from storage.

        This must be implemented by subclasses.

        Returns:
            reviewboard.certs.cert.Certificate:
            The resulting loaded certificate.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the certificate from storage.

                Details are in the error message.
        """
        raise NotImplementedError

class BaseStoredCertificateBundle(BaseStoredData):
    """Base class for a stored CA bundle.

    These represent a root CA bundle in the backend, associating root and
    intermediary certificate data with backend-specific data such as a
    storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`load_bundle`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The certificate bundle data being stored.
    #:
    #: Type:
    #:     reviewboard.certs.cert.CertificateBundle
    _bundle: Optional[CertificateBundle]

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        bundle: Optional[CertificateBundle] = None,
        storage_id: Optional[str] = None,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Initialize the stored certificate.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            bundle (reviewboard.certs.cert.CertificateBundle, optional):
                The certificate bundle data being stored.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)

        self._bundle = bundle

    @property
    def bundle(self) -> CertificateBundle:
        """The CA bundle data in storage.

        Type:
            reviewboard.certs.cert.CertificateBundle

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the CA bundle from storage.

                Details are in the error message.
        """
        if self._bundle is None:
            self._bundle = self.load_bundle()

        return self._bundle

    @abstractmethod
    def get_bundle_file_path(
        self,
        *,
        data_format: CertDataFormat = CertDataFormat.PEM,
    ) -> str:
        """Return the filesystem path to a certificate bundle.

        This must be implemented by subclasses.

        Args:
            data_format (reviewboard.certs.cert.CertDataFormat, optional):
                The requested certificate bundle data file format.

        Returns:
            str:
            The resulting file path.
        """
        raise NotImplementedError

    @abstractmethod
    def load_bundle(self) -> CertificateBundle:
        """Load and return the CA bundle data from storage.

        Returns:
            reviewboard.certs.cert.CertificateBundle:
            The resulting loaded CA bundle.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the CA bundle from storage.

                Details are in the error message.
        """
        raise NotImplementedError

class BaseStoredCertificateFingerprints(BaseStoredData):
    """Base class for stored certificate fingerprints.

    These represent fingerprints that can be used to verify a server-issued
    certificate, associating them with backend-specific data such as a
    storage ID.

    Backends must subclass this in order to implement the following:

    * :py:meth:`load_fingerprints`

    Version Added:
        6.0
    """

    ######################
    # Instance variables #
    ######################

    #: The fingerprints data being stored.
    #:
    #: Type:
    #:     reviewboard.certs.cert.CertificateFingerprints
    _fingerprints: Optional[CertificateFingerprints]

    def __init__(
        self,
        *,
        storage: BaseCertificateStorageBackend,
        fingerprints: Optional[CertificateFingerprints] = None,
        storage_id: Optional[str] = None,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Initialize the stored certificate.

        Args:
            storage (BaseCertificateStorageBackend):
                The storage backend managing this stored certificate.

            fingerprints (reviewboard.certs.cert.CertificateFingerprints):
                The certificate fingerprints data being stored.

            storage_id (str, optional):
                The opaque ID of the stored data in the backend.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site owning this stored certificate.
        """
        super().__init__(storage=storage,
                         storage_id=storage_id,
                         local_site=local_site)

        self._fingerprints = fingerprints

    @property
    def fingerprints(self) -> CertificateFingerprints:
        """The certificate fingerprint data in storage.

        Type:
            reviewboard.certs.cert.CertificateFingerprints

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the fingerprints from storage.

                Details are in the error message.
        """
        if self._fingerprints is None:
            self._fingerprints = self.load_fingerprints()

        return self._fingerprints

    @abstractmethod
    def load_fingerprints(self) -> CertificateFingerprints:
        """Load and return the certificate fingerprint data from storage.

        Returns:
            reviewboard.certs.cert.CertificateFingerprints:
            The resulting loaded fingerprints.

        Raises:
            reviewboard.certs.errors.CertificateStorageError:
                There was an error loading the fingerprints from storage.

                Details are in the error message.
        """
        raise NotImplementedError

class StorageStats(TypedDict):
    """Statistics about the content in a storage backend.

    This contains counts for stored CA bundles, certificates, and verified
    fingerprints. Along with this, it contains a UUID representing the current
    generation of the stats, which is useful for caching.

    Version Added:
        6.0
    """

    #: The number of stored CA bundles.
    ca_bundle_count: int

    #: The number of stored certificates.
    cert_count: int

    #: The number of stored verified fingerprints.
    fingerprint_count: int

    #: A UUID specific to the current state of the content in the backend.
    #:
    #: This can be used to help with caching and invalidation of other state.
    state_uuid: str

_StoredCertT = TypeVar(
    '_StoredCertT',
    bound=BaseStoredCertificate)
_StoredCertBundleT = TypeVar(
    '_StoredCertBundleT',
    bound=BaseStoredCertificateBundle)
_StoredCertFingerprintsT = TypeVar(
    '_StoredCertFingerprintsT',
    bound=BaseStoredCertificateFingerprints)

class BaseCertificateStorageBackend(
    ABC,
    Generic[_StoredCertT, _StoredCertBundleT, _StoredCertFingerprintsT],
):
    """Base class for a certificate storage backend.

    Store backends are responsible for storing certificate and private key
    data, CA bundles, and fingerprint verfication information.

    Backends must be able to persist certificates, private keys, and CA bundles
    on the filesystem for the lifetime of the web server process in order to
    allow consumers to access them on demand.

    Subclasses must implement the following attributes:

    * :py:attr:`backend_id`
    * :py:attr:`name`

    And the following methods:

    * :py:meth:`add_ca_bundle`
    * :py:meth:`add_certificate`
    * :py:meth:`add_fingerprints`
    * :py:meth:`delete_ca_bundle`
    * :py:meth:`delete_certificate`
    * :py:meth:`delete_fingerprints`
    * :py:meth:`get_stats`
    * :py:meth:`get_stored_ca_bundle_by_id`
    * :py:meth:`get_stored_ca_bundle`
    * :py:meth:`get_stored_certificate_by_id`
    * :py:meth:`get_stored_certificate`
    * :py:meth:`get_stored_fingerprints_by_id`
    * :py:meth:`get_stored_fingerprints`
    * :py:meth:`iter_ca_bundles`
    * :py:meth:`iter_stored_ca_bundles`
    * :py:meth:`iter_stored_certificates`
    * :py:meth:`iter_stored_fingerprints`

    Version Added:
        6.0
    """

    #: The ID of this storage backend.
    #:
    #: This must be provided by subclasses, and must be unique.
    #:
    #: Type:
    #:     str
    backend_id: Optional[str] = None

    #: The display name of the storage backend.
    #:
    #: This must be provided by subclasses.
    #:
    #: Type:
    #:     str
    name: Optional[StrOrPromise] = ''

    ######################
    # Instance variables #
    ######################

    #: The path to the local filesystem storage usable by this backend.
    #:
    #: This is set when constructing the backend, and should not be changed.
    #: All filesystem operations must be limited to directories under this
    #: path.
    #:
    #: Type:
    #:     str
    storage_path: str

    def __init__(
        self,
        storage_path: str,
    ) -> None:
        """Initialize the storage backend."""
        assert self.backend_id, 'backend_id must be set.'
        assert self.name, 'name must be set.'

        self.storage_path = storage_path

    @abstractmethod
    def get_stats(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
    ) -> StorageStats:
        """Return statistics on the certificates managed by the backend.

        This will include the total number of stored certificates, CA bundles,
        and verified fingerprints across zero, one, or all Local Sites.

        It also includes state UUIDs that represent the current states of the
        backend.

        Subclasses must ensure that any modifications to storage result in
        updates to this UUID. They may also want to ensure information is
        properly cached for a period of time, and invalidated upon
        modification.

        Args:
            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite bound to the stats.

                If ``None``, the global site will be used.

        Returns:
            StorageStats:
            The computed or cached storage statistics.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error looking up cetificate stats.
        """
        raise NotImplementedError

    @abstractmethod
    def add_ca_bundle(
        self,
        bundle: CertificateBundle,
        *,
        local_site: Optional[LocalSite] = None,
    ) -> _StoredCertBundleT:
        """Add a root CA bundle to storage.

        The certificate bundle's name along with ``local_site`` are considered
        unique within the storage backend. If there's an existing bundle with
        this information, it will be replaced.

        Args:
            bundle (reviewboard.certs.cert.CertificateBundle):
                The bundle to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateBundle:
            The resulting stored certificate bundle.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing this CA bundle.
        """
        raise NotImplementedError

    def delete_ca_bundle(
        self,
        *,
        name: str,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Delete a root CA bundle from storage.

        Args:
            name (str):
                The unique name of the CA bundle in storage.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The CA bundle was not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this CA bundle.
        """
        stored_bundle = self.get_stored_ca_bundle(
            name=name,
            local_site=local_site)

        if stored_bundle is None:
            raise CertificateNotFoundError()

        assert stored_bundle.storage_id is not None

        self.delete_ca_bundle_by_id(stored_bundle.storage_id)

    @abstractmethod
    def delete_ca_bundle_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete a root CA bundle from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the CA bundle to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_ca_bundle(
        self,
        *,
        name: str,
        local_site: Optional[LocalSite] = None,
    ) -> Optional[_StoredCertBundleT]:
        """Return a root CA bundle in storage.

        Args:
            name (str):
                The unique name of the CA bundle in storage.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateBundle:
            The stored certificate bundle in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_ca_bundle_by_id(
        self,
        storage_id: str,
    ) -> Optional[_StoredCertBundleT]:
        """Return a root CA bundle in storage identified by ID.

        Args:
            storage_id (str):
                The ID of the CA bundle in storage.

        Returns:
            BaseStoredCertificateBundle:
            The stored certificate bundle in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this CA bundle.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_ca_bundles(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertBundleT]:
        """Iterate through all root CA bundles in storage.

        Args:
            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificateBundle:
            Each stored certificate bundle in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through CA bundles.
        """
        raise NotImplementedError

    def get_ca_bundles_dir(
        self,
        *,
        local_site: Optional[LocalSite] = None,
    ) -> str:
        """Return a path containing all CA bundle files.

        Args:
            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the bundles.

                If ``None``, the global site will be used.

        Returns:
            str:
            The path to the bundles directory.
        """
        raise NotImplementedError

    @abstractmethod
    def add_certificate(
        self,
        certificate: Certificate,
        *,
        local_site: Optional[LocalSite] = None,
    ) -> _StoredCertT:
        """Add a certificate to storage.

        The certificate's hostname and port along with ``local_site`` are
        considered unique within the storage backend. If there's an existing
        certificate with this information, it will be replaced.

        Args:
            certificate (reviewboard.certs.cert.Certificate):
                The certificate to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificate:
            The resulting stored certificate.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing this certificate.
        """
        raise NotImplementedError

    def delete_certificate(
        self,
        *,
        hostname: str,
        port: int,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Delete a certificate from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this certificate.
        """
        stored_certificate = self.get_stored_certificate(
            hostname=hostname,
            port=port,
            local_site=local_site)

        if stored_certificate is None:
            raise CertificateNotFoundError()

        assert stored_certificate.storage_id

        self.delete_certificate_by_id(stored_certificate.storage_id)

    @abstractmethod
    def delete_certificate_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete a certificate from storage.

        Args:
            storage_id (str):
                The ID of the certificate to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_certificate(
        self,
        *,
        hostname: str,
        port: int,
        local_site: Optional[LocalSite] = None,
    ) -> Optional[_StoredCertT]:
        """Return a certificate from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificate:
            The stored certificate in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_certificate_by_id(
        self,
        storage_id: str,
    ) -> Optional[_StoredCertT]:
        """Return a certificate from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate in storage.

        Returns:
            BaseStoredCertificate:
            The stored certificate in storage, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving this certificate.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_certificates(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertT]:
        """Iterate through all certificates in storage.

        Args:
            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite that owns the certificates.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificate:
            Each stored certificate in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through stored certificates.
        """
        raise NotImplementedError

    @abstractmethod
    def add_fingerprints(
        self,
        fingerprints: CertificateFingerprints,
        *,
        hostname: str,
        port: int,
        local_site: Optional[LocalSite] = None,
    ) -> _StoredCertFingerprintsT:
        """Add verified certificate fingerprints to storage.

        The ``hostname``, ``port``, and ``local_site`` values taken together
        must be unique within a storage backend.

        Backends may store this together with the certificates or separately.

        Args:
            hostname (str):
                The hostname serving the certificate.

            port (int):
                The port on the host serving the certificate.

            fingerprints (reviewboard.certs.cert.CertificateFingerprints):
                The certificate fingerprints to add.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateFingerprints:
            The resulting stored certificate fingerprints.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error storing these fingerprints.
        """
        raise NotImplementedError

    def delete_fingerprints(
        self,
        *,
        hostname: str,
        port: int,
        local_site: Optional[LocalSite] = None,
    ) -> None:
        """Delete certificate fingerprints from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting these fingerprints.
        """
        stored_fingerprints = self.get_stored_fingerprints(
            hostname=hostname,
            port=port,
            local_site=local_site)

        if stored_fingerprints is None:
            raise CertificateNotFoundError()

        assert stored_fingerprints.storage_id is not None

        self.delete_fingerprints_by_id(
            stored_fingerprints.storage_id)

    @abstractmethod
    def delete_fingerprints_by_id(
        self,
        storage_id: str,
    ) -> None:
        """Delete certificate fingerprints from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate fingerprints to delete.

        Raises:
            reviewboard.cert.errors.CertificateNotFoundError:
                The fingerprints were not found.

            reviewboard.cert.errors.CertificateStorageError:
                There was an error deleting these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def iter_stored_fingerprints(
        self,
        *,
        local_site: AnyOrAllLocalSites = None,
        start: int = 0,
    ) -> Iterator[_StoredCertFingerprintsT]:
        """Iterate through all certificate fingerprints in storage.

        Args:
            local_site (reviewboard.site.models.LocalSite or
                        reviewboard.site.models.LocalSite.ALL, optional):
                An optional LocalSite that owns the certificate fingerprints.

                If ``None``, the global site will be used.

            start (int, optional):
                The 0-based index within the list of root CA bundles to start
                iterating at.

        Yields:
            BaseStoredCertificateFingerprints:
            Each stored certificate fingerprints in storage.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error iterating through these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_fingerprints(
        self,
        *,
        hostname: str,
        port: int,
        local_site: Optional[LocalSite] = None,
    ) -> Optional[_StoredCertFingerprintsT]:
        """Return certificate fingerprints from storage.

        Args:
            hostname (str):
                The hostname of the certificate in storage.

            port (int):
                The port on the host serving the certificate.

            local_site (reviewboard.site.models.LocalSite, optional):
                An optional LocalSite that owns the certificate.

                If ``None``, the global site will be used.

        Returns:
            BaseStoredCertificateFingerprints:
            The stored certificate fingerprints, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving these fingerprints.
        """
        raise NotImplementedError

    @abstractmethod
    def get_stored_fingerprints_by_id(
        self,
        storage_id: str,
    ) -> Optional[_StoredCertFingerprintsT]:
        """Return certificate fingerpritns from storage identified by ID.

        Args:
            storage_id (str):
                The ID of the certificate fingerprints in storage.

        Returns:
            CertificateFingerprints:
            The stored certificate fingerprints, or ``None`` if not found.

        Raises:
            reviewboard.cert.errors.CertificateStorageError:
                There was an error retrieving these fingerprints.
        """
        raise NotImplementedError


"""
qfly | Qualisys Drone SDK Example Script: Control Engineering / Step Response Demo

UNSTABLE / EXPERIMENTAL / UNDER DEVELOPMENT
"""

import pynput
from time import sleep, time

import numpy as np
import matplotlib.pyplot as plt

from qfly import Pose, QualisysCrazyflie, World

# SETTINGS
cf_body_name = 'E7E7E7E704'  # QTM rigid body name
cf_uri = 'radio://0/80/2M/E7E7E7E704'  # Crazyflie address
cf_marker_ids = [41, 42, 43, 44]

# Watch key presses with a global variable
last_key_pressed = None

# Set up keyboard callback
def on_press(key):
    """React to keyboard."""
    global last_key_pressed
    last_key_pressed = key
    if key == pynput.keyboard.Key.esc:
        fly = False

# Listen to the keyboard
listener = pynput.keyboard.Listener(on_press=on_press)
listener.start()

# Set up world - the World object comes with sane defaults
world = World()

# Prepare for liftoff
with QualisysCrazyflie(cf_body_name,
                       cf_uri,
                       world,
                       marker_ids=cf_marker_ids) as qcf:

    # Let there be time
    t = time()
    dt = 0

    print("Beginning maneuvers...")

    log = []

    # MAIN LOOP WITH SAFETY CHECK
    while(qcf.is_safe()):

        # Terminate upon Esc command
        if last_key_pressed == pynput.keyboard.Key.esc:
            break

        # Mind the clock
        dt = time() - t

        # Keep track of altitude
        log.append(qcf.pose)

        # Take off and hover in the center of safe airspace for 5 seconds
        if dt < 20:
            print(f'[t={int(dt)}] Ascending...')
            qcf.rise_in_place()
            continue

        if dt < 30:
            qcf.land_in_place()
            continue

        # if dt < 50:
        #     print(f'[t={int(dt)}] Ascending...')
        #     qcf.rise_in_place(0.7)
        #     continue

        # if dt < 60:
        #     qcf.land_in_place()
        #     continue

        # if dt < 80:
        #     print(f'[t={int(dt)}] Ascending...')
        #     qcf.rise_in_place(0.4)
        #     continue

        # if dt < 90:
        #     qcf.land_in_place()
        #     continue

        else:
            break

    # Land calmly
    qcf.land_in_place()

# Plot z-axis response
_t = range(len(log))
_x = list(pose.x for pose in log)
_y = list(pose.y for pose in log)
_z = list(pose.z for pose in log)
fig, ax = plt.subplots()
ax.scatter(_x, _y)

plt.show()


# flask_web/app.py

from flask import Flask

def create_app():
    f_app = Flask(__name__)
    return f_app

app = create_app()

@app.route('/')
def hello_world():
    return 'Hey, we have Flask in a Docker container!'

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')


from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
import time

link = "http://suninjuly.github.io/selects2.html"

def sum(a, b):
    return str(int(a) + int(b))

try:
    browser = webdriver.Chrome()
    browser.get(link)

    # Получаем из верстки переменные
    number_1 = browser.find_element(By.ID, 'num1')
    number_2 = browser.find_element(By.ID, 'num2')

    # Вычисляем сумму полученных переменных
    summary = sum(number_1.text, number_2.text)

    # Ищем в select полученное значение
    select = Select(browser.find_element(By.ID, "dropdown"))
    select.select_by_value(f"{summary}")

    button = browser.find_element(By.CLASS_NAME, "btn.btn-default")
    button.click()

finally:

    # Ожидаем, чтобы визуально оценить результаты прохождения скрипта
    time.sleep(10)
    # Закрываем браузер после всех манипуляций
    browser.quit()
# Не забываем оставить пустую строку в конце файла


from django.db.models import Q

from Ads_Project.functions import LoginRequiredMixin
from django.views.generic import CreateView, UpdateView, ListView, TemplateView
from django.utils.safestring import mark_safe
from django_datatables_view.base_datatable_view import BaseDatatableView
import simplejson as json
from system.models import User, HistoryIndirect
from django.db.models import Sum

class Chart_2_View(LoginRequiredMixin, TemplateView):
    template_name = "system/moshahede/chart_2.html"

    def get_context_data(self, *args, **kwargs):
        context = context = super().get_context_data(**kwargs)
        chart_config={
            'chart': {
                'container': "#OrganiseChart1",
                'rootOrientation': 'WEST',  # NORTH || EAST || WEST || SOUTH
                'scrollbar': "fancy",
                # 'levelSeparation': 30,
                'siblingSeparatio': 40,
                'subTeeSeparation': 80,
                'connectors': {
                    'type': 'step'
                },
                'node': {
                    'HTMLclass': 'nodeExample1'
                }
            }
        }
        user=self.request.user
        parrent=self.request.user.code_moaref
        print(parrent)
        if parrent is not None:
            chart_config['nodeStructure']={
                'text':{
                    'name': "دعوت کننده شما:",
                    'title': parrent.first_name + " " + parrent.last_name
                },
                'image': "/static/img/avatar/avatar.png",
                'children': []
            }
        else:
            chart_config['nodeStructure'] = {
                'text': {
                    'name':user.first_name + ' ' + user.last_name ,
                    'title': "شما در سطح یک قرار دارید"
                },
                'image': "/static/img/avatar/avatar.png",
                'children': []
            }
        print(chart_config)
        context['chart_config'] = mark_safe(str(chart_config))
        return context

class Chart_1_View(LoginRequiredMixin, TemplateView):
    template_name = "system/moshahede/parent_children.html"

class ParentChildrenDatatableView(LoginRequiredMixin, BaseDatatableView):
    model = User
    columns = ['first_name','sath','daramad']

    def get_initial_queryset(self):
        user=self.request.user
        qs = super().get_initial_queryset()
        qs = qs.filter(list_parent__contains="[{}]".format(user.id))
        return qs

    def render_column(self, row:User, column):
        jsonDec = json.decoder.JSONDecoder()
        parents = jsonDec.decode(row.list_parent)
        if column == 'first_name':
            return row.first_name + " " +row.last_name
        if column =='sath':
            return parents.index([self.request.user.id])+1
        if column =="daramad":
            history=HistoryIndirect.objects.filter(parent=self.request.user,montasher_konande=row).all().aggregate(Sum('mablagh'))
            return history['mablagh__sum']
        return super().render_column(row, column)

class Parent_Children_List_View(LoginRequiredMixin, ListView):
    model = User
    template_name = 'system/moshahede/list_of_parent_children.html'


import pytest

import pickle

import pandas as pd

import daskperiment
from daskperiment.backend import (init_backend, LocalBackend,
                                  MongoBackend, RedisBackend)
from daskperiment.backend.base import maybe_mongo, maybe_redis

class TestInitBackend(object):

    def test_local_init_str(self):
        b = init_backend('local_backend', backend='local')
        assert isinstance(b, LocalBackend)

        exp = daskperiment.config._CACHE_DIR / 'local_backend'
        assert b.cache_dir == exp

    def test_local_init_path(self):
        p = daskperiment.config._CACHE_DIR / 'local_backend'
        b = init_backend('local_backend', backend=p)
        assert isinstance(b, LocalBackend)

        assert b.cache_dir == p

    @pytest.mark.parametrize('backend', ['redis://localhost:6379/0',
                                         'redis://dumy:9999/1'
                                         'rediss://localhost:6379/0'])
    def test_redis_init_protocol(self, backend):
        b = init_backend('local_backend', backend=backend)
        assert isinstance(b, RedisBackend)

        assert b.uri == backend

    def test_redis_init_pool(self):
        import redis
        uri = 'redis://localhost:6379/0'
        pool = redis.ConnectionPool.from_url(uri)
        b = init_backend('local_backend', backend=pool)
        assert isinstance(b, RedisBackend)

        assert b.uri == uri
        assert b.pool is pool

    def test_maybe_redis(self):
        import redis
        assert maybe_redis('redis://xxx')
        assert not maybe_redis('xxx://xxx')
        assert not maybe_redis('http://xxx')
        assert not maybe_redis(3)
        assert not maybe_redis('local')

        uri = 'redis://localhost:6379/0'
        pool = redis.ConnectionPool.from_url(uri)
        assert maybe_redis(pool)

    def test_mongo_init_db(self):
        import pymongo
        uri = 'mongodb://localhost:27017'
        client = pymongo.MongoClient(uri)
        b = init_backend('local_backend', backend=client.test_db)
        assert isinstance(b, MongoBackend)

        assert b.uri == 'mongodb://localhost:27017/test_db'
        assert b.client is client

        uri = 'mongodb://127.0.0.1:27017'
        client = pymongo.MongoClient(uri)
        b = init_backend('local_backend', backend=client.new_db)
        assert isinstance(b, MongoBackend)

        assert b.uri == 'mongodb://127.0.0.1:27017/new_db'
        assert b.client is client

    def test_mongo_no_db_raise(self):
        uri = 'mongodb://localhost:27017'
        with pytest.raises(ValueError):
            init_backend('local_backend', backend=uri)

    def test_maybe_mongo(self):
        import pymongo

        assert maybe_mongo('mongodb://xxx')
        assert not maybe_mongo('xxx://xxx')
        assert not maybe_mongo('http://xxx')
        assert not maybe_mongo(3)
        assert not maybe_mongo('local')

        uri = 'mongodb://localhost:27017/'
        client = pymongo.MongoClient(uri)
        assert maybe_mongo(client.test_db)

    def test_maybe_mongo_raise(self):
        import pymongo
        uri = 'mongodb://localhost:27017/'
        client = pymongo.MongoClient(uri)
        with pytest.raises(ValueError):
            # mongo client
            assert maybe_mongo(client)

        with pytest.raises(ValueError):
            # document collection
            assert maybe_mongo(client.test_db.test_collection)

class TestBackend(object):

    def test_local_backend(self):
        ex = daskperiment.Experiment('local_backend', backend='local')
        assert isinstance(ex._backend, LocalBackend)

        exp = "LocalBackend('daskperiment_cache/local_backend')"
        assert repr(ex._backend) == exp

        ex._delete_cache()

    def test_redis_backend(self):
        backend = 'redis://localhost:6379/0'
        ex = daskperiment.Experiment('redis_backend', backend=backend)
        assert isinstance(ex._backend, RedisBackend)
        assert ex._backend.uri == backend

        exp = "RedisBackend('redis://localhost:6379/0')"
        assert repr(ex._backend) == exp

    def test_mongo_backend(self):
        backend = 'mongodb://localhost:27017/test_db'
        ex = daskperiment.Experiment('mongo_backend', backend=backend)
        assert isinstance(ex._backend, MongoBackend)
        assert ex._backend.uri == 'mongodb://localhost:27017/test_db'

        exp = "MongoBackend('mongodb://localhost:27017/test_db')"
        assert repr(ex._backend) == exp
        assert ex._backend.collection.full_name == 'test_db.mongo_backend'
        assert ex._backend.collection.database.name == 'test_db'
        assert ex._backend.dbname == 'test_db'

    def test_local_backend_eq(self):
        r = init_backend('local_backend', backend='local')
        assert r == r
        r2 = init_backend('local_backend', backend='local')
        assert r2 == r
        assert r == r2

        r2 = init_backend('local_backend2', backend='local')
        assert r2 != r
        assert r != r2

    def test_local_backend_picklable(self):
        r = init_backend('local_backend', backend='local')
        res = pickle.loads(pickle.dumps(r))
        assert r == res
        assert res.cache_dir == r.cache_dir

    def test_redis_backend_picklable(self):
        backend = 'redis://localhost:6379/0'
        r = RedisBackend('redis_experiment', backend)
        res = pickle.loads(pickle.dumps(r))
        assert r == res
        assert res.uri == backend

    def test_redis_pickle_roundtrip(self):
        backend = 'redis://localhost:6379/0'
        r = RedisBackend('redis_experiment', backend)

        obj = dict(a=1, b=2)
        r.save_object('redis_experiment:obj', obj)
        res = r.load_object('redis_experiment:obj')
        assert res == obj

        obj = dict(a=pd.Timestamp('2011-01-01'), b=[1, 2, 4])
        r.save_object('redis_experiment:obj', obj)
        res = r.load_object('redis_experiment:obj')
        assert res == obj


from rest_framework import viewsets

from . import models
from .serializers import GenericDictTableSerializer

def dict_table_viewset_factory(dict_table_model):
    """A class factory for generating viewsets for dicttable based models

    This function acts as a class factory. It generates classes that derive
    from django-rest-framework's ``viewsets.ReadOnlyModelViewSet``.
    It returns a viewset class that can be used to generate API endpoints for
    the various models that are based on ``nfdcore.models.DictionaryTable``.

    The code used here is somewhat obscure, as it is based on meta-programming
    techniques. This approach was chosen in order to reduce the number of
    lines of code that would need to be written in order to generate viewsets
    for all of the existing dicttable models.

    The code itself just calls ``type()`` with the correct arguments in order
    to generate a class at runtime.

    Examples
    --------

    The following example defines the ``ReservationViewSet`` class:

    >>> ReservationViewSet = dict_table_viewset_factory(models.Reservation)

    Without the current function, the equivalent code would be:

    >>> Class ReservationViewSet(viewsets.ReadOnlyModelViewSet):
    ...     queryset = models.Reservation.objects.all()
    ...     serializer_class = GenericDictTableSerializer
    ...
    ...     def get_serializer(self, *args, **kwargs):
    ...         return self.serializer_class(
    ...             model=models.Reservation, *args, **kwargs)

    """

    def get_serializer(self, *args, **kwargs):
        return self.serializer_class(model=dict_table_model, *args, **kwargs)

    cls_name = "{}ViewSet".format(dict_table_model.__name__)
    cls_attrs = {
        "queryset": dict_table_model.objects.all(),
        "serializer_class": GenericDictTableSerializer,
        "get_serializer": get_serializer,
    }

    return type(
        cls_name,
        (viewsets.ReadOnlyModelViewSet,),
        cls_attrs
    )

_MODELS_TO_SERIALIZE = [
    models.AnimalLifestages,
    models.AquaticHabitatCategory,
    models.AquaticSampler,
    models.Aspect,
    models.BedrockAndOutcrops,
    models.CanopyCover,
    models.ChannelType,
    models.CMSensitivity,
    models.CmStatus,  # this is extended
    models.ConiferLifestages,
    models.DayTime,
    models.DiseasesAndAbnormalities,
    models.ElementType,
    models.FernLifestages,
    models.FloweringPlantLifestages,
    models.FungalAssociationType,
    models.FungusApparentSubstrate,
    models.Gender,
    models.GeneralHabitatCategory,
    models.GlacialDeposit,
    models.GlacialDepositPleistoceneAge,
    models.GroundSurface,
    models.GRank,
    models.HmfeiLocalAbundance,
    models.IucnRedListCategory,
    models.LakeMicrohabitat,
    models.LandscapePosition,
    models.LeapLandCover,
    models.LoticHabitatType,
    models.Marks,
    models.MoistureRegime,
    models.MossLifestages,
    models.MushroomGroup,
    models.MushroomGrowthForm,
    models.MushroomOdor,
    models.MushroomVerticalLocation,
    models.NaturalAreaCondition,
    models.NaturalAreaType,
    models.NRank,
    models.OccurrenceCategory,
    models.PlantCount,
    models.PondLakeType,
    models.PondLakeUse,
    models.Preservative,
    models.RecordOrigin,
    models.RecordingStation,
    models.RegionalFrequency,
    models.RegionalStatus,  # this is extended
    models.Repository,
    models.Reservation,
    models.Season,
    models.ShorelineType,
    models.SlimeMoldClass,  # this is extended
    models.SlimeMoldMedia,
    models.Slope,
    models.SRank,
    models.Storage,
    models.StreamDesignatedUse,
    models.TerrestrialSampler,
    models.TerrestrialStratum,
    models.UsfwsStatus,
    models.WaterFlowType,
    models.WaterSource,
    models.Watershed,
    models.WetlandConnectivity,
    models.WetlandHabitatFeature,
    models.WetlandLocation,
    models.WetlandType,
]

for cls_model in _MODELS_TO_SERIALIZE:
    serializer_cls = dict_table_viewset_factory(cls_model)
    # put serializer_cls in module's namespace so it can be easily accessed
    globals()[serializer_cls.__name__] = serializer_cls


from diff_match_patch import diff_match_patch
from django.db.models import (
    AutoField,
    BigAutoField,
    BigIntegerField,
    BinaryField,
    BooleanField,
    CharField,
    DateField,
    DateTimeField,
    DecimalField,
    DurationField,
    EmailField,
    FileField,
    FilePathField,
    FloatField,
    ForeignKey,
    ImageField,
    IntegerField,
    ManyToManyField,
    OneToOneField,
    PositiveIntegerField,
    PositiveSmallIntegerField,
    SmallIntegerField,
    TextField,
    URLField,
    UUIDField,
)

def get_field_diff(old_value, new_value):
    dmp = diff_match_patch()
    diffs = dmp.diff_main(old_value, new_value)
    dmp.diff_cleanupSemantic(diffs)
    return dmp.diff_prettyHtml(diffs)

def format_field_value(field, value):
    if isinstance(field, BooleanField):
        return "No" if not value else "Yes"
    elif isinstance(
        field,
        (
            DecimalField,
            FloatField,
            IntegerField,
            PositiveIntegerField,
            PositiveSmallIntegerField,
            SmallIntegerField,
            BigIntegerField,
            BinaryField,
        ),
    ):
        return str(value)
    elif isinstance(field, DateField):
        return value.strftime("%Y-%m-%d") if value else ""
    elif isinstance(field, (DateTimeField, DurationField)):
        return value.strftime("%Y-%m-%d %H:%M:%S") if value else ""
    elif isinstance(field, (EmailField, URLField, FilePathField, CharField, TextField, UUIDField)):
        return value
    elif isinstance(field, (ImageField, FileField)):
        return value.url if value else ""
    elif isinstance(field, (AutoField, BigAutoField)):
        return str(value)
    elif isinstance(field, (OneToOneField, ForeignKey)):
        return field.related_model.objects.filter(pk=value).first().__str__() if value else ""
    elif isinstance(field, ManyToManyField):
        return list(value.values_list("pk", flat=True)) if value else ""
    else:
        return ""