zrb 1.0.0a21__py3-none-any.whl → 1.0.0b2__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 (191) hide show
  1. zrb/__init__.py +2 -1
  2. zrb/__main__.py +0 -3
  3. zrb/builtin/__init__.py +3 -0
  4. zrb/builtin/group.py +1 -0
  5. zrb/builtin/llm/llm_chat.py +2 -2
  6. zrb/builtin/llm/tool/web.py +1 -1
  7. zrb/builtin/project/add/fastapp/fastapp_task.py +2 -0
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -2
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +80 -20
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +150 -42
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +16 -6
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/client_method.py +57 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +72 -0
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/format_task.py +1 -1
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +13 -0
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +23 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -0
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +7 -0
  24. 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
  25. 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
  26. 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
  27. 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
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/route.py +11 -11
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +2 -2
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +8 -8
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +47 -20
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app_factory.py +29 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +185 -101
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +236 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/{db_engine.py → db_engine_factory.py} +1 -1
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +12 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/logger_factory.py +10 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/parser_factory.py +7 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/app.py +47 -0
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +105 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/user_agent.py +58 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +37 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +25 -1
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/main.py +1 -1
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_api_client.py +16 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +163 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client_factory.py +9 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_direct_client.py +15 -0
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +160 -0
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +18 -1
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +7 -3
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +117 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service_factory.py +11 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_db_repository.py +26 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_repository.py +61 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/repository/permission_repository_factory.py +13 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +75 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +59 -0
  59. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository_factory.py +13 -0
  60. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +105 -0
  61. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service_factory.py +7 -0
  62. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +42 -13
  63. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +38 -17
  64. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository_factory.py +2 -2
  65. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +105 -0
  66. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +7 -0
  67. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +43 -14
  68. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +198 -28
  69. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +74 -0
  70. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +6 -0
  71. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/homepage.html +6 -0
  72. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-192x192.png +0 -0
  73. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/android-chrome-512x512.png +0 -0
  74. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/images/favicon-32x32.png +0 -0
  75. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.amber.min.css +4 -0
  76. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.blue.min.css +4 -0
  77. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.cyan.min.css +4 -0
  78. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.fuchsia.min.css +4 -0
  79. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.green.min.css +4 -0
  80. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.grey.min.css +4 -0
  81. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.indigo.min.css +4 -0
  82. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.jade.min.css +4 -0
  83. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.lime.min.css +4 -0
  84. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.min.css +4 -0
  85. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.orange.min.css +4 -0
  86. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pink.min.css +4 -0
  87. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.pumpkin.min.css +4 -0
  88. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.purple.min.css +4 -0
  89. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.red.min.css +4 -0
  90. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.sand.min.css +4 -0
  91. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.slate.min.css +4 -0
  92. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.violet.min.css +4 -0
  93. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.yellow.min.css +4 -0
  94. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/pico-css/pico.zinc.min.css +4 -0
  95. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +34 -0
  96. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -0
  97. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +17 -5
  98. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +50 -4
  99. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +52 -0
  100. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +30 -5
  101. zrb/builtin/python.py +1 -1
  102. zrb/builtin/random.py +61 -0
  103. zrb/cmd/cmd_val.py +6 -5
  104. zrb/content_transformer/any_content_transformer.py +7 -0
  105. zrb/content_transformer/content_transformer.py +6 -0
  106. zrb/runner/cli.py +14 -7
  107. zrb/runner/web_app.py +28 -280
  108. zrb/runner/web_config/config.py +91 -0
  109. zrb/runner/web_config/config_factory.py +26 -0
  110. zrb/runner/web_route/docs_route.py +17 -0
  111. zrb/runner/web_route/error_page/serve_default_404.py +28 -0
  112. zrb/runner/{web_controller/error_page/controller.py → web_route/error_page/show_error_page.py} +2 -2
  113. zrb/runner/{web_controller → web_route}/error_page/view.html +5 -0
  114. zrb/runner/web_route/home_page/home_page_route.py +51 -0
  115. zrb/runner/web_route/login_api_route.py +31 -0
  116. zrb/runner/web_route/login_page/login_page_route.py +39 -0
  117. zrb/runner/web_route/logout_api_route.py +18 -0
  118. zrb/runner/web_route/logout_page/logout_page_route.py +40 -0
  119. zrb/runner/{web_controller/group_info_page/controller.py → web_route/node_page/group/show_group_page.py} +3 -3
  120. zrb/runner/web_route/node_page/node_page_route.py +50 -0
  121. zrb/runner/{web_controller/session_page/controller.py → web_route/node_page/task/show_task_page.py} +3 -3
  122. zrb/runner/web_route/refresh_token_api_route.py +38 -0
  123. zrb/runner/web_route/static/static_route.py +44 -0
  124. zrb/runner/web_route/task_input_api_route.py +47 -0
  125. zrb/runner/web_route/task_session_api_route.py +102 -0
  126. zrb/runner/web_schema/session.py +5 -0
  127. zrb/runner/web_schema/token.py +11 -0
  128. zrb/runner/web_schema/user.py +32 -0
  129. zrb/runner/web_util/cookie.py +29 -0
  130. zrb/runner/{web_util.py → web_util/html.py} +1 -23
  131. zrb/runner/web_util/token.py +72 -0
  132. zrb/runner/web_util/user.py +63 -0
  133. zrb/session/session.py +6 -4
  134. zrb/session_state_logger/{default_session_state_logger.py → session_state_logger_factory.py} +1 -1
  135. zrb/task/base_task.py +53 -6
  136. zrb/task/base_trigger.py +2 -0
  137. zrb/task/cmd_task.py +9 -5
  138. zrb/task/http_check.py +2 -0
  139. zrb/task/llm_task.py +2 -0
  140. zrb/task/make_task.py +2 -0
  141. zrb/task/rsync_task.py +2 -0
  142. zrb/task/scaffolder.py +8 -5
  143. zrb/task/scheduler.py +2 -0
  144. zrb/task/tcp_check.py +2 -0
  145. zrb/task_status/task_status.py +4 -3
  146. zrb/util/cmd/command.py +1 -0
  147. zrb/util/file.py +7 -1
  148. {zrb-1.0.0a21.dist-info → zrb-1.0.0b2.dist-info}/METADATA +1 -1
  149. zrb-1.0.0b2.dist-info/RECORD +307 -0
  150. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/any_client_method.py +0 -27
  151. 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
  152. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/api_client.py +0 -6
  153. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/direct_client.py +0 -6
  154. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/my_module/client/factory.py +0 -9
  155. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/app.py +0 -20
  156. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_usecase.py +0 -245
  157. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/any_client.py +0 -33
  158. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/api_client.py +0 -7
  159. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/direct_client.py +0 -6
  160. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/factory.py +0 -9
  161. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_user_table.py +0 -37
  162. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase.py +0 -53
  163. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_usecase_factory.py +0 -6
  164. zrb/runner/web_config.py +0 -274
  165. zrb/runner/web_controller/home_page/controller.py +0 -33
  166. zrb/runner/web_controller/login_page/controller.py +0 -25
  167. zrb/runner/web_controller/logout_page/controller.py +0 -26
  168. zrb-1.0.0a21.dist-info/RECORD +0 -244
  169. /zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/{create_column_task.py → add_column_task.py} +0 -0
  170. /zrb/{runner/web_controller → builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission}/__init__.py +0 -0
  171. /zrb/{runner/web_controller/group_info_page → builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role}/__init__.py +0 -0
  172. /zrb/runner/{web_controller/home_page → web_route}/__init__.py +0 -0
  173. /zrb/runner/{web_controller/session_page → web_route/home_page}/__init__.py +0 -0
  174. /zrb/runner/{web_controller → web_route}/home_page/view.html +0 -0
  175. /zrb/runner/{web_controller → web_route}/login_page/view.html +0 -0
  176. /zrb/runner/{web_controller → web_route}/logout_page/view.html +0 -0
  177. /zrb/runner/{web_controller/group_info_page → web_route/node_page/group}/view.html +0 -0
  178. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/partial/input.html +0 -0
  179. /zrb/runner/{web_controller/session_page → web_route/node_page/task}/view.html +0 -0
  180. /zrb/runner/{refresh-token.template.js → web_route/static/refresh-token.template.js} +0 -0
  181. /zrb/runner/{web_controller/static → web_route/static/resources}/common.css +0 -0
  182. /zrb/runner/{web_controller/static → web_route/static/resources}/favicon-32x32.png +0 -0
  183. /zrb/runner/{web_controller/static → web_route/static/resources}/login/event.js +0 -0
  184. /zrb/runner/{web_controller/static → web_route/static/resources}/logout/event.js +0 -0
  185. /zrb/runner/{web_controller/static → web_route/static/resources}/pico.min.css +0 -0
  186. /zrb/runner/{web_controller/static → web_route/static/resources}/session/common-util.js +0 -0
  187. /zrb/runner/{web_controller/static → web_route/static/resources}/session/current-session.js +0 -0
  188. /zrb/runner/{web_controller/static → web_route/static/resources}/session/event.js +0 -0
  189. /zrb/runner/{web_controller/static → web_route/static/resources}/session/past-session.js +0 -0
  190. {zrb-1.0.0a21.dist-info → zrb-1.0.0b2.dist-info}/WHEEL +0 -0
  191. {zrb-1.0.0a21.dist-info → zrb-1.0.0b2.dist-info}/entry_points.txt +0 -0
