zrb 1.0.0a21__py3-none-any.whl → 1.0.0b5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. zrb/__init__.py +2 -1
  2. zrb/__main__.py +3 -3
  3. zrb/builtin/__init__.py +3 -0
  4. zrb/builtin/group.py +1 -0
  5. zrb/builtin/llm/llm_chat.py +87 -7
  6. zrb/builtin/llm/previous-session.js +21 -0
  7. zrb/builtin/llm/tool/api.py +29 -0
  8. zrb/builtin/llm/tool/cli.py +1 -1
  9. zrb/builtin/llm/tool/rag.py +108 -145
  10. zrb/builtin/llm/tool/web.py +1 -1
  11. zrb/builtin/project/add/fastapp/fastapp_task.py +2 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -2
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +80 -20
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +150 -42
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service.py +113 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service_factory.py +9 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/repository/my_entity_db_repository.py +0 -10
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/repository/my_entity_repository.py +37 -16
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/repository/{factory.py → my_entity_repository_factory.py} +2 -2
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +16 -6
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/client_method.py +57 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +74 -0
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/format_task.py +1 -1
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +13 -0
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +23 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -0
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +7 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/my_module_api_client.py +6 -0
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/{any_client.py → my_module_client.py} +1 -1
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/my_module_client_factory.py +11 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/my_module_direct_client.py +5 -0
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/route.py +11 -11
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +2 -2
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +8 -8
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +47 -20
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app_factory.py +29 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +230 -102
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +236 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/{db_engine.py → db_engine_factory.py} +1 -1
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +12 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/logger_factory.py +10 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/parser_factory.py +7 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/app.py +47 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +105 -0
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/user_agent.py +58 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +37 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +37 -1
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/main.py +1 -1
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_api_client.py +16 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +169 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client_factory.py +9 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_direct_client.py +15 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +160 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +18 -1
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +7 -3
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +117 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service_factory.py +11 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_db_repository.py +26 -0
  59. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_repository.py +61 -0
  60. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_repository_factory.py +13 -0
  61. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +89 -0
  62. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +67 -0
  63. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository_factory.py +13 -0
  64. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +137 -0
  65. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service_factory.py +7 -0
  66. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +179 -12
  67. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +67 -17
  68. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository_factory.py +2 -2
  69. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +127 -0
  70. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +7 -0
  71. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +43 -14
  72. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +200 -30
  73. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +74 -0
  74. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +6 -0
  75. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/homepage.html +6 -0
  76. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-192x192.png +0 -0
  77. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-512x512.png +0 -0
  78. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/favicon-32x32.png +0 -0
  79. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.amber.min.css +4 -0
  80. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.blue.min.css +4 -0
  81. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.cyan.min.css +4 -0
  82. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.fuchsia.min.css +4 -0
  83. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.green.min.css +4 -0
  84. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.grey.min.css +4 -0
  85. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.indigo.min.css +4 -0
  86. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.jade.min.css +4 -0
  87. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.lime.min.css +4 -0
  88. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.min.css +4 -0
  89. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.orange.min.css +4 -0
  90. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pink.min.css +4 -0
  91. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pumpkin.min.css +4 -0
  92. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.purple.min.css +4 -0
  93. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.red.min.css +4 -0
  94. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.sand.min.css +4 -0
  95. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.slate.min.css +4 -0
  96. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.violet.min.css +4 -0
  97. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.yellow.min.css +4 -0
  98. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.zinc.min.css +4 -0
  99. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +34 -0
  100. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -0
  101. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +17 -5
  102. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +78 -4
  103. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +48 -0
  104. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +69 -5
  105. zrb/builtin/python.py +1 -1
  106. zrb/builtin/random.py +61 -0
  107. zrb/builtin/todo.py +1 -0
  108. zrb/cmd/cmd_val.py +6 -5
  109. zrb/config.py +15 -4
  110. zrb/content_transformer/any_content_transformer.py +7 -0
  111. zrb/content_transformer/content_transformer.py +6 -0
  112. zrb/input/any_input.py +5 -0
  113. zrb/input/base_input.py +6 -0
  114. zrb/input/bool_input.py +2 -0
  115. zrb/input/float_input.py +2 -0
  116. zrb/input/int_input.py +2 -0
  117. zrb/input/option_input.py +2 -0
  118. zrb/input/password_input.py +2 -0
  119. zrb/input/text_input.py +2 -0
  120. zrb/runner/cli.py +14 -7
  121. zrb/runner/common_util.py +1 -1
  122. zrb/runner/web_app.py +28 -280
  123. zrb/runner/web_config/config.py +91 -0
  124. zrb/runner/web_config/config_factory.py +26 -0
  125. zrb/runner/web_route/docs_route.py +17 -0
  126. zrb/runner/web_route/error_page/serve_default_404.py +28 -0
  127. zrb/runner/{web_controller/error_page/controller.py → web_route/error_page/show_error_page.py} +4 -3
  128. zrb/runner/{web_controller → web_route}/error_page/view.html +5 -0
  129. zrb/runner/web_route/home_page/home_page_route.py +51 -0
  130. zrb/runner/web_route/login_api_route.py +31 -0
  131. zrb/runner/web_route/login_page/login_page_route.py +39 -0
  132. zrb/runner/web_route/logout_api_route.py +18 -0
  133. zrb/runner/web_route/logout_page/logout_page_route.py +40 -0
  134. zrb/runner/{web_controller/group_info_page/controller.py → web_route/node_page/group/show_group_page.py} +3 -3
  135. zrb/runner/web_route/node_page/node_page_route.py +50 -0
  136. zrb/runner/{web_controller/session_page/controller.py → web_route/node_page/task/show_task_page.py} +3 -3
  137. zrb/runner/{web_controller/session_page → web_route/node_page/task}/view.html +1 -1
  138. zrb/runner/web_route/refresh_token_api_route.py +38 -0
  139. zrb/runner/{web_controller/static → web_route/static/resources}/session/current-session.js +12 -6
  140. zrb/runner/{web_controller/static → web_route/static/resources}/session/event.js +17 -2
  141. zrb/runner/web_route/static/static_route.py +44 -0
  142. zrb/runner/web_route/task_input_api_route.py +47 -0
  143. zrb/runner/web_route/task_session_api_route.py +147 -0
  144. zrb/runner/web_schema/session.py +5 -0
  145. zrb/runner/web_schema/token.py +11 -0
  146. zrb/runner/web_schema/user.py +32 -0
  147. zrb/runner/web_util/cookie.py +29 -0
  148. zrb/runner/{web_util.py → web_util/html.py} +1 -23
  149. zrb/runner/web_util/token.py +72 -0
  150. zrb/runner/web_util/user.py +63 -0
  151. zrb/session/session.py +6 -4
  152. zrb/session_state_logger/{default_session_state_logger.py → session_state_logger_factory.py} +1 -1
  153. zrb/task/base_task.py +56 -8
  154. zrb/task/base_trigger.py +2 -0
  155. zrb/task/cmd_task.py +9 -5
  156. zrb/task/http_check.py +2 -0
  157. zrb/task/llm_task.py +93 -110
  158. zrb/task/make_task.py +2 -0
  159. zrb/task/rsync_task.py +2 -0
  160. zrb/task/scaffolder.py +8 -5
  161. zrb/task/scheduler.py +2 -0
  162. zrb/task/tcp_check.py +2 -0
  163. zrb/task_status/task_status.py +4 -3
  164. zrb/util/cmd/command.py +1 -0
  165. zrb/util/file.py +7 -1
  166. zrb/util/llm/tool.py +36 -12
  167. {zrb-1.0.0a21.dist-info → zrb-1.0.0b5.dist-info}/METADATA +3 -2
  168. zrb-1.0.0b5.dist-info/RECORD +309 -0
  169. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/any_client_method.py +0 -27
  170. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_usecase.py +0 -65
  171. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/api_client.py +0 -6
  172. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/direct_client.py +0 -6
  173. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/factory.py +0 -9
  174. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app.py +0 -20
  175. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_usecase.py +0 -245
  176. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/any_client.py +0 -33
  177. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/api_client.py +0 -7
  178. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/direct_client.py +0 -6
  179. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/factory.py +0 -9
  180. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_user_table.py +0 -37
  181. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase.py +0 -53
  182. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +0 -6
  183. zrb/runner/web_config.py +0 -274
  184. zrb/runner/web_controller/home_page/controller.py +0 -33
  185. zrb/runner/web_controller/login_page/controller.py +0 -25
  186. zrb/runner/web_controller/logout_page/controller.py +0 -26
  187. zrb-1.0.0a21.dist-info/RECORD +0 -244
  188. /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/{create_column_task.py → add_column_task.py} +0 -0
  189. /zrb/{runner/web_controller → builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission}/__init__.py +0 -0
  190. /zrb/{runner/web_controller/group_info_page → builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role}/__init__.py +0 -0
  191. /zrb/runner/{web_controller/home_page → web_route}/__init__.py +0 -0
  192. /zrb/runner/{web_controller/session_page → web_route/home_page}/__init__.py +0 -0
  193. /zrb/runner/{web_controller → web_route}/home_page/view.html +0 -0
  194. /zrb/runner/{web_controller → web_route}/login_page/view.html +0 -0
  195. /zrb/runner/{web_controller → web_route}/logout_page/view.html +0 -0
  196. /zrb/runner/{web_controller/group_info_page → web_route/node_page/group}/view.html +0 -0
  197. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/partial/input.html +0 -0
  198. /zrb/runner/{refresh-token.template.js → web_route/static/refresh-token.template.js} +0 -0
  199. /zrb/runner/{web_controller/static → web_route/static/resources}/common.css +0 -0
  200. /zrb/runner/{web_controller/static → web_route/static/resources}/favicon-32x32.png +0 -0
  201. /zrb/runner/{web_controller/static → web_route/static/resources}/login/event.js +0 -0
  202. /zrb/runner/{web_controller/static → web_route/static/resources}/logout/event.js +0 -0
  203. /zrb/runner/{web_controller/static → web_route/static/resources}/pico.min.css +0 -0
  204. /zrb/runner/{web_controller/static → web_route/static/resources}/session/common-util.js +0 -0
  205. /zrb/runner/{web_controller/static → web_route/static/resources}/session/past-session.js +0 -0
  206. {zrb-1.0.0a21.dist-info → zrb-1.0.0b5.dist-info}/WHEEL +0 -0
  207. {zrb-1.0.0a21.dist-info → zrb-1.0.0b5.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,22 @@
1
+ import datetime
2
+ from contextlib import asynccontextmanager
1
3
  from typing import Any, Callable, Generic, Type, TypeVar
2
4
 
3
- from my_app_name.common.error import NotFoundError
4
- from sqlalchemy import Engine
5
+ import ulid
6
+ from my_app_name.common.error import InvalidValueError, NotFoundError
7
+ from my_app_name.common.parser_factory import (
8
+ parse_filter_param as default_parse_filter_param,
9
+ )
10
+ from my_app_name.common.parser_factory import (
11
+ parse_sort_param as default_parse_sort_param,
12
+ )
13
+ from sqlalchemy import Engine, delete, func, insert, select, update
5
14
  from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
6
- from sqlmodel import Session, SQLModel, select
15
+ from sqlalchemy.sql import ClauseElement, ColumnElement, Select
16
+ from sqlmodel import Session, SQLModel
7
17
 
8
18
  DBModel = TypeVar("DBModel", bound=SQLModel)
9
- ResponseModel = TypeVar("Model", bound=SQLModel)
19
+ ResponseModel = TypeVar("ResponseModel", bound=SQLModel)
10
20
  CreateModel = TypeVar("CreateModel", bound=SQLModel)
11
21
  UpdateModel = TypeVar("UpdateModel", bound=SQLModel)
12
22
 
@@ -19,116 +29,234 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
19
29
  entity_name: str = "entity"
20
30
  column_preprocessors: dict[str, Callable[[Any], Any]] = {}
21
31
 
22
- def __init__(self, engine: Engine | AsyncEngine):
32
+ def __init__(
33
+ self,
34
+ engine: Engine | AsyncEngine,
35
+ filter_param_parser: (
36
+ Callable[[SQLModel, str], list[ClauseElement]] | None
37
+ ) = None,
38
+ sort_param_parser: Callable[[SQLModel, str], list[ColumnElement]] | None = None,
39
+ ):
23
40
  self.engine = engine
24
- self.is_async = isinstance(engine, AsyncEngine)
41
+ self._is_async = isinstance(engine, AsyncEngine)
42
+ self._parse_filter_param = (
43
+ filter_param_parser if filter_param_parser else default_parse_filter_param
44
+ )
45
+ self._parse_sort_param = (
46
+ sort_param_parser if sort_param_parser else default_parse_sort_param
47
+ )
25
48
 
26
- def _to_response(self, db_instance: DBModel) -> ResponseModel:
27
- return self.response_model(**db_instance.model_dump())
49
+ @property
50
+ def is_async(self) -> bool:
51
+ return self._is_async
28
52
 
29
- async def create(self, data: CreateModel) -> ResponseModel:
53
+ def _select(self) -> Select:
54
+ """
55
+ This method is used to contruct select statement for get, get_by_id and get_by_ids.
56
+ To parse the result of the statement, make sure you override _rows_to_response as well.
57
+ """
58
+ return select(self.db_model)
59
+
60
+ def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[ResponseModel]:
61
+ """
62
+ This method is used to parse the result of select statement generated by _select.
63
+ """
64
+ return [self.response_model.model_validate(row[0]) for row in rows]
65
+
66
+ async def _select_to_response(
67
+ self, query_modifier: Callable[[Select], Any]
68
+ ) -> list[ResponseModel]:
69
+ statement = query_modifier(self._select())
70
+ async with self._session_scope() as session:
71
+ result = await self._execute_statement(session, statement)
72
+ return self._rows_to_responses(result.all())
73
+
74
+ def _ensure_one(self, responses: list[ResponseModel]) -> ResponseModel:
75
+ if not responses:
76
+ raise NotFoundError(f"{self.entity_name} not found")
77
+ if len(responses) > 1:
78
+ raise InvalidValueError(f"Duplicate {self.entity_name}")
79
+ return responses[0]
80
+
81
+ def _model_to_data_dict(
82
+ self, data: SQLModel, **additional_data: Any
83
+ ) -> dict[str, Any]:
84
+ """
85
+ This method transform SQLModel into dictionary for insert/update operation.
86
+ """
30
87
  data_dict = data.model_dump(exclude_unset=True)
88
+ data_dict.update(additional_data)
31
89
  for key, preprocessor in self.column_preprocessors.items():
32
- if key in data_dict:
33
- data_dict[key] = preprocessor(data_dict[key])
34
- db_instance = self.db_model(**data_dict)
35
- if self.is_async:
36
- async with AsyncSession(self.engine) as session:
37
- session.add(db_instance)
38
- await session.commit()
39
- await session.refresh(db_instance)
40
- else:
41
- with Session(self.engine) as session:
42
- session.add(db_instance)
43
- session.commit()
44
- session.refresh(db_instance)
45
- return self._to_response(db_instance)
90
+ if key not in data_dict:
91
+ continue
92
+ if not hasattr(self.db_model, key):
93
+ raise InvalidValueError(f"Invalid {self.entity_name} property: {key}")
94
+ data_dict[key] = preprocessor(data_dict[key])
95
+ return data_dict
46
96
 
47
- async def get_by_id(self, item_id: str) -> ResponseModel:
97
+ @asynccontextmanager
98
+ async def _session_scope(self):
48
99
  if self.is_async:
49
100
  async with AsyncSession(self.engine) as session:
50
- db_instance = await session.get(self.db_model, item_id)
101
+ async with session.begin():
102
+ yield session
51
103
  else:
52
104
  with Session(self.engine) as session:
53
- db_instance = session.get(self.db_model, item_id)
54
- if not db_instance:
55
- raise NotFoundError(f"{self.entity_name} not found")
56
- return self._to_response(db_instance)
105
+ with session.begin():
106
+ yield session
57
107
 
58
- async def get_all(self, page: int = 1, page_size: int = 10) -> list[ResponseModel]:
59
- offset = (page - 1) * page_size
60
- statement = select(self.db_model).offset(offset).limit(page_size)
108
+ async def _commit(self, session: Session | AsyncSession):
61
109
  if self.is_async:
62
- async with AsyncSession(self.engine) as session:
63
- result = await session.execute(statement)
64
- results = result.scalars().all()
110
+ await session.commit()
65
111
  else:
66
- with Session(self.engine) as session:
67
- results = session.exec(statement).all()
68
- return [self._to_response(instance) for instance in results]
69
-
70
- async def update(self, item_id: str, data: UpdateModel) -> ResponseModel:
71
- update_data = data.model_dump(exclude_unset=True)
72
- for key, value in update_data.items():
73
- if key in self.column_preprocessors:
74
- update_data[key] = self.column_preprocessors[key](value)
75
- if self.is_async:
76
- async with AsyncSession(self.engine) as session:
77
- db_instance = await session.get(self.db_model, item_id)
78
- if not db_instance:
79
- raise NotFoundError(f"{self.entity_name} not found")
80
- for key, value in update_data.items():
81
- setattr(db_instance, key, value)
82
- session.add(db_instance)
83
- await session.commit()
84
- await session.refresh(db_instance)
85
- else:
86
- with Session(self.engine) as session:
87
- db_instance = session.get(self.db_model, item_id)
88
- if not db_instance:
89
- raise NotFoundError(f"{self.entity_name} not found")
90
- for key, value in update_data.items():
91
- setattr(db_instance, key, value)
92
- session.add(db_instance)
93
- session.commit()
94
- session.refresh(db_instance)
95
- return self._to_response(db_instance)
96
-
97
- async def delete(self, item_id: str) -> ResponseModel:
98
- if self.is_async:
99
- async with AsyncSession(self.engine) as session:
100
- db_instance = await session.get(self.db_model, item_id)
101
- if not db_instance:
102
- raise NotFoundError(f"{self.entity_name} not found")
103
- await session.delete(db_instance)
104
- await session.commit()
105
- else:
106
- with Session(self.engine) as session:
107
- db_instance = session.get(self.db_model, item_id)
108
- if not db_instance:
109
- raise NotFoundError(f"{self.entity_name} not found")
110
- session.delete(db_instance)
111
- session.commit()
112
- return self._to_response(db_instance)
113
-
114
- async def create_bulk(self, data_list: list[CreateModel]) -> list[ResponseModel]:
115
- db_instances = []
116
- for data in data_list:
117
- data_dict = data.model_dump(exclude_unset=True)
118
- for key, preprocessor in self.column_preprocessors.items():
119
- if key in data_dict:
120
- data_dict[key] = preprocessor(data_dict[key])
121
- db_instances.append(self.db_model(**data_dict))
112
+ session.commit()
113
+
114
+ async def _execute_statement(self, session, statement: Any) -> Any:
122
115
  if self.is_async:
123
- async with AsyncSession(self.engine) as session:
124
- session.add_all(db_instances)
125
- await session.commit()
126
- for instance in db_instances:
127
- await session.refresh(instance)
116
+ return await session.execute(statement)
128
117
  else:
129
- with Session(self.engine) as session:
130
- session.add_all(db_instances)
131
- session.commit()
132
- for instance in db_instances:
133
- session.refresh(instance)
134
- return [self._to_response(instance) for instance in db_instances]
118
+ return session.execute(statement)
119
+
120
+ async def get_by_id(self, id: str) -> ResponseModel:
121
+ rows = await self._select_to_response(lambda q: q.where(self.db_model.id == id))
122
+ return self._ensure_one(rows)
123
+
124
+ async def get_by_ids(self, id_list: list[str]) -> list[ResponseModel]:
125
+ return await self._select_to_response(
126
+ lambda q: q.where(self.db_model.id.in_(id_list))
127
+ )
128
+
129
+ async def count(self, filter: str | None = None) -> int:
130
+ count_statement = select(func.count(1)).select_from(self.db_model)
131
+ if filter:
132
+ filter_param = self._parse_filter_param(self.db_model, filter)
133
+ count_statement = count_statement.where(*filter_param)
134
+ async with self._session_scope() as session:
135
+ result = await self._execute_statement(session, count_statement)
136
+ return result.scalar_one()
137
+
138
+ async def get(
139
+ self,
140
+ page: int = 1,
141
+ page_size: int = 10,
142
+ filter: str | None = None,
143
+ sort: str | None = None,
144
+ ) -> list[ResponseModel]:
145
+ return await self._select_to_response(
146
+ self._get_pagination_query_modifier(
147
+ page=page, page_size=page_size, filter=filter, sort=sort
148
+ )
149
+ )
150
+
151
+ def _get_pagination_query_modifier(
152
+ self,
153
+ page: int = 1,
154
+ page_size: int = 10,
155
+ filter: str | None = None,
156
+ sort: str | None = None,
157
+ ) -> Callable[[Select], Any]:
158
+ def pagination_query_modifier(statement: Select) -> Any:
159
+ offset = (page - 1) * page_size
160
+ statement = statement.offset(offset).limit(page_size)
161
+ if filter:
162
+ filter_param = self._parse_filter_param(self.db_model, filter)
163
+ statement = statement.where(*filter_param)
164
+ if sort:
165
+ sort_param = self._parse_sort_param(self.db_model, sort)
166
+ statement = statement.order_by(*sort_param)
167
+ return statement
168
+
169
+ return pagination_query_modifier
170
+
171
+ async def create(self, data: CreateModel) -> DBModel:
172
+ now = datetime.datetime.now(datetime.timezone.utc)
173
+ data_dict = self._model_to_data_dict(data, created_at=now, id=ulid.new().str)
174
+ async with self._session_scope() as session:
175
+ await self._execute_statement(
176
+ session, insert(self.db_model).values(**data_dict)
177
+ )
178
+ statement = select(self.db_model).where(self.db_model.id == data_dict["id"])
179
+ result = await self._execute_statement(session, statement)
180
+ created_entity = result.scalar_one_or_none()
181
+ if created_entity is None:
182
+ raise NotFoundError(f"{self.entity_name} not found after creation")
183
+ return self.db_model(**created_entity.model_dump())
184
+
185
+ async def create_bulk(self, data_list: list[CreateModel]) -> list[DBModel]:
186
+ now = datetime.datetime.now(datetime.timezone.utc)
187
+ data_dicts = [
188
+ self._model_to_data_dict(data, created_at=now, id=ulid.new().str)
189
+ for data in data_list
190
+ ]
191
+ async with self._session_scope() as session:
192
+ await self._execute_statement(
193
+ session, insert(self.db_model).values(data_dicts)
194
+ )
195
+ id_list = [d["id"] for d in data_dicts]
196
+ statement = select(self.db_model).where(self.db_model.id.in_(id_list))
197
+ result = await self._execute_statement(session, statement)
198
+ return [
199
+ self.db_model(**entity.model_dump())
200
+ for entity in result.scalars().all()
201
+ ]
202
+
203
+ async def delete(self, id: str) -> DBModel:
204
+ async with self._session_scope() as session:
205
+ statement = select(self.db_model).where(self.db_model.id == id)
206
+ result = await self._execute_statement(session, statement)
207
+ entity = result.scalar_one_or_none()
208
+ if not entity:
209
+ raise NotFoundError(f"{self.entity_name} not found")
210
+ await self._execute_statement(
211
+ session, delete(self.db_model).where(self.db_model.id == id)
212
+ )
213
+ return self.db_model(**entity.model_dump())
214
+
215
+ async def delete_bulk(self, id_list: list[str]) -> list[DBModel]:
216
+ async with self._session_scope() as session:
217
+ statement = select(self.db_model).where(self.db_model.id.in_(id_list))
218
+ result = await self._execute_statement(session, statement)
219
+ entities = result.scalars().all()
220
+ await self._execute_statement(
221
+ session, delete(self.db_model).where(self.db_model.id.in_(id_list))
222
+ )
223
+ return [self.db_model(**entity.model_dump()) for entity in entities]
224
+
225
+ async def update(self, id: str, data: UpdateModel) -> DBModel:
226
+ now = datetime.datetime.now(datetime.timezone.utc)
227
+ update_data = self._model_to_data_dict(data, updated_at=now)
228
+ async with self._session_scope() as session:
229
+ statement = (
230
+ update(self.db_model)
231
+ .where(self.db_model.id == id)
232
+ .values(**update_data)
233
+ )
234
+ await self._execute_statement(session, statement)
235
+ result = await self._execute_statement(
236
+ session, select(self.db_model).where(self.db_model.id == id)
237
+ )
238
+ updated_instance = result.scalar_one_or_none()
239
+ if not updated_instance:
240
+ raise NotFoundError(f"{self.entity_name} not found")
241
+ return self.db_model(**updated_instance.model_dump())
242
+
243
+ async def update_bulk(self, id_list: list[str], data: UpdateModel) -> list[DBModel]:
244
+ now = datetime.datetime.now(datetime.timezone.utc)
245
+ update_data = self._model_to_data_dict(data, updated_at=now)
246
+ update_data = {k: v for k, v in update_data.items() if v is not None}
247
+ if not update_data:
248
+ raise InvalidValueError("No valid update data provided")
249
+ async with self._session_scope() as session:
250
+ statement = (
251
+ update(self.db_model)
252
+ .where(self.db_model.id.in_(id_list))
253
+ .values(**update_data)
254
+ )
255
+ await self._execute_statement(session, statement)
256
+ result = await self._execute_statement(
257
+ session, select(self.db_model).where(self.db_model.id.in_(id_list))
258
+ )
259
+ return [
260
+ self.db_model(**entity.model_dump())
261
+ for entity in result.scalars().all()
262
+ ]
@@ -0,0 +1,236 @@
1
+ import inspect
2
+ from enum import Enum
3
+ from functools import partial
4
+ from logging import Logger
5
+ from typing import Any, Callable, Sequence
6
+
7
+ import httpx
8
+ from fastapi import APIRouter, Depends, params
9
+ from my_app_name.common.error import ClientAPIError
10
+ from pydantic import BaseModel
11
+
12
+
13
+ class RouteParam:
14
+ def __init__(
15
+ self,
16
+ path: str,
17
+ response_model: Any,
18
+ status_code: int | None = None,
19
+ tags: list[str | Enum] | None = None,
20
+ summary: str | None = None,
21
+ description: str = "",
22
+ deprecated: bool | None = None,
23
+ methods: set[str] | list[str] | None = None,
24
+ func: Callable | None = None,
25
+ ):
26
+ self.path = path
27
+ self.response_model = response_model
28
+ self.status_code = status_code
29
+ self.tags = tags
30
+ self.summary = summary
31
+ self.description = description
32
+ self.deprecated = deprecated
33
+ self.methods = methods
34
+ self.func = func
35
+
36
+
37
+ class BaseService:
38
+ _route_params: dict[str, RouteParam] = {}
39
+
40
+ def __init__(self, logger: Logger):
41
+ self._logger = logger
42
+ self._route_params: dict[str, RouteParam] = {}
43
+ for name, method in self.__class__.__dict__.items():
44
+ if hasattr(method, "__route_param__"):
45
+ self._route_params[name] = getattr(method, "__route_param__")
46
+
47
+ @property
48
+ def logger(self) -> Logger:
49
+ return self._logger
50
+
51
+ @classmethod
52
+ def route(
53
+ cls,
54
+ path: str,
55
+ *,
56
+ response_model: Any = None,
57
+ status_code: int | None = None,
58
+ tags: list[str | Enum] | None = None,
59
+ dependencies: Sequence[params.Depends] | None = None,
60
+ summary: str | None = None,
61
+ description: str = None,
62
+ deprecated: bool | None = None,
63
+ methods: set[str] | list[str] | None = None,
64
+ ):
65
+ """
66
+ Decorator to register a method with its HTTP details.
67
+ """
68
+
69
+ def decorator(func: Callable):
70
+ # Check for Depends in function parameters
71
+ sig = inspect.signature(func)
72
+ for param in sig.parameters.values():
73
+ if param.annotation is Depends or (
74
+ hasattr(param.annotation, "__origin__")
75
+ and param.annotation.__origin__ is Depends
76
+ ):
77
+ raise ValueError(
78
+ f"Depends is not allowed in function parameters. Found in {func.__name__}" # noqa
79
+ )
80
+ # Inject __route_param__ property to the method
81
+ # Method with __route_param__ property will automatically
82
+ # registered to self._route_param and will be automatically exposed
83
+ # into DirectClient and APIClient
84
+ func.__route_param__ = RouteParam(
85
+ path=path,
86
+ response_model=response_model,
87
+ status_code=status_code,
88
+ tags=tags,
89
+ summary=summary,
90
+ description=description,
91
+ deprecated=deprecated,
92
+ methods=methods,
93
+ func=func,
94
+ )
95
+ return func
96
+
97
+ return decorator
98
+
99
+ def as_direct_client(self):
100
+ """
101
+ Dynamically create a direct client class.
102
+ """
103
+ _methods = self._route_params
104
+ DirectClient = _create_client_class("DirectClient")
105
+ for name, details in _methods.items():
106
+ func = details.func
107
+ client_method = _create_direct_client_method(self._logger, func, self)
108
+ # Use __get__ to make a bounded method,
109
+ # ensuring that client_method use DirectClient as `self`
110
+ setattr(DirectClient, name, client_method.__get__(DirectClient))
111
+ return DirectClient
112
+
113
+ def as_api_client(self, base_url: str):
114
+ """
115
+ Dynamically create an API client class.
116
+ """
117
+ _methods = self._route_params
118
+ APIClient = _create_client_class("APIClient")
119
+ # Dynamically generate methods
120
+ for name, param in _methods.items():
121
+ client_method = _create_api_client_method(self._logger, param, base_url)
122
+ # Use __get__ to make a bounded method,
123
+ # ensuring that client_method use APIClient as `self`
124
+ setattr(APIClient, name, client_method.__get__(APIClient))
125
+ return APIClient
126
+
127
+ def serve_route(self, app: APIRouter):
128
+ """
129
+ Dynamically add routes to FastAPI.
130
+ """
131
+ for _, route_param in self._route_params.items():
132
+ bound_func = partial(route_param.func, self)
133
+ bound_func.__name__ = route_param.func.__name__
134
+ bound_func.__doc__ = route_param.func.__doc__
135
+ app.add_api_route(
136
+ path=route_param.path,
137
+ endpoint=bound_func,
138
+ response_model=route_param.response_model,
139
+ status_code=route_param.status_code,
140
+ tags=route_param.tags,
141
+ summary=route_param.summary,
142
+ description=route_param.description,
143
+ deprecated=route_param.deprecated,
144
+ methods=route_param.methods,
145
+ )
146
+
147
+
148
+ def _create_client_class(name):
149
+ class Client:
150
+ pass
151
+
152
+ Client.__name__ = name
153
+ return Client
154
+
155
+
156
+ def _create_direct_client_method(logger: Logger, func: Callable, service: BaseService):
157
+ async def client_method(self, *args, **kwargs):
158
+ return await func(service, *args, **kwargs)
159
+
160
+ return client_method
161
+
162
+
163
+ def _create_api_client_method(logger: Logger, param: RouteParam, base_url: str):
164
+ async def client_method(*args, **kwargs):
165
+ url = base_url + param.path
166
+ method = (
167
+ param.methods[0].lower()
168
+ if isinstance(param.methods, list)
169
+ else param.methods.lower()
170
+ )
171
+ # Get the signature of the original function
172
+ sig = inspect.signature(param.func)
173
+ # Bind the arguments to the signature
174
+ bound_args = sig.bind(*args, **kwargs)
175
+ bound_args.apply_defaults()
176
+ # Analyze parameters
177
+ params = list(sig.parameters.values())
178
+ body_params = [
179
+ p
180
+ for p in params
181
+ if p.name != "self" and p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
182
+ ]
183
+ # Prepare the request
184
+ path_params = {}
185
+ query_params = {}
186
+ body = {}
187
+ for name, value in bound_args.arguments.items():
188
+ if name == "self":
189
+ continue
190
+ if f"{{{name}}}" in param.path:
191
+ path_params[name] = value
192
+ elif isinstance(value, BaseModel):
193
+ body = _parse_api_param(value)
194
+ elif method in ["get", "delete"]:
195
+ query_params[name] = _parse_api_param(value)
196
+ elif len(body_params) == 1 and name == body_params[0].name:
197
+ # If there's only one body parameter, use its value directly
198
+ body = _parse_api_param(value)
199
+ else:
200
+ body[name] = _parse_api_param(value)
201
+ # Format the URL with path parameters
202
+ url = url.format(**path_params)
203
+ logger.info(
204
+ f"Sending request to {url} with method {method}, json={body}, params={query_params}" # noqa
205
+ )
206
+ async with httpx.AsyncClient() as client:
207
+ if method in ["get", "delete"]:
208
+ response = await getattr(client, method)(url, params=query_params)
209
+ else:
210
+ response = await getattr(client, method)(
211
+ url, json=body, params=query_params
212
+ )
213
+ logger.info(
214
+ f"Received response: status={response.status_code}, content={response.content}"
215
+ )
216
+ if response.status_code >= 400:
217
+ error_detail = (
218
+ response.json()
219
+ if response.headers.get("content-type") == "application/json"
220
+ else response.text
221
+ )
222
+ raise ClientAPIError(response.status_code, error_detail)
223
+ return response.json()
224
+
225
+ return client_method
226
+
227
+
228
+ def _parse_api_param(data: Any) -> Any:
229
+ if isinstance(data, BaseModel):
230
+ return data.model_dump()
231
+ elif isinstance(data, list):
232
+ return [_parse_api_param(item) for item in data]
233
+ elif isinstance(data, dict):
234
+ return {key: _parse_api_param(value) for key, value in data.items()}
235
+ else:
236
+ return data
@@ -2,4 +2,4 @@ from my_app_name.config import APP_DB_URL
2
2
  from sqlmodel import create_engine
3
3
 
4
4
  connect_args = {"check_same_thread": False}
5
- engine = create_engine(APP_DB_URL, connect_args=connect_args)
5
+ db_engine = create_engine(APP_DB_URL, connect_args=connect_args, echo=True)
@@ -6,3 +6,15 @@ from fastapi import HTTPException
6
6
  class NotFoundError(HTTPException):
7
7
  def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
8
8
  super().__init__(404, {"message": message}, headers)
9
+
10
+
11
+ class InvalidValueError(HTTPException):
12
+ def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
13
+ super().__init__(422, {"message": message}, headers)
14
+
15
+
16
+ class ClientAPIError(HTTPException):
17
+ def __init__(
18
+ self, status_code: int, message: str, headers: Dict[str, str] | None = None
19
+ ) -> None:
20
+ super().__init__(status_code, {"message": message}, headers)
@@ -0,0 +1,10 @@
1
+ import logging
2
+
3
+ # Set up logging
4
+ logging.basicConfig(
5
+ level=logging.INFO,
6
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
7
+ )
8
+
9
+ # Create a logger for your application
10
+ logger: logging.Logger = logging.getLogger("fastapp")
@@ -0,0 +1,7 @@
1
+ from my_app_name.common.util.parser import (
2
+ create_default_filter_param_parser,
3
+ create_default_sort_param_parser,
4
+ )
5
+
6
+ parse_filter_param = create_default_filter_param_parser()
7
+ parse_sort_param = create_default_sort_param_parser()
@@ -0,0 +1,47 @@
1
+ import os
2
+ from contextlib import asynccontextmanager
3
+
4
+ from fastapi import FastAPI, HTTPException
5
+ from fastapi.openapi.docs import get_swagger_ui_html
6
+ from fastapi.responses import FileResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from sqlalchemy.engine import Engine
9
+ from sqlmodel import SQLModel
10
+ from starlette.types import Lifespan
11
+
12
+
13
+ def get_default_app_title(app_title: str, mode: str, modules: list[str] = []) -> str:
14
+ if mode == "monolith":
15
+ return app_title
16
+ return f"{app_title} - {', '.join(modules)}"
17
+
18
+
19
+ def create_default_app_lifespan(db_engine: Engine) -> Lifespan:
20
+ @asynccontextmanager
21
+ async def default_app_lifespan(app: FastAPI):
22
+ SQLModel.metadata.create_all(db_engine)
23
+ yield
24
+
25
+ return default_app_lifespan
26
+
27
+
28
+ def serve_static_dir(app: FastAPI, static_dir: str):
29
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
30
+
31
+ # Serve static files
32
+ @app.get("/static/{file_path:path}", include_in_schema=False)
33
+ async def static_files(file_path: str):
34
+ full_path = os.path.join(static_dir, file_path)
35
+ if os.path.isfile(full_path):
36
+ return FileResponse(full_path)
37
+ raise HTTPException(status_code=404, detail="File not found")
38
+
39
+
40
+ def serve_docs(app: FastAPI, app_title: str, favicon_url: str):
41
+ @app.get("/docs", include_in_schema=False)
42
+ async def swagger_ui_html():
43
+ return get_swagger_ui_html(
44
+ openapi_url="/openapi.json",
45
+ title=app_title,
46
+ swagger_favicon_url=favicon_url,
47
+ )