Skip to content

插件开发

注:这篇文档假定你有Python异步经验
插件是.py文件的形式,PyFSD启动时自动加载。

Note

此处的插件是指一个.py文件,其中定义的PyFSDPlugin等子类的实例被称为子插件,下文如此。

基本模板

每个插件文件都必须声明以下信息:

plugin_info = {
    "name": "plugin_name",
    "api": 4,
    "version": (1, "0.1.0"),
    "expected_config": {"a": int},
    "initializer": function_that_do_something,
}
name(字符串): 插件名。
api(整数): 与插件搭配的PyFSD的API版本,目前为4。
version(元组): 第一个元素为整数版本号,第二个为字符串版本号。
expected_config(None或字典或TypedDict): 插件预期的配置结构(详见check_dict函数),不需要配置则置None。 initializer(Callable[[], None]): 无参数、无返回值函数,会在导入插件文件后,即将加载前运行。不需要则置None

依赖注入

PyFSD以前将所有东西保存在PyFSDService里,但现在改用依赖注入。 所用可用依赖见依赖容器
详细用法见dependency_injector文档。基本用法如下:

db_engine = Provide[Container.db_engine]

@inject
def function_that_needs_db(dbengine = Provide[Container.db_engine]): ...
插件被PyFSD载入时(插件文件导入(import)后)PyFSD会自动把Provide[相关依赖]替换成相关依赖。

Note

如果有多个装饰器,@inject必须是最底下一个(最靠近函数的一个)

注意!此种写法是无效的:

@inject
def function_that_do_something(db_engine = Provide[Container.db_engine]): ...
    print(db_engine)

function_that_do_something()  # 在导入插件的过程中就运行
<dependency_injector.wiring.Provide object at 0xae9b0f50c152>
依赖注入是在插件文件导入完成后才进行的。在导入插件的过程中就被运行,但db_engine参数仍未被替换成对应的依赖。
对此问题,就可以使用plugin_infoinitializer属性了:
@inject
def after_import(db_engine: AsyncEngine = Provide[Container.db_engine]) -> None:
    print(db_engine)

plugin_info = {
    "name": "plugin_name",
    "api": 4,
    "version": (1, "0.1.0"),
    "expected_config": {"a": int},
    "initializer": after_import,
}
于是after_import会在插件文件导入、检查plugin_info无误之后,在加载子插件之前运行:
<sqlalchemy.ext.asyncio.engine.AsyncEngine object at 0x682669bc19c8>

子插件

现在三种插件形式:
AwaitableMaker: 制作一个可等待对象,并和PyFSD一起运行。
MetarFetcher: Metar源。
PyFSDPlugin: 接受关于PyFSD本体的一些事件,如新用户连接等。
注:子插件必须实例化才能被加载:

class APlugin: ...

a_plugin = APlugin()  # 这很重要!

PyFSDPlugin

通过这种子插件,你可以监听一些事件(所有事件见API)。

from pyfsd.plugin.interfaces import Plugin, PyFSDPlugin

class MyPlugin(Plugin, PyFSDPlugin):
    # 例如line_received_from_client事件
    def line_received_from_client(self, protocol: ClientProtocol, line: bytes) -> None:
        if line.startswith(b"#HI"):
            protocol.send_line(b"#HI:" + byte_line)
            # 在你确保你已经处理完这个事件后,可以抛出PreventEvent异常
            # 来停止传播此事件。
            raise PreventEvent
            # 注意! 目前你只能在line_received_from_client事件处理函数中抛出此异常


# 必须实例化,否则无法加载
plugin = MyPlugin()

MetarFetcher

通过这种子插件,你可以注册一个新的Metar来源。

from asyncio import get_event_loop
from typing import Optional

from aiohttp import ClientSession
from metar.Metar import Metar
from pyfsd.metar.fetch import MetarFetcher, MetarInfoDict
from pyfsd.plugin.interfaces import Plugin