@@ -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
+ )
@@ -0,0 +1,105 @@
1
+ import re
2
+ from typing import Callable
3
+
4
+ from sqlalchemy import asc, desc
5
+ from sqlalchemy.sql import ClauseElement, ColumnElement
6
+ from sqlmodel import SQLModel
7
+
8
+
9
+ def create_default_sort_param_parser() -> (
10
+ Callable[[SQLModel, str], list[ColumnElement]]
11
+ ):
12
+ def parse_sort_param(model: SQLModel, query: str) -> list[ColumnElement]:
13
+ """
14
+ Parse the sort parameter and return a list of SQLAlchemy ColumnElement objects.
15
+
16
+ Args:
17
+ model (Type[SQLModel]): The SQLModel class to be queried.
18
+ query (str): A comma-separated string of fields to sort by.
19
+ Prefix a field with '-' for descending order.
20
+
21
+ Returns:
22
+ list[ColumnElement]: A list of SQLAlchemy ColumnElement objects representing the sort order.
23
+
24
+ Example:
25
+ parse_sort_param(UserModel, "name,-age")
26
+ # Returns [asc(UserModel.name), desc(UserModel.age)]
27
+ """
28
+ sorts: list[ColumnElement] = []
29
+ sort_parts = query.split(",")
30
+ for part in sort_parts:
31
+ if part.startswith("-"):
32
+ key = part[1:]
33
+ order = desc
34
+ else:
35
+ key = part
36
+ order = asc
37
+ if hasattr(model, key):
38
+ sorts.append(order(getattr(model, key)))
39
+ return sorts
40
+
41
+ return parse_sort_param
42
+
43
+
44
+ def create_default_filter_param_parser() -> (
45
+ Callable[[SQLModel, str], list[ClauseElement]]
46
+ ):
47
+ def parse_filter_param(model: SQLModel, query: str) -> list[ClauseElement]:
48
+ """
49
+ Parse the filter parameter and return a list of SQLAlchemy ClauseElement objects.
50
+
51
+ Args:
52
+ model (Type[SQLModel]): The SQLModel class to be queried.
53
+ query (str): A comma-separated string of filters.
54
+ Each filter should be in the format "field:operator:value".
55
+ Use '\,' to escape commas within values.
56
+
57
+ Returns:
58
+ list[ClauseElement]: A list of SQLAlchemy ClauseElement objects representing the filters.
59
+
60
+ Supported operators:
61
+ eq: Equal to
62
+ ne: Not equal to
63
+ gt: Greater than
64
+ gte: Greater than or equal to
65
+ lt: Less than
66
+ lte: Less than or equal to
67
+ like: SQL LIKE (use % for wildcards)
68
+ in: In a list of values (semicolon-separated)
69
+
70
+ Example:
71
+ parse_filter_param(UserModel, "age:gte:18,name:like:John%,role:in:admin;user,address:eq:123\, Main St.")
72
+ # Returns [UserModel.age >= 18, UserModel.name.like("John%"), UserModel.role.in_(["admin", "user"]), UserModel.address == "123, Main St."]
73
+ """
74
+ filters: list[ClauseElement] = []
75
+ filter_parts = split_by_comma(query)
76
+ for part in filter_parts:
77
+ match = re.match(r"(.+):(.+):(.+)", part)
78
+ if match:
79
+ key, op, value = match.groups()
80
+ value = value.replace(r"\,", ",") # Unescape commas in the value
81
+ if hasattr(model, key):
82
+ column = getattr(model, key)
83
+ if op == "eq":
84
+ filters.append(column == value)
85
+ elif op == "ne":
86
+ filters.append(column != value)
87
+ elif op == "gt":
88
+ filters.append(column > value)
89
+ elif op == "gte":
90
+ filters.append(column >= value)
91
+ elif op == "lt":
92
+ filters.append(column < value)
93
+ elif op == "lte":
94
+ filters.append(column <= value)
95
+ elif op == "like":
96
+ filters.append(column.like(value))
97
+ elif op == "in":
98
+ filters.append(column.in_(value.split(";")))
99
+ return filters
100
+
101
+ return parse_filter_param
102
+
103
+
104
+ def split_by_comma(s: str, delimiter: str = ",") -> list[str]:
105
+ return re.split(r"(?<!\\)" + re.escape(delimiter), s)
@@ -0,0 +1,58 @@
1
+ import re
2
+
3
+
4
+ def get_os_from_user_agent(user_agent: str) -> str:
5
+ os_patterns = [
6
+ (r"Windows NT (\d+\.\d+)", "Windows"),
7
+ (r"Mac OS X (\d+[_\.]\d+)", "MacOS"),
8
+ (r"Android (\d+[\.\d]*)", "Android"),
9
+ (r"iPhone OS (\d+[_\.]\d+)", "iOS"),
10
+ (r"iPad.*OS (\d+[_\.]\d+)", "iPadOS"),
11
+ (r"Linux", "Linux"),
12
+ ]
13
+ os = "Unknown OS"
14
+ # Match OS
15
+ for pattern, name in os_patterns:
16
+ match = re.search(pattern, user_agent)
17
+ if match:
18
+ os = (
19
+ f"{name} {match.group(1).replace('_', '.')}" if match.groups() else name
20
+ )
21
+ break
22
+ return os
23
+
24
+
25
+ def get_browser_from_user_agent(user_agent: str) -> str:
26
+ browser_patterns = [
27
+ (r"Chrome/([\d\.]+)", "Chrome"),
28
+ (r"Firefox/([\d\.]+)", "Firefox"),
29
+ (r"Edg/([\d\.]+)", "Edge"),
30
+ (r"Safari/([\d\.]+)", "Safari"),
31
+ ]
32
+ browser = "Unknown Browser"
33
+ # Match Browser
34
+ for pattern, name in browser_patterns:
35
+ match = re.search(pattern, user_agent)
36
+ if match:
37
+ browser = f"{name} {match.group(1)}"
38
+ break
39
+ return browser
40
+
41
+
42
+ def get_device_from_user_agent(user_agent: str):
43
+ device_patterns = [
44
+ (r"iPhone", "iPhone"),
45
+ (r"iPad", "iPad"),
46
+ (r"Android.*Mobile", "Android Phone"),
47
+ (r"Android", "Android Tablet"),
48
+ (r"Macintosh", "Mac"),
49
+ (r"Windows NT", "Windows PC"),
50
+ (r"Linux", "Linux Device"),
51
+ ]
52
+ device = "Unknown Device"
53
+ # Match Device
54
+ for pattern, name in device_patterns:
55
+ if re.search(pattern, user_agent):
56
+ device = name
57
+ break
58
+ return device
@@ -0,0 +1,37 @@
1
+ import os
2
+ from typing import Any
3
+
4
+ from fastapi.responses import HTMLResponse
5
+ from jinja2 import Environment, FileSystemLoader
6
+
7
+
8
+ def render_str(template_path: str, **data: Any) -> str:
9
+ """
10
+ Renders a Jinja2 template with the given data.
11
+
12
+ :param template_path: The absolute or relative file path of the template.
13
+ :param data: A dictionary of data to pass to the template (default is None).
14
+ :return: Rendered template as a string.
15
+ """
16
+ if data is None:
17
+ data = {}
18
+ # Get the directory and file name
19
+ directory, template_name = os.path.split(template_path)
20
+ # Set up Jinja2 environment with the specific directory
21
+ env = Environment(loader=FileSystemLoader(directory))
22
+ # Render the template
23
+ template = env.get_template(template_name)
24
+ return template.render(data)
25
+
26
+
27
+ def render_page(
28
+ template_path: str,
29
+ status_code: int = 200,
30
+ headers: dict[str, str] = None,
31
+ media_type: str | None = None,
32
+ **data: Any
33
+ ) -> HTMLResponse:
34
+ content = render_str(template_path, **data)
35
+ return HTMLResponse(
36
+ content=content, status_code=status_code, headers=headers, media_type=media_type
37
+ )
@@ -1,6 +1,30 @@
1
1
  import os
