ese774_frame.api_server のソースコード

#!/usr/bin/env python
"""
FastAPI 用の汎用サーバーラッパー。

- device_cls でデバイス実体を生成
- router_cls で API ルータを生成
- FastAPI の lifespan で初期化/解放を管理

Kengo NAKADA:
https://github.com/shimane-dev, https://github.com/kengo-nakada
kengo.nakada@mat.shimane-u.ac.jp, kengo.nakada@gmail.com
"""

from typing import Any, Dict, Tuple, Optional

# generic_api_server.py

from fastapi import FastAPI
from contextlib import asynccontextmanager



[ドキュメント] class FastApiServer: """ device/router/api_spec を束ねて FastAPI アプリを構築・実行する。 lifecycle: - 起動: device 生成 -> router 生成 -> route 登録 - 停止: device.disconnect があれば呼び出し """ def __init__( self, device_cls, router_cls, config, api_spec=None, device_kwargs=None, logger: Optional[Any] = None, logger_name: str = "FastApiServer", log_level: Optional[str] = None, lifespan_msg_prefix: str = "DEVICE", object_name: Optional[str] = None, dispatch_exclude=None, ): if logger is None: import logging log_level = log_level or "INFO" logging.basicConfig(level=log_level.upper()) logger = logging.getLogger(__name__) self.device_cls = device_cls self.router_cls = router_cls self.config = config self.device_kwargs = device_kwargs or {} self.api_spec = api_spec self.object_name = object_name self.dispatch_exclude = dispatch_exclude # self.logger = logger or XLogger(log_level="debug", logger_name=logger_name) self.logger = logger self.log_level = log_level self.lifespan_msg_prefix = lifespan_msg_prefix # FastAPI 本体(lifespan で初期化/解放) self.app = FastAPI(lifespan=self.lifespan) # 起動時に生成される実体 self._device = None self._router = None
[ドキュメント] @asynccontextmanager async def lifespan( self, app: FastAPI, ): """ FastAPI lifespan ハンドラ。 起動時に device/router を初期化し、終了時に disconnect を実行する。 """ try: # デバイス初期化 self.logger.info(f"{self.lifespan_msg_prefix} 初期化中...") self._device = self.device_cls( **self.device_kwargs, logger=self.logger, ) self.logger.info(f"{self.lifespan_msg_prefix} 初期化完了") except Exception as exc: message = "{} 初期化失敗: {}".format( self.lifespan_msg_prefix, exc, ) raise RuntimeError(message) from None try: # Router 生成 → FastAPI へ登録 # # api_spec is None: # 完全動的モード。 # router_cls=None の場合だけ Framework 標準 DeviceRouter を使用する。 # router_cls が指定されている場合は、その機器固有 Router を維持する。 # 機器固有 Router は DeviceRouter を継承し、特殊な remote semantics # (例: server/client で処理を変える API)のみ override する。 # override されていない API は DeviceRouter の完全動的 dispatch に # フォールバックする。 # # api_spec is not None: # 従来の ApiSpec/機器固有 Router モードをそのまま維持する。 if self.api_spec is None: from ese774_frame.routers.device_router import DeviceRouter dynamic_router_cls = self.router_cls or DeviceRouter # 完全動的モード用の追加引数を受け取れる Router には渡す。 # 旧機器 Router の __init__ が従来シグネチャのままでも動くよう、 # TypeError ではなく signature を見て渡す引数を選別する。 import inspect init_sig = inspect.signature(dynamic_router_cls.__init__) params = init_sig.parameters accepts_varkw = any( p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values() ) router_kwargs = { "logger": self.logger, "log_level": self.log_level, } if accepts_varkw or "object_name" in params: router_kwargs["object_name"] = self.object_name if accepts_varkw or "dispatch_exclude" in params: router_kwargs["dispatch_exclude"] = self.dispatch_exclude self._router = dynamic_router_cls( self._device, None, **router_kwargs, ) # 旧 Router が object_name / dispatch_exclude を __init__ で # 受け取らない場合でも、基底 DeviceRouter の状態へ反映する。 # route 登録後に object_name を変えることはできないため、 # object_name の明示指定だけは旧 Router では利用不可とする。 if self.dispatch_exclude is not None: self._router._dispatch_exclude = set(self.dispatch_exclude) if ( self.object_name is not None and "object_name" not in params and not accepts_varkw and getattr(self._router, "_object_name", None) != self.object_name ): raise TypeError( "dynamic custom router does not accept object_name; " "update its __init__ to forward object_name to DeviceRouter" ) else: if self.router_cls is None: raise ValueError( "router_cls is required when api_spec is not None" ) # 既存 ApiSpec モードは従来の呼び出し形を完全維持する。 self._router = self.router_cls( self._device, self.api_spec, logger=self.logger, log_level=self.log_level, ) app.include_router(self._router.router) # サーバ起動中の寿命区間 yield # サーバー起動中 except KeyboardInterrupt as e: self.logger.error(e) except Exception as e: self.logger.error(e) finally: self.logger.info(f"{self.lifespan_msg_prefix} 解放中...") disconnect = getattr(self._device, "disconnect", None) if callable(disconnect): disconnect()
[ドキュメント] def run( self, host, port, reload=False, ): """ uvicorn で FastAPI サーバを起動する。 """ import multiprocessing import uvicorn # Windows 実行対策 multiprocessing.freeze_support() uvicorn.run( self.app, host=host, port=port, reload=reload, log_config=None, )