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,244 @@
1
+ from typing import Annotated, Any
2
+
3
+ from fastapi import APIRouter, Depends, Query, Request, status
4
+
5
+ from core_framework.api.dependencies import RequiredFirebaseUser, RequiredUserID, check_not_banned
6
+ from core_framework.api.users.authenticated.schemas import (
7
+ AccountResponse,
8
+ AccountUpdateRequest,
9
+ AppealRequest,
10
+ BlockedUserResponse,
11
+ MyAppealsResponse,
12
+ PreferencesResponse,
13
+ PreferencesUpdateRequest,
14
+ ProfileResponse,
15
+ ProfileUpdateRequest,
16
+ UserReportRequest,
17
+ )
18
+ from core_framework.api.users.shared.schemas import validate_user_id
19
+ from core_framework.application.users.authenticated_service import (
20
+ add_appeal,
21
+ add_user_report,
22
+ block_user,
23
+ cancel_account_deletion,
24
+ change_my_account,
25
+ change_my_preferences,
26
+ change_my_profile,
27
+ delete_my_current_appeal,
28
+ remove_user_report,
29
+ retrieve_my_account,
30
+ retrieve_my_appeals,
31
+ retrieve_my_blocked_users,
32
+ retrieve_my_preferences,
33
+ retrieve_my_profile,
34
+ schedule_account_deletion,
35
+ unblock_user,
36
+ )
37
+ from core_framework.core.pagination import (
38
+ PaginationCursorType,
39
+ PaginationResponse,
40
+ TokenCursorQueryParams,
41
+ paginate_cursor,
42
+ )
43
+ from core_framework.domains.moderation import AppealDecision
44
+
45
+ router = APIRouter(
46
+ prefix="/users",
47
+ tags=["users - authenticated"],
48
+ )
49
+
50
+
51
+ @router.get(
52
+ "/me/blocks",
53
+ response_model=PaginationResponse[BlockedUserResponse],
54
+ dependencies=[Depends(check_not_banned)],
55
+ )
56
+ async def get_my_blocked_users(
57
+ request: Request,
58
+ user_id: RequiredUserID,
59
+ pagination_params: Annotated[TokenCursorQueryParams, Depends()],
60
+ ) -> Any:
61
+ blocked_users = await retrieve_my_blocked_users(
62
+ user_id=user_id.root, created_at=pagination_params.cursor, limit=pagination_params.limit
63
+ )
64
+ return paginate_cursor(
65
+ request,
66
+ blocked_users,
67
+ pagination_params.limit,
68
+ PaginationCursorType.CREATED_AT,
69
+ retrieved=pagination_params.retrieved,
70
+ )
71
+
72
+
73
+ @router.get(
74
+ "/me/preferences",
75
+ response_model=PreferencesResponse,
76
+ dependencies=[Depends(check_not_banned)],
77
+ )
78
+ async def get_my_preferences(user_id: RequiredUserID) -> Any:
79
+ return await retrieve_my_preferences(user_id=user_id.root)
80
+
81
+
82
+ @router.patch(
83
+ "/me/preferences",
84
+ response_model=PreferencesResponse,
85
+ dependencies=[Depends(check_not_banned)],
86
+ )
87
+ async def patch_my_preferences(user_id: RequiredUserID, update_request: PreferencesUpdateRequest) -> Any:
88
+ validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
89
+ return await change_my_preferences(user_id=user_id.root, validated_update_request=validated_update_request)
90
+
91
+
92
+ @router.get(
93
+ "/me/profile",
94
+ response_model=ProfileResponse,
95
+ dependencies=[Depends(check_not_banned)],
96
+ )
97
+ async def get_my_profile(user_id: RequiredUserID) -> Any:
98
+ return await retrieve_my_profile(user_id=user_id.root)
99
+
100
+
101
+ @router.patch(
102
+ "/me/profile",
103
+ response_model=ProfileResponse,
104
+ dependencies=[Depends(check_not_banned)],
105
+ )
106
+ async def patch_my_profile(user_id: RequiredUserID, update_request: ProfileUpdateRequest) -> Any:
107
+ validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
108
+ return await change_my_profile(user_id=user_id.root, validated_update_request=validated_update_request)
109
+
110
+
111
+ @router.get(
112
+ "/me/account",
113
+ response_model=AccountResponse,
114
+ dependencies=[Depends(check_not_banned)],
115
+ )
116
+ async def get_my_account(firebase_user: RequiredFirebaseUser) -> Any:
117
+ return await retrieve_my_account(
118
+ user_id=firebase_user.user_id.root,
119
+ email=firebase_user.email,
120
+ email_verified=firebase_user.email_verified,
121
+ )
122
+
123
+
124
+ @router.patch(
125
+ "/me/account",
126
+ response_model=AccountResponse,
127
+ dependencies=[Depends(check_not_banned)],
128
+ )
129
+ async def patch_my_account(
130
+ firebase_user: RequiredFirebaseUser,
131
+ update_request: AccountUpdateRequest,
132
+ ) -> Any:
133
+ validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
134
+ await change_my_account(user_id=firebase_user.user_id.root, validated_update_request=validated_update_request)
135
+ return await retrieve_my_account(
136
+ user_id=firebase_user.user_id.root,
137
+ email=firebase_user.email,
138
+ email_verified=firebase_user.email_verified,
139
+ )
140
+
141
+
142
+ @router.put(
143
+ "/me/account/deletion",
144
+ status_code=status.HTTP_204_NO_CONTENT,
145
+ dependencies=[Depends(check_not_banned)],
146
+ )
147
+ async def put_my_account_deletion(user_id: RequiredUserID) -> None:
148
+ await schedule_account_deletion(user_id=user_id.root)
149
+
150
+
151
+ @router.delete(
152
+ "/me/account/deletion",
153
+ status_code=status.HTTP_204_NO_CONTENT,
154
+ dependencies=[Depends(check_not_banned)],
155
+ )
156
+ async def delete_my_account_deletion(user_id: RequiredUserID) -> None:
157
+ await cancel_account_deletion(user_id=user_id.root)
158
+
159
+
160
+ @router.get("/me/appeals", response_model=PaginationResponse[MyAppealsResponse])
161
+ async def get_my_appeals(
162
+ request: Request,
163
+ user_id: RequiredUserID,
164
+ pagination_params: Annotated[TokenCursorQueryParams, Depends()],
165
+ status: Annotated[AppealDecision | None, Query()] = None,
166
+ ) -> Any:
167
+ appeals = await retrieve_my_appeals(
168
+ user_id=user_id.root,
169
+ status=status,
170
+ cursor=pagination_params.cursor,
171
+ limit=pagination_params.limit,
172
+ )
173
+ return paginate_cursor(
174
+ request,
175
+ appeals,
176
+ pagination_params.limit,
177
+ PaginationCursorType.UPDATED_AT,
178
+ retrieved=pagination_params.retrieved,
179
+ )
180
+
181
+
182
+ @router.post("/me/appeals", status_code=status.HTTP_204_NO_CONTENT)
183
+ async def post_my_appeals(user_id: RequiredUserID, appeal_request: AppealRequest) -> None:
184
+ await add_appeal(user_id=user_id.root, justification=appeal_request.justification)
185
+
186
+
187
+ @router.delete("/me/appeals", status_code=status.HTTP_204_NO_CONTENT)
188
+ async def delete_my_appeal(user_id: RequiredUserID) -> None:
189
+ await delete_my_current_appeal(user_id=user_id.root)
190
+
191
+
192
+ @router.put(
193
+ "/{user_id}/block",
194
+ status_code=status.HTTP_204_NO_CONTENT,
195
+ dependencies=[Depends(check_not_banned)],
196
+ )
197
+ async def put_block_user(requester_id: RequiredUserID, user_id: str) -> None:
198
+ validated_target_user_id = validate_user_id(user_id=user_id)
199
+ await block_user(user_id=requester_id.root, target_user_id=validated_target_user_id.root)
200
+
201
+
202
+ @router.delete(
203
+ "/{user_id}/block",
204
+ status_code=status.HTTP_204_NO_CONTENT,
205
+ dependencies=[Depends(check_not_banned)],
206
+ )
207
+ async def delete_block_user(requester_id: RequiredUserID, user_id: str) -> None:
208
+ validated_target_user_id = validate_user_id(user_id=user_id)
209
+ await unblock_user(user_id=requester_id.root, target_user_id=validated_target_user_id.root)
210
+
211
+
212
+ @router.post(
213
+ "/{user_id}/report",
214
+ status_code=status.HTTP_204_NO_CONTENT,
215
+ dependencies=[Depends(check_not_banned)],
216
+ )
217
+ async def post_user_report(
218
+ requester_id: RequiredUserID,
219
+ user_id: str,
220
+ report_request: UserReportRequest,
221
+ ) -> None:
222
+ validated_target_user_id = validate_user_id(user_id=user_id)
223
+ await add_user_report(
224
+ reporter_id=requester_id.root,
225
+ target_id=validated_target_user_id.root,
226
+ category=report_request.category,
227
+ reason=report_request.reason,
228
+ )
229
+
230
+
231
+ @router.delete(
232
+ "/{user_id}/report",
233
+ status_code=status.HTTP_204_NO_CONTENT,
234
+ dependencies=[Depends(check_not_banned)],
235
+ )
236
+ async def delete_user_report(
237
+ requester_id: RequiredUserID,
238
+ user_id: str,
239
+ ) -> None:
240
+ validated_target_user_id = validate_user_id(user_id=user_id)
241
+ await remove_user_report(
242
+ reporter_id=requester_id.root,
243
+ target_id=validated_target_user_id.root,
244
+ )
@@ -0,0 +1,81 @@
1
+ from datetime import datetime
2
+ from typing import Annotated
3
+
4
+ from pydantic import BaseModel, EmailStr, Field
5
+ from ulid import ULID
6
+
7
+ from core_framework.api.schemas import BasePatchRequest
8
+ from core_framework.api.users.shared.schemas import (
9
+ AvatarMixin,
10
+ BannerMixin,
11
+ Bio,
12
+ DisplayName,
13
+ UserID,
14
+ Username,
15
+ UserReference,
16
+ UserStatus,
17
+ )
18
+ from core_framework.domains.moderation import AppealDecision, ReportCategory
19
+ from core_framework.domains.user import ProfileVisibility, Theme
20
+
21
+
22
+ class BlockedUserResponse(BaseModel):
23
+ user_id: UserID
24
+ username: Username
25
+
26
+
27
+ class ProfileResponse(AvatarMixin, BannerMixin):
28
+ display_name: DisplayName | None
29
+ bio: Bio | None
30
+ status: UserStatus | None
31
+ social_links: dict[str, str]
32
+ profile_visibility: ProfileVisibility
33
+
34
+
35
+ class AccountResponse(BaseModel):
36
+ username: Username
37
+ email: EmailStr
38
+ email_verified: bool
39
+ deletion_scheduled_for: datetime | None
40
+
41
+
42
+ class AccountUpdateRequest(BasePatchRequest):
43
+ username: Username | None = None
44
+
45
+
46
+ class ProfileUpdateRequest(BasePatchRequest):
47
+ display_name: DisplayName | None = None
48
+ avatar_id: ULID | None = None
49
+ banner_id: ULID | None = None
50
+ bio: Bio | None = None
51
+ status: UserStatus | None = None
52
+ social_links: dict[str, str] | None = None
53
+ profile_visibility: ProfileVisibility | None = None
54
+
55
+
56
+ class UserReportRequest(BaseModel):
57
+ category: ReportCategory
58
+ reason: Annotated[str, Field(default="", max_length=250)]
59
+
60
+
61
+ class AppealRequest(BaseModel):
62
+ justification: Annotated[str, Field(..., min_length=50, max_length=1000)]
63
+
64
+
65
+ class MyAppealsResponse(BaseModel):
66
+ justification: str
67
+ reviewer: UserReference | None
68
+ decision_reason: str | None
69
+ status: AppealDecision
70
+ created_at: datetime
71
+ updated_at: datetime
72
+
73
+
74
+ class PreferencesResponse(BaseModel):
75
+ theme: Theme
76
+ language: str
77
+
78
+
79
+ class PreferencesUpdateRequest(BasePatchRequest):
80
+ theme: Theme | None = None
81
+ language: Annotated[str | None, Field(default=None, max_length=10)]
File without changes
@@ -0,0 +1,25 @@
1
+ from typing import Annotated, Any
2
+
3
+ from fastapi import APIRouter, Query
4
+ from fastapi.exceptions import RequestValidationError
5
+ from pydantic import ValidationError
6
+
7
+ from core_framework.api.users.public.schemas import UsernameExistsResponse
8
+ from core_framework.api.users.shared.schemas import Username
9
+ from core_framework.application.users.public_service import is_username_available
10
+
11
+ router = APIRouter(
12
+ prefix="/users",
13
+ tags=["users - public"],
14
+ )
15
+
16
+
17
+ @router.get("/usernames/exists", response_model=UsernameExistsResponse)
18
+ async def get_username_exists(username: Annotated[str, Query()]) -> Any:
19
+ try:
20
+ validated_username = Username(
21
+ root=username
22
+ ) # Note: Workaround for Pydantic's lack of support for Root Model in Query parameters
23
+ except ValidationError as e:
24
+ raise RequestValidationError(e.errors()) from e
25
+ return {"status": "available" if await is_username_available(username=validated_username.root) else "taken"}
@@ -0,0 +1,7 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class UsernameExistsResponse(BaseModel):
7
+ status: Literal["available", "taken"]
@@ -0,0 +1,9 @@
1
+ from fastapi import APIRouter
2
+
3
+ from core_framework.api.users.authenticated.router import router as authenticated_router
4
+ from core_framework.api.users.public.router import router as public_router
5
+
6
+ router = APIRouter()
7
+
8
+ router.include_router(public_router)
9
+ router.include_router(authenticated_router)
@@ -0,0 +1,174 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi.exceptions import RequestValidationError
4
+ from pydantic import (
5
+ BaseModel,
6
+ Field,
7
+ HttpUrl,
8
+ RootModel,
9
+ TypeAdapter,
10
+ ValidationError,
11
+ computed_field,
12
+ field_validator,
13
+ )
14
+
15
+ _http_url_adapter = TypeAdapter(HttpUrl)
16
+ _avatar_default_url: str | None = None
17
+ _avatar_base_url: str | None = None
18
+ _avatar_extension: str | None = None
19
+ _banner_default_url: str | None = None
20
+ _banner_base_url: str | None = None
21
+ _banner_extension: str | None = None
22
+
23
+
24
+ def configure_user_media_urls(
25
+ *,
26
+ avatar_default_url: str,
27
+ avatar_base_url: str,
28
+ avatar_extension: str,
29
+ banner_default_url: str,
30
+ banner_base_url: str,
31
+ banner_extension: str,
32
+ ) -> None:
33
+ global _avatar_default_url, _avatar_base_url, _avatar_extension
34
+ global _banner_default_url, _banner_base_url, _banner_extension
35
+ _avatar_default_url = avatar_default_url
36
+ _avatar_base_url = avatar_base_url.rstrip("/")
37
+ _avatar_extension = avatar_extension
38
+ _banner_default_url = banner_default_url
39
+ _banner_base_url = banner_base_url.rstrip("/")
40
+ _banner_extension = banner_extension
41
+
42
+
43
+ def _get_user_media_urls() -> dict[str, str]:
44
+ if (
45
+ _avatar_default_url is not None
46
+ and _avatar_base_url is not None
47
+ and _avatar_extension is not None
48
+ and _banner_default_url is not None
49
+ and _banner_base_url is not None
50
+ and _banner_extension is not None
51
+ ):
52
+ return {
53
+ "avatar_default_url": _avatar_default_url,
54
+ "avatar_base_url": _avatar_base_url,
55
+ "avatar_extension": _avatar_extension,
56
+ "banner_default_url": _banner_default_url,
57
+ "banner_base_url": _banner_base_url,
58
+ "banner_extension": _banner_extension,
59
+ }
60
+ msg = "User media URLs are not configured. Call configure_user_media_urls() during application bootstrap."
61
+ raise RuntimeError(msg)
62
+
63
+
64
+ class UserID(RootModel[str]):
65
+ root: Annotated[
66
+ str,
67
+ Field(
68
+ description="The ID of the user",
69
+ min_length=1,
70
+ max_length=128,
71
+ ),
72
+ ]
73
+
74
+
75
+ class Username(RootModel[str]):
76
+ root: Annotated[
77
+ str,
78
+ Field(
79
+ description="The username of the user",
80
+ min_length=1,
81
+ max_length=50,
82
+ ),
83
+ ]
84
+
85
+ @field_validator("root", mode="after")
86
+ @classmethod
87
+ def no_whitespace(cls, v: str) -> str:
88
+ if any(ch.isspace() for ch in v):
89
+ raise ValueError("Username must not contain whitespace")
90
+ return v
91
+
92
+
93
+ class Bio(RootModel[str]):
94
+ root: Annotated[
95
+ str,
96
+ Field(
97
+ description="User profile bio",
98
+ max_length=500,
99
+ ),
100
+ ]
101
+
102
+
103
+ class UserStatus(RootModel[str]):
104
+ root: Annotated[
105
+ str,
106
+ Field(
107
+ description="User status (e.g. away, online)",
108
+ max_length=150,
109
+ ),
110
+ ]
111
+
112
+
113
+ class DisplayName(RootModel[str]):
114
+ root: Annotated[
115
+ str,
116
+ Field(
117
+ description="The display name of the user",
118
+ min_length=1,
119
+ max_length=100,
120
+ ),
121
+ ]
122
+
123
+
124
+ class AvatarMixin(BaseModel):
125
+ avatar_id: Annotated[str | None, Field(exclude=True)]
126
+
127
+ @computed_field
128
+ @property
129
+ def avatar(self) -> HttpUrl:
130
+ url_string = _construct_avatar_url(avatar_id=self.avatar_id)
131
+ return _http_url_adapter.validate_python(url_string)
132
+
133
+
134
+ class BannerMixin(BaseModel):
135
+ banner_id: Annotated[str | None, Field(exclude=True)]
136
+
137
+ @computed_field
138
+ @property
139
+ def banner(self) -> HttpUrl:
140
+ url_string = _construct_banner_url(banner_id=self.banner_id)
141
+ return _http_url_adapter.validate_python(url_string)
142
+
143
+
144
+ class UserReference(BaseModel):
145
+ user_id: UserID
146
+ username: Username
147
+ display_name: DisplayName | None = None
148
+
149
+
150
+ def validate_user_id(*, user_id: str) -> UserID:
151
+ try:
152
+ return UserID(root=user_id) # Note: Workaround for Pydantic's lack of support for Root Model in Path parameters
153
+ except ValidationError as e:
154
+ raise RequestValidationError(e.errors()) from e
155
+
156
+
157
+ def validate_optional_user_id(*, user_id: str | None) -> UserID | None:
158
+ if user_id is None:
159
+ return None
160
+ return validate_user_id(user_id=user_id)
161
+
162
+
163
+ def _construct_avatar_url(*, avatar_id: str | None) -> str:
164
+ media_urls = _get_user_media_urls()
165
+ if avatar_id is None:
166
+ return media_urls["avatar_default_url"]
167
+ return f"{media_urls['avatar_base_url']}/{avatar_id}.{media_urls['avatar_extension']}"
168
+
169
+
170
+ def _construct_banner_url(*, banner_id: str | None) -> str:
171
+ media_urls = _get_user_media_urls()
172
+ if banner_id is None:
173
+ return media_urls["banner_default_url"]
174
+ return f"{media_urls['banner_base_url']}/{banner_id}.{media_urls['banner_extension']}"
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+
3
+ import core_framework.domains.moderation.dependencies as moderation_deps
4
+ import core_framework.domains.user.dependencies as user_deps
5
+ from core_framework.application.auth.models import UserDetail
6
+ from core_framework.application.shared.enums import RedisKeys
7
+ from core_framework.core.cache import cache
8
+ from core_framework.domains.user import UserIdentity
9
+
10
+
11
+ @cache(
12
+ resource_name=RedisKeys.USER_DETAIL,
13
+ ttl=86_400, # 1 day
14
+ skip_if=lambda r: r.identity == UserIdentity.DEFAULT,
15
+ )
16
+ async def get_user_detail(*, user_id: str) -> UserDetail:
17
+ async with asyncio.TaskGroup() as tg:
18
+ identity_task = tg.create_task(user_deps.user_service.retrieve_user_identity_mapping(user_ids={user_id}))
19
+ moderation_task = tg.create_task(
20
+ moderation_deps.moderation_service.retrieve_user_moderation_mapping(user_ids={user_id})
21
+ )
22
+
23
+ return UserDetail(
24
+ identity=identity_task.result()[user_id],
25
+ moderation=moderation_task.result()[user_id],
26
+ )
@@ -0,0 +1,10 @@
1
+ import core_framework.core as core_fw
2
+ import core_framework.domains.user.dependencies as user_deps
3
+ from core_framework.application.shared.enums import RedisKeys
4
+ from core_framework.domains.user import CreatedUser
5
+
6
+
7
+ async def register_user(*, user_id: str) -> CreatedUser:
8
+ created_user = await user_deps.user_service.add_user(user_id=user_id)
9
+ await core_fw.redis_cache.sadd(RedisKeys.TAKEN_USERNAMES, created_user.username)
10
+ return created_user
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+ from core_framework.domains.moderation import UserModeration
4
+ from core_framework.domains.user import UserIdentity
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, kw_only=True)
8
+ class UserDetail:
9
+ identity: UserIdentity
10
+ moderation: UserModeration
@@ -0,0 +1,19 @@
1
+ from core_framework.core.cache import configure_cache
2
+ from core_framework.core.runtime import CoreRuntime
3
+ from core_framework.domains.comment.dependencies import configure_comment_dependencies
4
+ from core_framework.domains.moderation.dependencies import configure_moderation_dependencies
5
+ from core_framework.domains.post.dependencies import configure_post_dependencies
6
+ from core_framework.domains.user.dependencies import configure_user_dependencies
7
+
8
+
9
+ def configure_application_dependencies(runtime: CoreRuntime) -> None:
10
+ configure_cache(redis_cache=runtime.redis_cache)
11
+ configure_comment_dependencies(runtime)
12
+ configure_moderation_dependencies(runtime)
13
+ configure_post_dependencies(runtime)
14
+ configure_user_dependencies(runtime)
15
+
16
+ # alru_cache on user id set must not survive a rebound UserService instance
17
+ from core_framework.application.users.admin_service import retrieve_admin_user_ids
18
+
19
+ retrieve_admin_user_ids.cache_clear()