2
2
 
3
3
  APP_PATH = os.path.dirname(__file__)
4
+ APP_VERSION = "0.1.0"
5
+
6
+ APP_GATEWAY_VIEW_PATH = os.path.join(APP_PATH, "module", "gateway", "view")
7
+ APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH = os.getenv(
8
+ "MY_APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
9
+ os.path.join("template", "default.html"),
10
+ )
11
+ _DEFAULT_CSS_PATH = "/static/pico-css/pico.min.css"
12
+ APP_GATEWAY_CSS_PATH_LIST = [
13
+ path
14
+ for path in os.getenv("MY_APP_GATEWAY_CSS_PATH", _DEFAULT_CSS_PATH).split(":")
15
+ if path != ""
16
+ ]
17
+ APP_GATEWAY_JS_PATH_LIST = [
18
+ path for path in os.getenv("MY_APP_GATEWAY_JS_PATH", "").split(":") if path != ""
19
+ ]
20
+ APP_GATEWAY_TITLE = os.getenv("MY_APP_GATEWAY_TITLE", "My App Name")
21
+ APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_GATEWAY_SUBTITLE", "Just Another App")
22
+ APP_GATEWAY_LOGO_PATH = os.getenv(
23
+ "MY_APP_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
24
+ )
25
+ APP_GATEWAY_FAVICON_PATH = os.getenv(
26
+ "MY_APP_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
27
+ )
4
28
 