class MetarFetcher(Plugin, MetarFetcher):
    metar_source = "example"  # Metar源名

    # 此处的config参数是配置文件的[pyfsd.metar]部分,下同
    async def fetch(self, config: dict, icao: str) -> Optional[Metar]:
        # 使用异步的aiohttp来下载Metar,不会使PyFSD整体阻塞
        # 详细用法请见aiohttp文档
        async with ClientSession() as session, session.get(url_of_metar) as resp:
            if resp.status != 200:  # 检查HTTP状态码是否是200
                return None
            metar_text = await resp.text("ascii", "ignore")
        if metar_text is None:
            return None
        else:
            return Metar(parser.metar_text, strict=False)

    async def fetch_all(self, config: dict) -> Optional[MetarInfoDict]:
        metar_text = {
            "ZSFZ": "ZSFZ 200300Z 17004MPS 9999 FEW020 33/27 Q1008 NOSIG",
            "ZBAA": "ZBAA 200400Z VRB02MPS CAVOK 34/21 Q1006 NOSIG",
        }  # 假设如此

        # 把解析部分单拎出来作为函数,方便在另外线程中运行
        def parser() -> MetarInfoDict:
            all_metar: MetarInfoDict = {}
            for icao, metar in metar_text.items():
                all_metar[icao] = Metar(metar, strict=False)
            return all_metar

        # fetch与fetch_all默认在主线程运行,所以fetch或fetch_all阻塞会导致PyFSD整个堵塞住
        # 这里使用asyncio的工具函数让parser函数在另一个线程运行,“异步化”
        return await get_event_loop().run_in_executor(None, parser)


# 必须实例化,否则无法加载
fetcher = MetarFetcher()

AwaitableMaker

通过这种子插件,你可以创建一个阻塞的可等待对象并让它和PyFSD一起运行。

from asyncio import sleep
from time import time
from typing import Awaitable, Iterable

from pyfsd.plugin.interfaces import AwaitableMaker, Plugin


class AMaker(Plugin, AwaitableMaker):
    def __call__(self) -> Iterable[Awaitable]:
        async def func() -> None:  # 假设你那个阻塞的服务是这样的。。
            while True:
                print("Working", time())
                await sleep(1)

        yield func()  # 生成可等待对象
        # 接下来的代码会在PyFSD停止后运行,没需要留空也可以
        print("Clean up!")


# 必须实例化,否则无法加载
maker = AMaker()
2024-05-11 09:36:32 [info     ] PyFSD 0.0.1.2.dev0+86.gd72d5ce.dirty [pyfsd.main]
Working 1715391392.5279753
Working 1715391393.5289881
Working 1715391394.5294101
Working 1715391395.530576
2024-05-11 09:36:36 [info     ] Stopping                       [pyfsd.main]
Clean up!

子插件API参考

PreventEvent

PreventEvent(result: Optional[dict] = None)

Bases: BaseException

Prevent a PyFSD plugin event.

Attributes:

  • result (dict) –

    The event result reported by plugin.

Source code in src/pyfsd/plugin/__init__.py
24
25
26
27
28
def __init__(self, result: Optional[dict] = None) -> None:
    """Create a PreventEvent instance."""
    if result is None:
        result = {}
    self.result = result

PyFSDPlugin

Bases: ABC

Interface of PyFSD Plugin.

audit_line_from_client async

audit_line_from_client(protocol: ClientProtocol, line: bytes, result: PyFSDHandledLineResult | PluginHandledEventResult) -> None

Called when line received from client (after lineReceivedFromClient).

Note that this event cannot be prevented.

Parameters:

Source code in src/pyfsd/plugin/interfaces.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
async def audit_line_from_client(
    self,
    protocol: "ClientProtocol",
    line: bytes,
    result: "PyFSDHandledLineResult | PluginHandledEventResult",
) -> None:
    """Called when line received from client (after lineReceivedFromClient).

    Note that this event cannot be prevented.

    Args:
        protocol: Protocol of the connection which received line.
        line: Line data.
        result: The lineReceivedFromClient event result.

    """

before_start async

before_start() -> None

Called before PyFSD start.

Source code in src/pyfsd/plugin/interfaces.py
30
31
async def before_start(self) -> None:
    """Called before PyFSD start."""

before_stop async

before_stop() -> None

Called when PyFSD stopping.

Source code in src/pyfsd/plugin/interfaces.py
33
34
async def before_stop(self) -> None:
    """Called when PyFSD stopping."""

client_disconnected async

client_disconnected(protocol: ClientProtocol, client: Optional[Client]) -> None

Called when connection disconnected.

Parameters:

  • protocol (ClientProtocol) –

    The protocol of the connection which disconnected.

  • client (Optional[Client]) –

    The client attribute of the protocol.

