实施FastAPI服务–关注点的抽象和分离

2021-01-16 06:35:47

本文介绍了一种在考虑多个服务的情况下构造FastAPI应用程序的方法。所提出的结构遵循抽象和关注点分离的原则将各个服务分解为包和模块。下面讨论的代码也可以在专用的GitHub存储库中进行整体研究。

FastAPI是一个基于Starlette的快速,高度直观且文档齐全的API框架。尽管相对较新,但它在开发人员社区中得到了广泛的采用,甚至在Netflix和Microsoft等公司的生产中也已采用。

按照UNIX的原则,“做一件事情,做好一件事情”,根据应用程序的任务分离它们的各个部分,可以提高代码的可读性和可维护性。最终降低了复杂性。以这种方式构造应用程序的主要好处是:

关注点分离–将应用程序分解为执行一项工作的模块。这允许接受请求(自上而下:控制器→服务→数据访问)并返回响应(自下而上:数据访问→服务→控制器),并明确区分应在哪个特定模块中实现哪些特定功能,从而减轻开发的认知负担。

抽象—应用程序的组件以可重用的方式设计。例如,ServiceResult被实现为服务操作(可以成功并返回响应,或者不成功并引发异常)的通用结果,可被应用程序的所有服务使用,并保持代码DRY。

名称空间的粒度性质允许区分应用程序的各个部分,例如,路由与属于特定服务的业务逻辑–将相似的任务分组在一起,同时使不同的部分保持分离。需要四个主要软件包。对于每种服务,将一个模块添加到这四个软件包中。例如,名为" Foo"的服务需要以下模块(下面详细讨论):

./routers/ foo.py#路由器实例和路由./services/ foo.py#业务逻辑(包括CRUD帮助器)./schemas/ foo.py#数据" schemas" (例如Pydantic模型)./models/ foo.py#数据库模型(例如SQLAlchemy模型)

四个主要程序包由两个通用程序包补充,它们包含特定于应用程序(而非服务特定)的功能,例如配置或实用程序功能。

在main内,实例化该应用程序,并且包括所有路由器。另外,实现了中间件和/或异常处理程序。下面讨论的示例应用程序基于名为Foo的服务,该服务需要许多路由。为了处理在服务层发生的自定义异常,作为AppExceptionCase类的实例,会将相应的异常处理程序添加到应用程序。

1#... 2从fastapi导入FastAPI 3从路由器导入foo 4 app = FastAPI()5 @ app.exception_handler(AppExceptionCase)6异步def custom_app_exception_handler(request,e):7 return await app_exception_handler(request,e)8 app .include_router(foo.router)

路由器及其路由在路由器包内的模块中定义。每个路由实例化各自的服务,并从请求依赖项传递数据库会话。通过handle_result()处理,返回服务结果(成功操作的结果所请求的数据,或异常)。如果发生异常,则main中的应用程序异常处理程序将代替处理返回异常,而不是(之前)返回任何响应。

1路由器= APIRouter(前缀=" / foo")2 @ router.post(" / item /",response_model = FooItem)3异步def create_item(item:FooItemCreate,db: get_db = Depends()):4结果= FooService(db).create_item(item)5返回handle_result(result)6 @ router.get(" / item / {item_id}",response_model = FooItem) 7异步定义get_item(item_id:int,db:get_db = Depends()):8结果= FooService(db).get_item(item_id)9 return handle_result(结果)

ServiceResult类定义服务操作的一般结果。如果操作成功,则返回包含在实例的value属性内的结果(或value)。如果发生自定义应用例外,则服务结果实例将包含有关引发的例外的信息(例如,应将哪种状态代码返回给客户端)。

1类ServiceResult:2 def __init__(self,arg):3 if isinstance(arg,AppExceptionCase):4 self .success = False 5 self .exception_case = arg.exception_case 6 self .status_code = arg.status_code 7 else:8 self。成功=真9 self .exception_case =无10 self .status_code =无11 self .value = arg 12 def __str__(self):如果self .success为14,则返回14:" [成功] 15 return f' [Exception]" {self .exception_case}"' 16 def __repr__(self):17如果self。成功:18 return"< ServiceResult Success>" 19返回f"< ServiceResult AppException {self .exception_case}>" 20 def __enter__(self):21 return self .value 22 def __exit__(self,* kwargs):23 pass 24 def handle_result(结果:ServiceResult)-> :25(如果未返回结果)成功:26(结果为异常):27引发异常28(结果为结果):29返回结果

服务在服务包中定义。每个服务都是AppService的子类。数据库会话是通过“类似于接口的接口”从请求依赖项传递下来的。 mixin实用程序类(可以通过多重继承添加其他混合类,以扩展可用属性)。

属于服务Foo的路由连接到FooService的方法,该方法封装了服务的所有业务逻辑。返回值的类型为ServiceResult:包含带有可返回数据的value属性,或者在发生异常的情况下包含AppException。在这两种情况下,各自的结果都返回给& 34; upwards"。到控制器层。