5
29
  APP_MODE = os.getenv("MY_APP_NAME_MODE", "monolith")
6
30
  APP_MODULES = [
@@ -15,7 +39,7 @@ APP_COMMUNICATION = os.getenv(
15
39
  )
16
40
  APP_REPOSITORY_TYPE = os.getenv("APP_REPOSITORY_TYPE", "db")
17
41
  APP_DB_URL = os.getenv(
18
- "APP_DB_URL",
42
+ "MY_APP_NAME_DB_URL",
19
43
  (
20
44
  f"sqlite:///{APP_PATH}/monolith.db"
21
45
  if APP_MODE == "monolith" or len(APP_MODULES) == 0
@@ -1,4 +1,4 @@
1
- from my_app_name.common.app import app
1
+ from my_app_name.common.app_factory import app
2
2
  from my_app_name.module.auth import route as auth_route
3
3
  from my_app_name.module.gateway import route as gateway_route
4
4
 
@@ -0,0 +1,16 @@
1
+ from my_app_name.config import APP_AUTH_BASE_URL
2
+ from my_app_name.module.auth.client.auth_client import AuthClient
3
+ from my_app_name.module.auth.service.permission.permission_service_factory import (
4
+ permission_service,
5
+ )
6
+ from my_app_name.module.auth.service.role.role_service_factory import role_service
7
+ from my_app_name.module.auth.service.user.user_service_factory import user_service
8
+
9
+
10
+ class AuthAPIClient(
11
+ permission_service.as_api_client(base_url=APP_AUTH_BASE_URL),
12
+ role_service.as_api_client(base_url=APP_AUTH_BASE_URL),
13
+ user_service.as_api_client(base_url=APP_AUTH_BASE_URL),
14
+ AuthClient,
15
+ ):
16
+ pass