Source code in src/pyfsd/plugin/interfaces.py
82
83
84
85
86
87
88
89
90
91
92
async def client_disconnected(
    self,
    protocol: "ClientProtocol",
    client: Optional["Client"],
) -> None:
    """Called when connection disconnected.

    Args:
        protocol: The protocol of the connection which disconnected.
        client: The client attribute of the protocol.
    """

line_received_from_client async

line_received_from_client(protocol: ClientProtocol, line: bytes) -> None

Called when line received from client.

Parameters:

  • protocol (ClientProtocol) –

    Protocol of the connection which received line.

  • line (bytes) –

    Line data.

Raises:

Source code in src/pyfsd/plugin/interfaces.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
async def line_received_from_client(
    self,
    protocol: "ClientProtocol",
    line: bytes,
) -> None:
    """Called when line received from client.

    Args:
        protocol: Protocol of the connection which received line.
        line: Line data.

    Raises:
        PreventEvent: Prevent the event.
    """

new_client_created async

new_client_created(protocol: ClientProtocol) -> None

Called when new client pyfsd.object.client.Client created.

Parameters:

Source code in src/pyfsd/plugin/interfaces.py
43
44
45
46
47
48
async def new_client_created(self, protocol: "ClientProtocol") -> None:
    """Called when new client `pyfsd.object.client.Client` created.

    Args:
        protocol: Protocol of the client which created.
    """

new_connection_established async

new_connection_established(protocol: ClientProtocol) -> None

Called when new connection established.

Parameters:

  • protocol (ClientProtocol) –

    Protocol of the connection which established.

Source code in src/pyfsd/plugin/interfaces.py
36
37
38
39
40
41
async def new_connection_established(self, protocol: "ClientProtocol") -> None:
    """Called when new connection established.

    Args:
        protocol: Protocol of the connection which established.
    """

AwaitableMaker

Bases: ABC

Interface of Awaitable maker, a object which can make a awaitable object.

Used to await a blocking awaitable object when PyFSD starts.

__call__ abstractmethod

__call__() -> Generator[Optional[Awaitable], None, None]

Make a awaitable object.

Yields:

  • Optional[Awaitable]

    First time yield the awaitable object, the next time do clean up.

  • Optional[Awaitable]

    If nothing needs to be awaited, yield None first time. (code block after yield still executes.)

  • Example ( Optional[Awaitable] ) –

    : server = Server() server.prepare() yield server.run() server.clean()

Returns:

Source code in src/pyfsd/plugin/interfaces.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@abstractmethod
def __call__(self) -> Generator[Optional[Awaitable], None, None]:
    """Make a awaitable object.

    Yields:
        First time yield the awaitable object, the next time do clean up.
        If nothing needs to be awaited, yield None first time. (code block after
            yield still executes.)
        Example::
            server = Server()
            server.prepare()
            yield server.run()
            server.clean()

    Returns:
        A awaitable object.
    """

MetarFetcher

Bases: ABC

Metar fetcher.

Attributes:

  • metar_source (str) –

    Name of the METAR source.

fetch abstractmethod async

fetch(config: Union[dict, PyFSDMetarConfig], icao: str) -> Optional[Metar]

Fetch the METAR of the specified airport.

Parameters:

Returns:

  • Optional[Metar]

    The METAR of the specified airport. None if fetch failed.

Raises:

Source code in src/pyfsd/metar/fetch.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@abstractmethod
async def fetch(
    self, config: Union[dict, "PyFSDMetarConfig"], icao: str
) -> Optional[Metar]:
    """Fetch the METAR of the specified airport.

    Args:
        config: pyfsd.metar section of PyFSD configure file.
        icao: The ICAO of the airport.

    Returns:
        The METAR of the specified airport. None if fetch failed.

    Raises:
        NotImplemented: When fetch a single airport isn't supported.
    """

fetch_all abstractmethod async

fetch_all(config: Union[dict, PyFSDMetarConfig]) -> Optional[MetarInfoDict]

Fetch METAR for all airports.

Parameters:

Returns:

  • Optional[MetarInfoDict]

    All METAR. None if fetch failed.

Raises:

Source code in src/pyfsd/metar/fetch.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@abstractmethod
async def fetch_all(
    self, config: Union[dict, "PyFSDMetarConfig"]
) -> Optional[MetarInfoDict]:
    """Fetch METAR for all airports.

    Args:
        config: pyfsd.metar section of PyFSD configure file.

    Returns:
        All METAR. None if fetch failed.

    Raises:
        NotImplemented: When fetch all isn't supported.
    """