1类FooService(AppService):2 def create_item(self,项目:FooItemCreate)-> ServiceResult:3 foo_item = FooCRUD(self .db).create_item(item)4如果不是foo_item:5 return ServiceResult(AppException.FooCreateItem())6 return ServiceResult(foo_item)7 def get_item(self,item_id:int)-> ServiceResult:8 foo_item = FooCRUD(self .db).get_item(item_id)9如果不是foo_item:10 return ServiceResult(AppException.FooGetItem({" item_id":item_id}))11如果不是foo_item.public: 12返回ServiceResult(AppException.FooItemRequiresAuth())13返回ServiceResult(foo_item)

CRUD辅助方法在数据库上执行操作,并且是AppCRUD的子类。数据库会话从AppService实例向下传递。这些方法是原子性的,仅与在数据库上操作有关。它们不包含任何业务逻辑。

1类FooCRUD(AppCRUD):2 def create_item(self,item:FooItemCreate)-> FooItem:3 foo_item = FooItem(description = item.description,public = item.public)4 self .db.add(foo_item)5 self .db.commit()6 self .db.refresh(foo_item)7 return foo_item 8 def get_item(self,item_id:int)-> FooItem:9 foo_item = self .db.query(FooItem).filter(FooItem.id == item_id).first()10 if foo_item:11返回foo_item 12返回None

金字塔式"或模型是在模式包中定义的。它们主要包含两种不同的数据模型。首先,来自客户端的期望值是请求数据(路由方法定义中的路由参数)。其次,那些期望作为响应数据返回给客户端的路由(在路由定义的response_model参数中定义)。

此外,可以对在应用程序各层之间进行传递的任何类型的数据(控制器,服务和数据访问)进行建模。

1类FooItemBase(BaseModel):2描述:str 3类FooItemCreate(FooItemBase):4公共类:bool 5类FooItem(FooItemBase):6 id:int 7类配置:8 orm_mode = True

SQLAlchemy模型在models包中定义。它们定义了如何在关系数据库中存储数据。它们是从AppCRUD引用的。如果需要,请确保通过适当的导入命名空间来区分FooItem模型(SQLAlchemy)和FooItem模式(Pydantic)。

1类FooItem(Base):2 __tablename__ =" foo_items" 3 id =列(Integer,primary_key = True,索引= True)4描述=列(String)5 public =列(Boolean,默认= False)

应用程序异常在utils包中实现。首先,AppExceptionCase从基本Exception继承而来,并包含用于定义自定义应用程序异常方案的各种属性。定义用于处理自定义应用程序异常的任务的异常处理程序(添加到main中,请参见上文),其定义为包含有关应用程序异常信息的响应。

1类AppExceptionCase(Exception):2 def __init__(self,status_code:int,context:dict):3 self .exception_case = self。 __class__。 __name__ 4 self .status_code = status_code 5 self .context = context 6 def __str__(self):7 return(8 f"< AppException {self .exception_case}-" 9 + f" status_code = {self.status_code}-context = {self .context}>" 10)11 async def app_exception_handler(request:Request,exc:AppExceptionCase):12 return JSONResponse(13 status_code = exc.status_code,14 content = {15" app_exception":exc.exception_case,16" context":exc.context,17},18)

其次,定义和记录自定义异常情况发生在同一模块中,并且需要AppExceptionCase的子类化。每个应用程序异常都在文档字符串中包含一个描述,并定义了要返回给客户端的状态代码。

报告类名以通知客户端特定的异常情况-请参阅AppExceptionCase初始化和应用程序异常处理程序中的JSONResponse。

1类AppException:2类FooCreateItem(AppExceptionCase):3 def __init__(self,context:dict = None):4""" 5项目创建失败6""" 7 status_code = 500 8 AppExceptionCase。 __init__(self,status_code,context)9类FooGetItem(AppExceptionCase):10 def __init__(self,context:dict = None):11"""找不到12个项目13""" 14 status_code = 404 15 AppExceptionCase。 __init__(self,status_code,context)16类FooItemRequiresAuth(AppExceptionCase):17 def __init__(self,context:dict = None):18""" 19项不是公开的,需要auth 20""" 21 status_code = 401 22 AppExceptionCase。 __init__(self,status_code,context)

如有必要,可以向异常AppException.FooCreateItem({" id&#34 ;: 123})添加上下文,以便将异常的上下文通知客户端。

要编译所有异常的列表(例如,为了本地化显示给客户端的应用程序异常消息),请访问AppException范围内的所有属性:

从utils.app_exceptions中导入1导入AppException 2打印([&e在dir中为e(如果" __"不在e中,则为AppException))3#[' FooCreateItem',' FooGetItem& #39;,' FooItemRequiresAuth']

所提出的方法允许设计FastAPI应用程序,该应用程序具有高度结构化以容纳多个服务,从而允许以统一模式实施任意数量的服务。

希望您喜欢这篇文章!我很想听听您对这种拟议结构的想法,并希望从替代方法中学习-可以通过在本文随附的存储库中在GitHub上打开一个问题来开始讨论,或者直接与我联系。

在我写新帖子时,在下面输入您的电子邮件地址以接收电子邮件。