core-framework 0.3.0__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 (222) hide show
  1. core_framework/__init__.py +0 -0
  2. core_framework/alembic/comment/alembic/README +1 -0
  3. core_framework/alembic/comment/alembic/env.py +72 -0
  4. core_framework/alembic/comment/alembic/script.py.mako +28 -0
  5. core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
  6. core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
  7. core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
  8. core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
  9. core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
  10. core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
  11. core_framework/alembic/comment/alembic.ini +52 -0
  12. core_framework/alembic/extension/alembic/README +1 -0
  13. core_framework/alembic/extension/alembic/env.py +98 -0
  14. core_framework/alembic/extension/alembic/script.py.mako +28 -0
  15. core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
  16. core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
  17. core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
  18. core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
  19. core_framework/alembic/extension/alembic.ini +147 -0
  20. core_framework/alembic/moderation/alembic/README +1 -0
  21. core_framework/alembic/moderation/alembic/env.py +98 -0
  22. core_framework/alembic/moderation/alembic/script.py.mako +28 -0
  23. core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
  24. core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
  25. core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
  26. core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
  27. core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
  28. core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
  29. core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
  30. core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
  31. core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
  32. core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
  33. core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
  34. core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
  35. core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
  36. core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
  37. core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
  38. core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
  39. core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
  40. core_framework/alembic/moderation/alembic.ini +147 -0
  41. core_framework/alembic/post/alembic/README +1 -0
  42. core_framework/alembic/post/alembic/env.py +97 -0
  43. core_framework/alembic/post/alembic/script.py.mako +28 -0
  44. core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
  45. core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
  46. core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
  47. core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
  48. core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
  49. core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
  50. core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
  51. core_framework/alembic/post/alembic.ini +149 -0
  52. core_framework/alembic/user/alembic/README +1 -0
  53. core_framework/alembic/user/alembic/env.py +98 -0
  54. core_framework/alembic/user/alembic/script.py.mako +28 -0
  55. core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
  56. core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
  57. core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
  58. core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
  59. core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
  60. core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
  61. core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
  62. core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
  63. core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
  64. core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
  65. core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
  66. core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
  67. core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
  68. core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
  69. core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
  70. core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
  71. core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
  72. core_framework/alembic/user/alembic.ini +147 -0
  73. core_framework/api/__init__.py +0 -0
  74. core_framework/api/admin/__init__.py +0 -0
  75. core_framework/api/admin/comments/router.py +69 -0
  76. core_framework/api/admin/comments/schemas.py +53 -0
  77. core_framework/api/admin/moderation/__init__.py +0 -0
  78. core_framework/api/admin/moderation/router.py +205 -0
  79. core_framework/api/admin/moderation/schemas.py +110 -0
  80. core_framework/api/admin/posts/router.py +62 -0
  81. core_framework/api/admin/posts/schemas.py +29 -0
  82. core_framework/api/admin/router.py +17 -0
  83. core_framework/api/admin/users/__init__.py +0 -0
  84. core_framework/api/admin/users/router.py +181 -0
  85. core_framework/api/admin/users/schemas.py +137 -0
  86. core_framework/api/auth/__init__.py +0 -0
  87. core_framework/api/auth/router.py +21 -0
  88. core_framework/api/auth/schemas.py +28 -0
  89. core_framework/api/comments/authenticated/router.py +126 -0
  90. core_framework/api/comments/authenticated/schemas.py +27 -0
  91. core_framework/api/comments/public/router.py +103 -0
  92. core_framework/api/comments/public/schemas.py +36 -0
  93. core_framework/api/comments/router.py +9 -0
  94. core_framework/api/comments/schemas.py +17 -0
  95. core_framework/api/dependencies.py +168 -0
  96. core_framework/api/events/router.py +39 -0
  97. core_framework/api/events/schemas.py +20 -0
  98. core_framework/api/posts/authenticated/router.py +83 -0
  99. core_framework/api/posts/authenticated/schemas.py +37 -0
  100. core_framework/api/posts/public/router.py +100 -0
  101. core_framework/api/posts/public/schemas.py +39 -0
  102. core_framework/api/posts/router.py +9 -0
  103. core_framework/api/posts/schemas.py +39 -0
  104. core_framework/api/router.py +19 -0
  105. core_framework/api/schemas.py +9 -0
  106. core_framework/api/system/__init__.py +0 -0
  107. core_framework/api/system/router.py +108 -0
  108. core_framework/api/users/__init__.py +0 -0
  109. core_framework/api/users/authenticated/__init__.py +0 -0
  110. core_framework/api/users/authenticated/router.py +244 -0
  111. core_framework/api/users/authenticated/schemas.py +81 -0
  112. core_framework/api/users/public/__init__.py +0 -0
  113. core_framework/api/users/public/router.py +25 -0
  114. core_framework/api/users/public/schemas.py +7 -0
  115. core_framework/api/users/router.py +9 -0
  116. core_framework/api/users/shared/schemas.py +174 -0
  117. core_framework/application/__init__.py +0 -0
  118. core_framework/application/auth/__init__.py +0 -0
  119. core_framework/application/auth/access_service.py +26 -0
  120. core_framework/application/auth/auth_service.py +10 -0
  121. core_framework/application/auth/models.py +10 -0
  122. core_framework/application/bootstrap.py +19 -0
  123. core_framework/application/comments/admin_service.py +236 -0
  124. core_framework/application/comments/aggregation_service.py +28 -0
  125. core_framework/application/comments/authenticated_service.py +89 -0
  126. core_framework/application/comments/public_service.py +218 -0
  127. core_framework/application/events/README.md +26 -0
  128. core_framework/application/events/event_service.py +51 -0
  129. core_framework/application/events/event_token.py +46 -0
  130. core_framework/application/events/models.py +9 -0
  131. core_framework/application/moderation/__init__.py +0 -0
  132. core_framework/application/moderation/appeal_service.py +98 -0
  133. core_framework/application/moderation/moderator_service.py +46 -0
  134. core_framework/application/moderation/report_service.py +180 -0
  135. core_framework/application/moderation/scheduled_service.py +5 -0
  136. core_framework/application/moderation/user_service.py +180 -0
  137. core_framework/application/posts/admin_service.py +104 -0
  138. core_framework/application/posts/aggregation_service.py +28 -0
  139. core_framework/application/posts/authenticated_service.py +72 -0
  140. core_framework/application/posts/public_service.py +197 -0
  141. core_framework/application/shared/__init__.py +0 -0
  142. core_framework/application/shared/enums.py +16 -0
  143. core_framework/application/shared/exceptions.py +16 -0
  144. core_framework/application/shared/user_agent.py +24 -0
  145. core_framework/application/users/__init__.py +0 -0
  146. core_framework/application/users/admin_service.py +298 -0
  147. core_framework/application/users/authenticated_service.py +179 -0
  148. core_framework/application/users/public_service.py +7 -0
  149. core_framework/application/users/scheduled_service.py +5 -0
  150. core_framework/bundled_alembic.py +57 -0
  151. core_framework/core/__init__.py +37 -0
  152. core_framework/core/cache.py +234 -0
  153. core_framework/core/context.py +14 -0
  154. core_framework/core/database.py +111 -0
  155. core_framework/core/exception_handlers/__init__.py +3 -0
  156. core_framework/core/exception_handlers/comment.py +99 -0
  157. core_framework/core/exception_handlers/common.py +5 -0
  158. core_framework/core/exception_handlers/moderation.py +104 -0
  159. core_framework/core/exception_handlers/post.py +54 -0
  160. core_framework/core/exception_handlers/setup.py +80 -0
  161. core_framework/core/exception_handlers/user.py +72 -0
  162. core_framework/core/http_client.py +64 -0
  163. core_framework/core/logging.py +99 -0
  164. core_framework/core/middleware.py +64 -0
  165. core_framework/core/observability.py +36 -0
  166. core_framework/core/pagination.py +203 -0
  167. core_framework/core/redis.py +135 -0
  168. core_framework/core/runtime.py +66 -0
  169. core_framework/core/settings.py +189 -0
  170. core_framework/domains/__init__.py +0 -0
  171. core_framework/domains/comment/README.md +243 -0
  172. core_framework/domains/comment/__init__.py +25 -0
  173. core_framework/domains/comment/constants.py +3 -0
  174. core_framework/domains/comment/dependencies.py +29 -0
  175. core_framework/domains/comment/enums.py +11 -0
  176. core_framework/domains/comment/exceptions.py +31 -0
  177. core_framework/domains/comment/models.py +54 -0
  178. core_framework/domains/comment/repository.py +947 -0
  179. core_framework/domains/comment/service.py +259 -0
  180. core_framework/domains/moderation/README.md +138 -0
  181. core_framework/domains/moderation/__init__.py +47 -0
  182. core_framework/domains/moderation/dependencies.py +29 -0
  183. core_framework/domains/moderation/enums.py +62 -0
  184. core_framework/domains/moderation/exceptions.py +31 -0
  185. core_framework/domains/moderation/models.py +94 -0
  186. core_framework/domains/moderation/repository.py +828 -0
  187. core_framework/domains/moderation/service.py +334 -0
  188. core_framework/domains/post/README.md +182 -0
  189. core_framework/domains/post/__init__.py +22 -0
  190. core_framework/domains/post/constants.py +3 -0
  191. core_framework/domains/post/dependencies.py +29 -0
  192. core_framework/domains/post/enums.py +18 -0
  193. core_framework/domains/post/exceptions.py +21 -0
  194. core_framework/domains/post/models.py +53 -0
  195. core_framework/domains/post/repository.py +791 -0
  196. core_framework/domains/post/service.py +204 -0
  197. core_framework/domains/user/README.md +74 -0
  198. core_framework/domains/user/__init__.py +39 -0
  199. core_framework/domains/user/constants.py +8 -0
  200. core_framework/domains/user/dependencies.py +29 -0
  201. core_framework/domains/user/enums.py +19 -0
  202. core_framework/domains/user/exceptions.py +31 -0
  203. core_framework/domains/user/models.py +124 -0
  204. core_framework/domains/user/repository.py +612 -0
  205. core_framework/domains/user/service.py +257 -0
  206. core_framework/domains/user/utils.py +182 -0
  207. core_framework/main.py +104 -0
  208. core_framework/worker/__init__.py +0 -0
  209. core_framework/worker/main.py +56 -0
  210. core_framework/worker/schedules/__init__.py +35 -0
  211. core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
  212. core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
  213. core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
  214. core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
  215. core_framework/worker/tasks/__init__.py +11 -0
  216. core_framework/worker/tasks/process_account_deletion.py +13 -0
  217. core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
  218. core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
  219. core_framework/worker/tasks/process_mute_lift.py +13 -0
  220. core_framework-0.3.0.dist-info/METADATA +22 -0
  221. core_framework-0.3.0.dist-info/RECORD +222 -0
  222. core_framework-0.3.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,99 @@
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from core_framework.core.exception_handlers.common import GENERIC_INTERNAL_SERVER_ERROR_DETAIL
5
+ from core_framework.domains.comment import (
6
+ BaseCommentException,
7
+ CommentEditLimitReachedException,
8
+ CommentNotFoundException,
9
+ CommentUpdateNotFoundException,
10
+ MaxReplyDepthException,
11
+ ParentCommentNotFoundException,
12
+ )
13
+
14
+
15
+ async def parent_comment_not_found_exception_handler(
16
+ request: Request,
17
+ exc: ParentCommentNotFoundException,
18
+ ) -> JSONResponse:
19
+ return JSONResponse(
20
+ status_code=status.HTTP_404_NOT_FOUND,
21
+ content={"detail": exc.message},
22
+ )
23
+
24
+
25
+ async def comment_not_found_exception_handler(
26
+ request: Request,
27
+ exc: CommentNotFoundException,
28
+ ) -> JSONResponse:
29
+ return JSONResponse(
30
+ status_code=status.HTTP_404_NOT_FOUND,
31
+ content={"detail": exc.message},
32
+ )
33
+
34
+
35
+ async def max_reply_depth_exception_handler(
36
+ request: Request,
37
+ exc: MaxReplyDepthException,
38
+ ) -> JSONResponse:
39
+ return JSONResponse(
40
+ status_code=status.HTTP_400_BAD_REQUEST,
41
+ content={"detail": exc.message},
42
+ )
43
+
44
+
45
+ async def comment_update_not_found_exception_handler(
46
+ request: Request,
47
+ exc: CommentUpdateNotFoundException,
48
+ ) -> JSONResponse:
49
+ return JSONResponse(
50
+ status_code=status.HTTP_400_BAD_REQUEST,
51
+ content={"detail": exc.message},
52
+ )
53
+
54
+
55
+ async def comment_edit_limit_reached_exception_handler(
56
+ request: Request,
57
+ exc: CommentEditLimitReachedException,
58
+ ) -> JSONResponse:
59
+ return JSONResponse(
60
+ status_code=status.HTTP_409_CONFLICT,
61
+ content={"detail": exc.message},
62
+ )
63
+
64
+
65
+ async def base_comment_exception_handler(
66
+ request: Request,
67
+ exc: BaseCommentException,
68
+ ) -> JSONResponse:
69
+ return JSONResponse(
70
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
71
+ content={"detail": GENERIC_INTERNAL_SERVER_ERROR_DETAIL},
72
+ )
73
+
74
+
75
+ def add_comment_exception_handlers(app: FastAPI) -> None:
76
+ app.add_exception_handler(
77
+ CommentNotFoundException,
78
+ comment_not_found_exception_handler, # ty: ignore[invalid-argument-type]
79
+ )
80
+ app.add_exception_handler(
81
+ ParentCommentNotFoundException,
82
+ parent_comment_not_found_exception_handler, # ty: ignore[invalid-argument-type]
83
+ )
84
+ app.add_exception_handler(
85
+ MaxReplyDepthException,
86
+ max_reply_depth_exception_handler, # ty: ignore[invalid-argument-type]
87
+ )
88
+ app.add_exception_handler(
89
+ CommentUpdateNotFoundException,
90
+ comment_update_not_found_exception_handler, # ty: ignore[invalid-argument-type]
91
+ )
92
+ app.add_exception_handler(
93
+ CommentEditLimitReachedException,
94
+ comment_edit_limit_reached_exception_handler, # ty: ignore[invalid-argument-type]
95
+ )
96
+ app.add_exception_handler(
97
+ BaseCommentException,
98
+ base_comment_exception_handler, # ty: ignore[invalid-argument-type]
99
+ )
@@ -0,0 +1,5 @@
1
+ from typing import Final
2
+
3
+ GENERIC_INTERNAL_SERVER_ERROR_DETAIL: Final[str] = (
4
+ "Something went wrong while processing your request. Please try again later."
5
+ )
@@ -0,0 +1,104 @@
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from core_framework.core.exception_handlers.common import GENERIC_INTERNAL_SERVER_ERROR_DETAIL
5
+ from core_framework.domains.moderation import (
6
+ AppealAlreadyDecidedException,
7
+ AppealNotFoundException,
8
+ AppealRequirementException,
9
+ BaseModerationException,
10
+ ExistingPendingAppealException,
11
+ SelfReportException,
12
+ )
13
+
14
+
15
+ async def self_report_exception_handler(
16
+ request: Request,
17
+ exc: SelfReportException,
18
+ ) -> JSONResponse:
19
+ return JSONResponse(
20
+ status_code=status.HTTP_400_BAD_REQUEST,
21
+ content={"detail": exc.message},
22
+ )
23
+
24
+
25
+ async def appeal_requirement_exception_handler(
26
+ request: Request,
27
+ exc: AppealRequirementException,
28
+ ) -> JSONResponse:
29
+ return JSONResponse(
30
+ status_code=status.HTTP_400_BAD_REQUEST,
31
+ content={"detail": exc.message},
32
+ )
33
+
34
+
35
+ async def existing_pending_appeal_exception_handler(
36
+ request: Request,
37
+ exc: ExistingPendingAppealException,
38
+ ) -> JSONResponse:
39
+ return JSONResponse(
40
+ status_code=status.HTTP_400_BAD_REQUEST,
41
+ content={"detail": exc.message},
42
+ )
43
+
44
+
45
+ async def appeal_not_found_exception_handler(
46
+ request: Request,
47
+ exc: AppealNotFoundException,
48
+ ) -> JSONResponse:
49
+ return JSONResponse(
50
+ status_code=status.HTTP_404_NOT_FOUND,
51
+ content={"detail": exc.message},
52
+ )
53
+
54
+
55
+ async def appeal_already_decided_exception_handler(
56
+ request: Request,
57
+ exc: AppealAlreadyDecidedException,
58
+ ) -> JSONResponse:
59
+ return JSONResponse(
60
+ status_code=status.HTTP_400_BAD_REQUEST,
61
+ content={"detail": exc.message},
62
+ )
63
+
64
+
65
+ async def base_moderation_exception_handler(
66
+ request: Request,
67
+ exc: BaseModerationException,
68
+ ) -> JSONResponse:
69
+ return JSONResponse(
70
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
71
+ content={"detail": GENERIC_INTERNAL_SERVER_ERROR_DETAIL},
72
+ )
73
+
74
+
75
+ def add_moderation_exception_handlers(app: FastAPI) -> None:
76
+ app.add_exception_handler(
77
+ SelfReportException,
78
+ self_report_exception_handler, # ty: ignore[invalid-argument-type]
79
+ )
80
+
81
+ app.add_exception_handler(
82
+ AppealRequirementException,
83
+ appeal_requirement_exception_handler, # ty: ignore[invalid-argument-type]
84
+ )
85
+
86
+ app.add_exception_handler(
87
+ ExistingPendingAppealException,
88
+ existing_pending_appeal_exception_handler, # ty: ignore[invalid-argument-type]
89
+ )
90
+
91
+ app.add_exception_handler(
92
+ AppealNotFoundException,
93
+ appeal_not_found_exception_handler, # ty: ignore[invalid-argument-type]
94
+ )
95
+
96
+ app.add_exception_handler(
97
+ AppealAlreadyDecidedException,
98
+ appeal_already_decided_exception_handler, # ty: ignore[invalid-argument-type]
99
+ )
100
+
101
+ app.add_exception_handler(
102
+ BaseModerationException,
103
+ base_moderation_exception_handler, # ty: ignore[invalid-argument-type]
104
+ )
@@ -0,0 +1,54 @@
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from core_framework.core.exception_handlers.common import GENERIC_INTERNAL_SERVER_ERROR_DETAIL
5
+ from core_framework.domains.post import (
6
+ BasePostException,
7
+ EditLimitReachedException,
8
+ PostUpdateNotFoundException,
9
+ )
10
+
11
+
12
+ async def post_update_not_found_exception_handler(
13
+ request: Request,
14
+ exc: PostUpdateNotFoundException,
15
+ ) -> JSONResponse:
16
+ return JSONResponse(
17
+ status_code=status.HTTP_400_BAD_REQUEST,
18
+ content={"detail": exc.message},
19
+ )
20
+
21
+
22
+ async def edit_limit_reached_exception_handler(
23
+ request: Request,
24
+ exc: EditLimitReachedException,
25
+ ) -> JSONResponse:
26
+ return JSONResponse(
27
+ status_code=status.HTTP_409_CONFLICT,
28
+ content={"detail": exc.message},
29
+ )
30
+
31
+
32
+ async def base_post_exception_handler(
33
+ request: Request,
34
+ exc: BasePostException,
35
+ ) -> JSONResponse:
36
+ return JSONResponse(
37
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
38
+ content={"detail": GENERIC_INTERNAL_SERVER_ERROR_DETAIL},
39
+ )
40
+
41
+
42
+ def add_post_exception_handlers(app: FastAPI) -> None:
43
+ app.add_exception_handler(
44
+ PostUpdateNotFoundException,
45
+ post_update_not_found_exception_handler, # ty: ignore[invalid-argument-type]
46
+ )
47
+ app.add_exception_handler(
48
+ EditLimitReachedException,
49
+ edit_limit_reached_exception_handler, # ty: ignore[invalid-argument-type]
50
+ )
51
+ app.add_exception_handler(
52
+ BasePostException,
53
+ base_post_exception_handler, # ty: ignore[invalid-argument-type]
54
+ )
@@ -0,0 +1,80 @@
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from core_framework.application.shared.exceptions import (
5
+ ForbiddenException,
6
+ UnauthorizedException,
7
+ UserNotFoundException,
8
+ )
9
+ from core_framework.core.exception_handlers.comment import add_comment_exception_handlers
10
+ from core_framework.core.exception_handlers.moderation import add_moderation_exception_handlers
11
+ from core_framework.core.exception_handlers.post import add_post_exception_handlers
12
+ from core_framework.core.exception_handlers.user import add_user_exception_handlers
13
+ from core_framework.domains.post import PostNotFoundException
14
+
15
+
16
+ async def unauthorized_exception_handler(
17
+ request: Request,
18
+ exc: UnauthorizedException,
19
+ ) -> JSONResponse:
20
+ return JSONResponse(
21
+ status_code=status.HTTP_401_UNAUTHORIZED,
22
+ content={"detail": exc.message},
23
+ )
24
+
25
+
26
+ async def forbidden_exception_handler(
27
+ request: Request,
28
+ exc: ForbiddenException,
29
+ ) -> JSONResponse:
30
+ return JSONResponse(
31
+ status_code=status.HTTP_403_FORBIDDEN,
32
+ content={"detail": exc.message},
33
+ )
34
+
35
+
36
+ async def user_not_found_exception_handler(
37
+ request: Request,
38
+ exc: UserNotFoundException,
39
+ ) -> JSONResponse:
40
+ return JSONResponse(
41
+ status_code=status.HTTP_404_NOT_FOUND,
42
+ content={"detail": exc.message},
43
+ )
44
+
45
+
46
+ async def post_not_found_exception_handler(
47
+ request: Request,
48
+ exc: PostNotFoundException,
49
+ ) -> JSONResponse:
50
+ return JSONResponse(
51
+ status_code=status.HTTP_404_NOT_FOUND,
52
+ content={"detail": exc.message},
53
+ )
54
+
55
+
56
+ def setup_exception_handlers(app: FastAPI) -> None:
57
+ app.add_exception_handler(
58
+ UnauthorizedException,
59
+ unauthorized_exception_handler, # ty: ignore[invalid-argument-type]
60
+ )
61
+
62
+ app.add_exception_handler(
63
+ ForbiddenException,
64
+ forbidden_exception_handler, # ty: ignore[invalid-argument-type]
65
+ )
66
+
67
+ app.add_exception_handler(
68
+ UserNotFoundException,
69
+ user_not_found_exception_handler, # ty: ignore[invalid-argument-type]
70
+ )
71
+
72
+ app.add_exception_handler(
73
+ PostNotFoundException,
74
+ post_not_found_exception_handler, # ty: ignore[invalid-argument-type]
75
+ )
76
+
77
+ add_user_exception_handlers(app)
78
+ add_comment_exception_handlers(app)
79
+ add_moderation_exception_handlers(app)
80
+ add_post_exception_handlers(app)
@@ -0,0 +1,72 @@
1
+ from fastapi import FastAPI, Request, status
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from core_framework.core.exception_handlers.common import GENERIC_INTERNAL_SERVER_ERROR_DETAIL
5
+ from core_framework.domains.user import (
6
+ BaseUserException,
7
+ SelfBlockException,
8
+ UserIdConflictException,
9
+ UsernameConflictException,
10
+ )
11
+
12
+
13
+ async def username_conflict_exception_handler(
14
+ request: Request,
15
+ exc: UsernameConflictException,
16
+ ) -> JSONResponse:
17
+ return JSONResponse(
18
+ status_code=status.HTTP_409_CONFLICT,
19
+ content={"detail": exc.message},
20
+ )
21
+
22
+
23
+ async def user_id_conflict_exception_handler(
24
+ request: Request,
25
+ exc: UserIdConflictException,
26
+ ) -> JSONResponse:
27
+ return JSONResponse(
28
+ status_code=status.HTTP_409_CONFLICT,
29
+ content={"detail": exc.message},
30
+ )
31
+
32
+
33
+ async def self_block_exception_handler(
34
+ request: Request,
35
+ exc: SelfBlockException,
36
+ ) -> JSONResponse:
37
+ return JSONResponse(
38
+ status_code=status.HTTP_400_BAD_REQUEST,
39
+ content={"detail": exc.message},
40
+ )
41
+
42
+
43
+ async def base_user_exception_handler(
44
+ request: Request,
45
+ exc: BaseUserException,
46
+ ) -> JSONResponse:
47
+ return JSONResponse(
48
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
49
+ content={"detail": GENERIC_INTERNAL_SERVER_ERROR_DETAIL},
50
+ )
51
+
52
+
53
+ def add_user_exception_handlers(app: FastAPI) -> None:
54
+ app.add_exception_handler(
55
+ UsernameConflictException,
56
+ username_conflict_exception_handler, # ty: ignore[invalid-argument-type]
57
+ )
58
+
59
+ app.add_exception_handler(
60
+ UserIdConflictException,
61
+ user_id_conflict_exception_handler, # ty: ignore[invalid-argument-type]
62
+ )
63
+
64
+ app.add_exception_handler(
65
+ SelfBlockException,
66
+ self_block_exception_handler, # ty: ignore[invalid-argument-type]
67
+ )
68
+
69
+ app.add_exception_handler(
70
+ BaseUserException,
71
+ base_user_exception_handler, # ty: ignore[invalid-argument-type]
72
+ )
@@ -0,0 +1,64 @@
1
+ from types import TracebackType
2
+ from typing import Any, Final, Self
3
+
4
+ from httpx import AsyncClient, Limits, Timeout
5
+
6
+ HTTP_CLIENT_NOT_CONNECTED_MSG: Final[str] = "HTTP client is not connected"
7
+ HTTP_CLIENT_ALREADY_CONNECTED_MSG: Final[str] = "HTTP client is already connected"
8
+
9
+
10
+ class HttpClient:
11
+ __slots__ = ("_client", "_limits", "_timeout", "_event_hooks", "_headers")
12
+
13
+ def __init__(
14
+ self,
15
+ timeout: Timeout,
16
+ limits: Limits,
17
+ event_hooks: dict[str, Any] | None = None,
18
+ headers: dict[str, Any] | None = None,
19
+ ) -> None:
20
+ self._client: AsyncClient | None = None
21
+ self._timeout = timeout
22
+ self._limits = limits
23
+ self._event_hooks = event_hooks
24
+ self._headers = headers
25
+
26
+ async def __aenter__(self) -> Self:
27
+ return await self.connect()
28
+
29
+ async def __aexit__(
30
+ self,
31
+ exc_type: type[BaseException] | None,
32
+ exc_value: BaseException | None,
33
+ traceback: TracebackType | None,
34
+ ) -> None:
35
+ await self.disconnect()
36
+
37
+ async def connect(self) -> Self:
38
+ if self._client is not None:
39
+ raise RuntimeError(HTTP_CLIENT_ALREADY_CONNECTED_MSG)
40
+
41
+ self._client = AsyncClient(
42
+ http2=True,
43
+ timeout=self._timeout,
44
+ limits=self._limits,
45
+ event_hooks=self._event_hooks,
46
+ headers=self._headers,
47
+ )
48
+ return self
49
+
50
+ async def disconnect(self) -> None:
51
+ if self._client is None:
52
+ return
53
+
54
+ try:
55
+ await self._client.aclose()
56
+ finally:
57
+ self._client = None
58
+
59
+ @property
60
+ def client(self) -> AsyncClient:
61
+ if self._client is None:
62
+ raise RuntimeError(HTTP_CLIENT_NOT_CONNECTED_MSG)
63
+
64
+ return self._client
@@ -0,0 +1,99 @@
1
+ import logging
2
+
3
+ import loguru
4
+ import orjson
5
+ from loguru import logger
6
+
7
+ from core_framework.core.context import client_ip, request_id, user_id, user_ip
8
+ from core_framework.core.settings import Settings
9
+
10
+
11
+ class InterceptHandler(logging.Handler):
12
+ def emit(self, record: logging.LogRecord) -> None:
13
+ try:
14
+ level: str | int = logger.level(record.levelname).name
15
+ except ValueError:
16
+ level = record.levelno
17
+
18
+ frame = logging.currentframe()
19
+ depth = 2
20
+ while frame and frame.f_code.co_filename == logging.__file__:
21
+ frame = frame.f_back
22
+ depth += 1
23
+
24
+ logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
25
+
26
+
27
+ def _configure_uvicorn_logging() -> None:
28
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
29
+
30
+ for name in logging.root.manager.loggerDict.keys():
31
+ if name.startswith("uvicorn"):
32
+ logging.getLogger(name).handlers = [InterceptHandler()]
33
+ logging.getLogger(name).propagate = False
34
+
35
+
36
+ def _custom_serializer(record: loguru.Record) -> str:
37
+ subset = {
38
+ "time": record["time"].isoformat(),
39
+ "level": record["level"].name,
40
+ "message": record["message"],
41
+ }
42
+
43
+ if record["extra"].get("context"):
44
+ subset.update(record["extra"]["context"])
45
+
46
+ return orjson.dumps(subset).decode()
47
+
48
+
49
+ def _add_context_to_record(record: loguru.Record) -> None:
50
+ if "context" not in record["extra"]:
51
+ record["extra"]["context"] = {}
52
+
53
+ if (user_id_value := user_id.get()) is not None:
54
+ record["extra"]["context"]["user_id"] = user_id_value
55
+ if (user_ip_value := user_ip.get()) is not None:
56
+ record["extra"]["context"]["user_ip"] = user_ip_value
57
+ if (client_ip_value := client_ip.get()) is not None:
58
+ record["extra"]["context"]["client_ip"] = client_ip_value
59
+ if (request_id_value := request_id.get()) is not None:
60
+ record["extra"]["context"]["request_id"] = request_id_value
61
+
62
+
63
+ def _patch_serializer(record: loguru.Record) -> None:
64
+ _add_context_to_record(record)
65
+ record["extra"]["json_output"] = _custom_serializer(record)
66
+
67
+
68
+ def setup_logging(settings: Settings) -> None:
69
+ if settings.app.environment == "local":
70
+ return
71
+
72
+ if settings.observability.enabled:
73
+ patched_logger = logger.patch(_add_context_to_record)
74
+
75
+ loguru.logger = patched_logger
76
+ globals()["logger"] = patched_logger
77
+
78
+ _configure_uvicorn_logging()
79
+ return
80
+
81
+ logger.remove()
82
+ patched_logger = logger.patch(_patch_serializer)
83
+
84
+ loguru.logger = patched_logger
85
+ globals()["logger"] = patched_logger
86
+
87
+ patched_logger.add(
88
+ settings.logging.sink,
89
+ format="{extra[json_output]}",
90
+ level=settings.logging.level,
91
+ rotation="10 MB",
92
+ retention="7 days",
93
+ compression="gz",
94
+ enqueue=True,
95
+ backtrace=True,
96
+ diagnose=False,
97
+ )
98
+
99
+ _configure_uvicorn_logging()
@@ -0,0 +1,64 @@
1
+ from brotli_asgi import BrotliMiddleware
2
+ from fastapi import FastAPI, status
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.middleware.trustedhost import TrustedHostMiddleware
5
+ from fastapi.responses import JSONResponse
6
+ from starlette.types import ASGIApp, Receive, Scope, Send
7
+ from ulid import ULID
8
+ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
9
+
10
+ from core_framework.core.context import REQUEST_CONTEXTVARS, client_ip, request_id
11
+ from core_framework.core.settings import Settings
12
+
13
+
14
+ class ContextMiddleware:
15
+ def __init__(self, app: ASGIApp) -> None:
16
+ self.app = app
17
+
18
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
19
+ if scope["type"] != "http":
20
+ return await self.app(scope, receive, send)
21
+
22
+ try:
23
+ if scope.get("path", "") not in {"/health", "/readiness"}:
24
+ client = scope["client"]
25
+ if client is None:
26
+ response = JSONResponse(
27
+ status_code=status.HTTP_400_BAD_REQUEST,
28
+ content={"detail": "Client IP could not be determined"},
29
+ )
30
+ await response(scope, receive, send)
31
+ return
32
+ client_ip.set(client[0])
33
+ request_id.set(str(ULID()))
34
+ await self.app(scope, receive, send)
35
+ finally:
36
+ for contextvar in REQUEST_CONTEXTVARS:
37
+ contextvar.set(None)
38
+
39
+
40
+ def setup_middleware(app: FastAPI, settings: Settings) -> None:
41
+ app.add_middleware(ContextMiddleware) # ty: ignore[invalid-argument-type]
42
+
43
+ app.add_middleware(
44
+ BrotliMiddleware, # ty: ignore[invalid-argument-type]
45
+ minimum_size=102_400, # Decrease this if no longer using cloudflare
46
+ )
47
+
48
+ app.add_middleware(
49
+ CORSMiddleware, # ty: ignore[invalid-argument-type]
50
+ allow_credentials=settings.cors.allow_credentials,
51
+ allow_origins=settings.cors.allowed_origins,
52
+ allow_methods=settings.cors.allowed_methods,
53
+ allow_headers=settings.cors.allowed_headers,
54
+ )
55
+
56
+ app.add_middleware(
57
+ TrustedHostMiddleware, # ty: ignore[invalid-argument-type]
58
+ allowed_hosts=settings.app.allowed_hosts,
59
+ )
60
+
61
+ app.add_middleware(
62
+ ProxyHeadersMiddleware, # ty: ignore[invalid-argument-type]
63
+ trusted_hosts=["*"],
64
+ )
@@ -0,0 +1,36 @@
1
+ import logfire
2
+ import loguru
3
+ from fastapi import FastAPI
4
+
5
+ from core_framework.core.settings import Settings
6
+
7
+
8
+ def setup_observability(app: FastAPI, settings: Settings) -> None:
9
+ if settings.app.environment == "local":
10
+ return
11
+
12
+ if not settings.observability.enabled:
13
+ return
14
+
15
+ logfire.configure(
16
+ service_name="core_framework-api",
17
+ token=settings.observability.logfire_token,
18
+ min_level=settings.logging.level,
19
+ )
20
+
21
+ loguru.logger.configure(handlers=[logfire.loguru_handler()])
22
+
23
+ logfire.instrument_httpx()
24
+
25
+ logfire.instrument_asyncpg()
26
+
27
+ logfire.instrument_redis()
28
+
29
+ logfire.instrument_fastapi(
30
+ app=app,
31
+ excluded_urls=[
32
+ "/health",
33
+ "/readiness",
34
+ ],
35
+ capture_headers=False,
36
+ )