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,257 @@
1
+ from collections import defaultdict
2
+ from contextlib import suppress
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import Any, Final
5
+
6
+ from loguru import logger
7
+
8
+ from core_framework.domains.user.constants import REDACTED_AUTHOR_ID, SYSTEM_USER_ID
9
+ from core_framework.domains.user.enums import UserRole
10
+ from core_framework.domains.user.exceptions import (
11
+ DomainUserNotFoundException,
12
+ SelfBlockException,
13
+ UserCreationException,
14
+ UserIdConflictException,
15
+ UsernameConflictException,
16
+ )
17
+ from core_framework.domains.user.models import (
18
+ BlockedUser,
19
+ CreatedUser,
20
+ Preferences,
21
+ Profile,
22
+ UserChangeHistory,
23
+ UserIdentity,
24
+ UserWithProfile,
25
+ )
26
+ from core_framework.domains.user.repository import UserRepository
27
+ from core_framework.domains.user.utils import generate_random_username
28
+
29
+
30
+ class UserService:
31
+ CREATE_USER_MAX_ATTEMPTS: Final[int] = 10
32
+ ACCOUNT_DELETION_GRACE_DAYS: Final[int] = 7
33
+
34
+ def __init__(self, repository: UserRepository):
35
+ self.repository = repository
36
+
37
+ # User Management
38
+ async def retrieve_admin_user_ids(self) -> set[str]:
39
+ return await self.repository.select_admin_user_ids()
40
+
41
+ async def add_user(self, *, user_id: str) -> CreatedUser:
42
+ for attempt in range(self.CREATE_USER_MAX_ATTEMPTS):
43
+ try:
44
+ username = generate_random_username()
45
+ await self.repository.insert_user(
46
+ user_id=user_id,
47
+ username=username,
48
+ role=UserRole.MEMBER.value,
49
+ )
50
+ except UsernameConflictException:
51
+ logger.warning("Username conflict occurred while creating user")
52
+ if attempt == self.CREATE_USER_MAX_ATTEMPTS - 1:
53
+ raise
54
+ except UserIdConflictException, UserCreationException:
55
+ raise
56
+ except Exception as e:
57
+ logger.error(f"Unknown error occurred while creating user {user_id=}: {e}")
58
+ raise UserCreationException(user_id) from e
59
+ else:
60
+ return CreatedUser(
61
+ user_id=user_id,
62
+ username=username,
63
+ )
64
+ raise AssertionError(
65
+ "Unreachable: loop should always return or raise"
66
+ ) # TODO: Remove this assertion when ty supports exhaustive type checking
67
+
68
+ async def remove_user(self, *, user_id: str) -> None:
69
+ await self.repository.delete_user(user_id=user_id)
70
+
71
+ # Login/Authentication
72
+ async def add_login_event(self, *, user_id: str, request_context: dict[str, str]) -> None:
73
+ try:
74
+ await self.repository.insert_login_event(
75
+ user_id=user_id,
76
+ request_context=request_context,
77
+ )
78
+ except DomainUserNotFoundException:
79
+ pass
80
+ except Exception as e:
81
+ logger.error(f"Unknown error occurred while logging in user {user_id=}: {e}")
82
+ return
83
+ else:
84
+ return
85
+
86
+ with suppress(UserIdConflictException):
87
+ await self.add_user(user_id=user_id)
88
+
89
+ try:
90
+ await self.repository.insert_login_event(
91
+ user_id=user_id,
92
+ request_context=request_context,
93
+ )
94
+ except Exception as e:
95
+ logger.error(f"Unknown error occurred while logging in user {user_id=}: {e}")
96
+
97
+ # User Blocks
98
+ async def retrieve_blocked_users(self, *, blocker_id: str, created_at: datetime, limit: int) -> list[BlockedUser]:
99
+ return await self.repository.select_blocked_users(blocker_id=blocker_id, created_at=created_at, limit=limit)
100
+
101
+ async def block_user(self, *, user_id: str, target_user_id: str) -> None:
102
+ if user_id == target_user_id:
103
+ raise SelfBlockException()
104
+ await self.repository.insert_user_block(blocker_id=user_id, blocked_id=target_user_id)
105
+
106
+ async def unblock_user(self, *, user_id: str, target_user_id: str) -> None:
107
+ await self.repository.delete_user_block(blocker_id=user_id, blocked_id=target_user_id)
108
+
109
+ # Preferences
110
+ async def retrieve_preferences(self, *, user_id: str) -> Preferences:
111
+ try:
112
+ return await self.repository.select_preferences(user_id=user_id)
113
+ except DomainUserNotFoundException:
114
+ return Preferences.DEFAULT
115
+
116
+ async def retrieve_preferences_strong(self, *, user_id: str) -> Preferences:
117
+ try:
118
+ return await self.repository.select_preferences_strong(user_id=user_id)
119
+ except DomainUserNotFoundException:
120
+ return Preferences.DEFAULT
121
+
122
+ async def change_preferences(self, *, user_id: str, validated_update_request: dict[str, Any]) -> None:
123
+ await self.repository.update_preferences(
124
+ user_id=user_id,
125
+ validated_update_request=validated_update_request,
126
+ )
127
+
128
+ # Profile
129
+ async def retrieve_profile(self, *, user_id: str) -> Profile:
130
+ try:
131
+ return await self.repository.select_profile(user_id=user_id)
132
+ except DomainUserNotFoundException:
133
+ return Profile.DEFAULT
134
+
135
+ async def retrieve_profile_strong(self, *, user_id: str) -> Profile:
136
+ try:
137
+ return await self.repository.select_profile_strong(user_id=user_id)
138
+ except DomainUserNotFoundException:
139
+ return Profile.DEFAULT
140
+
141
+ async def retrieve_profile_optional(self, *, user_id: str) -> Profile | None:
142
+ try:
143
+ return await self.repository.select_profile(user_id=user_id)
144
+ except DomainUserNotFoundException:
145
+ return None
146
+
147
+ async def retrieve_profile_optional_strong(self, *, user_id: str) -> Profile | None:
148
+ try:
149
+ return await self.repository.select_profile_strong(user_id=user_id)
150
+ except DomainUserNotFoundException:
151
+ return None
152
+
153
+ async def change_profile(
154
+ self,
155
+ *,
156
+ actor_id: str,
157
+ user_id: str,
158
+ validated_update_request: dict[str, Any],
159
+ ) -> None:
160
+ await self.repository.update_profile(
161
+ user_id=user_id,
162
+ validated_update_request=validated_update_request,
163
+ actor_id=actor_id,
164
+ )
165
+
166
+ async def change_account(
167
+ self,
168
+ *,
169
+ actor_id: str,
170
+ user_id: str,
171
+ validated_update_request: dict[str, Any],
172
+ ) -> None:
173
+ await self.repository.update_account(
174
+ user_id=user_id,
175
+ validated_update_request=validated_update_request,
176
+ actor_id=actor_id,
177
+ )
178
+
179
+ async def change_user_role(
180
+ self,
181
+ *,
182
+ actor_id: str,
183
+ user_id: str,
184
+ role: str,
185
+ ) -> None:
186
+ await self.repository.update_user_role(
187
+ user_id=user_id,
188
+ role=role,
189
+ actor_id=actor_id,
190
+ )
191
+
192
+ # Account Deletion
193
+ async def schedule_account_deletion(self, *, user_id: str) -> None:
194
+ scheduled_for = datetime.now(timezone.utc) + timedelta(days=self.ACCOUNT_DELETION_GRACE_DAYS)
195
+ await self.repository.insert_user_deletion(
196
+ user_id=user_id,
197
+ scheduled_for=scheduled_for,
198
+ actor_id=user_id,
199
+ )
200
+
201
+ async def cancel_account_deletion(self, *, user_id: str) -> None:
202
+ await self.repository.delete_user_deletion(user_id=user_id, actor_id=user_id)
203
+
204
+ async def retrieve_user_deletion_scheduled_for(self, *, user_id: str) -> datetime | None:
205
+ return await self.repository.select_user_deletion_scheduled_for(user_id=user_id)
206
+
207
+ async def retrieve_expired_user_deletions(self) -> list[str]:
208
+ return await self.repository.select_expired_user_deletions()
209
+
210
+ # User Identity
211
+ async def retrieve_user_for_detail(self, *, user_id: str) -> UserWithProfile | None:
212
+ return await self.repository.select_user_for_detail(user_id=user_id)
213
+
214
+ async def retrieve_user_identity(self, *, user_id: str) -> UserIdentity:
215
+ mapping = await self.retrieve_user_identity_mapping(user_ids={user_id})
216
+ return mapping[user_id]
217
+
218
+ async def retrieve_user_identity_strong(self, *, user_id: str) -> UserIdentity:
219
+ mapping = await self.retrieve_user_identity_mapping_strong(user_ids={user_id})
220
+ return mapping[user_id]
221
+
222
+ async def retrieve_user_identity_mapping(self, *, user_ids: set[str]) -> defaultdict[str, UserIdentity]:
223
+ db_user_ids = user_ids - {SYSTEM_USER_ID, REDACTED_AUTHOR_ID}
224
+ user_identity_mapping = await self.repository.select_user_identity_mapping(user_ids=db_user_ids)
225
+ if SYSTEM_USER_ID in user_ids:
226
+ user_identity_mapping[SYSTEM_USER_ID] = UserIdentity.SYSTEM
227
+ if REDACTED_AUTHOR_ID in user_ids:
228
+ user_identity_mapping[REDACTED_AUTHOR_ID] = UserIdentity.REDACTED_AUTHOR
229
+ return defaultdict(lambda: UserIdentity.DEFAULT, user_identity_mapping)
230
+
231
+ async def retrieve_user_identity_mapping_strong(self, *, user_ids: set[str]) -> defaultdict[str, UserIdentity]:
232
+ db_user_ids = user_ids - {SYSTEM_USER_ID, REDACTED_AUTHOR_ID}
233
+ user_identity_mapping = await self.repository.select_user_identity_mapping_strong(user_ids=db_user_ids)
234
+ if SYSTEM_USER_ID in user_ids:
235
+ user_identity_mapping[SYSTEM_USER_ID] = UserIdentity.SYSTEM
236
+ if REDACTED_AUTHOR_ID in user_ids:
237
+ user_identity_mapping[REDACTED_AUTHOR_ID] = UserIdentity.REDACTED_AUTHOR
238
+ return defaultdict(lambda: UserIdentity.DEFAULT, user_identity_mapping)
239
+
240
+ async def retrieve_user_identities(
241
+ self,
242
+ *,
243
+ username: str | None,
244
+ role: UserRole | None,
245
+ cursor: datetime,
246
+ limit: int,
247
+ ) -> list[UserIdentity]:
248
+ return await self.repository.select_user_identities(username=username, role=role, cursor=cursor, limit=limit)
249
+
250
+ async def retrieve_user_change_history(
251
+ self,
252
+ *,
253
+ user_id: str,
254
+ cursor: datetime,
255
+ limit: int,
256
+ ) -> list[UserChangeHistory]:
257
+ return await self.repository.select_user_change_history(user_id=user_id, cursor=cursor, limit=limit)
@@ -0,0 +1,182 @@
1
+ import secrets
2
+ from typing import Final
3
+
4
+ ADJECTIVES: Final[tuple[str, ...]] = (
5
+ "swift",
6
+ "bold",
7
+ "bright",
8
+ "calm",
9
+ "clever",
10
+ "cosmic",
11
+ "crisp",
12
+ "daring",
13
+ "eager",
14
+ "fierce",
15
+ "gentle",
16
+ "happy",
17
+ "jolly",
18
+ "keen",
19
+ "lively",
20
+ "lucky",
21
+ "mellow",
22
+ "mighty",
23
+ "nimble",
24
+ "noble",
25
+ "proud",
26
+ "quick",
27
+ "quiet",
28
+ "rapid",
29
+ "sharp",
30
+ "silent",
31
+ "smooth",
32
+ "sneaky",
33
+ "stellar",
34
+ "sunny",
35
+ "wise",
36
+ "brave",
37
+ "cool",
38
+ "curious",
39
+ "dynamic",
40
+ "epic",
41
+ "fearless",
42
+ "fresh",
43
+ "glowing",
44
+ "grand",
45
+ "humble",
46
+ "iconic",
47
+ "jovial",
48
+ "kind",
49
+ "legendary",
50
+ "magnetic",
51
+ "neat",
52
+ "optimistic",
53
+ "patient",
54
+ "playful",
55
+ "polished",
56
+ "radiant",
57
+ "relaxed",
58
+ "resilient",
59
+ "robust",
60
+ "savvy",
61
+ "serene",
62
+ "skillful",
63
+ "steady",
64
+ "strong",
65
+ "tactful",
66
+ "vibrant",
67
+ "witty",
68
+ "alert",
69
+ "brilliant",
70
+ "chill",
71
+ "confident",
72
+ "creative",
73
+ "decisive",
74
+ "efficient",
75
+ "energetic",
76
+ "focused",
77
+ "graceful",
78
+ "intrepid",
79
+ "loyal",
80
+ "modest",
81
+ "motivated",
82
+ "peaceful",
83
+ "reliable",
84
+ "resourceful",
85
+ "spirited",
86
+ "thoughtful",
87
+ "upbeat",
88
+ "versatile",
89
+ "watchful",
90
+ )
91
+
92
+ NOUNS: Final[tuple[str, ...]] = (
93
+ "badger",
94
+ "bear",
95
+ "cobra",
96
+ "dragon",
97
+ "eagle",
98
+ "falcon",
99
+ "fox",
100
+ "hawk",
101
+ "jaguar",
102
+ "lion",
103
+ "lynx",
104
+ "otter",
105
+ "owl",
106
+ "panther",
107
+ "phoenix",
108
+ "raven",
109
+ "shark",
110
+ "tiger",
111
+ "viper",
112
+ "wolf",
113
+ "knight",
114
+ "ninja",
115
+ "pilot",
116
+ "ranger",
117
+ "sage",
118
+ "scout",
119
+ "warrior",
120
+ "guardian",
121
+ "voyager",
122
+ "wanderer",
123
+ "seeker",
124
+ "nomad",
125
+ "captain",
126
+ "sentinel",
127
+ "champion",
128
+ "comet",
129
+ "meteor",
130
+ "orbit",
131
+ "nebula",
132
+ "nova",
133
+ "cosmos",
134
+ "galaxy",
135
+ "eclipse",
136
+ "zenith",
137
+ "horizon",
138
+ "storm",
139
+ "thunder",
140
+ "flame",
141
+ "frost",
142
+ "ember",
143
+ "shadow",
144
+ "spark",
145
+ "breeze",
146
+ "current",
147
+ "summit",
148
+ "forge",
149
+ "anvil",
150
+ "beacon",
151
+ "compass",
152
+ "signal",
153
+ "anchor",
154
+ "harbor",
155
+ "citadel",
156
+ "outpost",
157
+ "stronghold",
158
+ "falconer",
159
+ "pathfinder",
160
+ "trailblazer",
161
+ "wayfinder",
162
+ "architect",
163
+ "builder",
164
+ "maker",
165
+ "engineer",
166
+ "echo",
167
+ "pulse",
168
+ "rhythm",
169
+ "vector",
170
+ "vertex",
171
+ "cipher",
172
+ "matrix",
173
+ )
174
+
175
+ MAX_NUMBER: Final[int] = 9999
176
+
177
+
178
+ def generate_random_username() -> str:
179
+ adjective = secrets.choice(ADJECTIVES)
180
+ noun = secrets.choice(NOUNS)
181
+ number = secrets.randbelow(MAX_NUMBER) + 1
182
+ return f"{adjective.capitalize()}{noun.capitalize()}{number}"
core_framework/main.py ADDED
@@ -0,0 +1,104 @@
1
+ import os
2
+ from collections.abc import AsyncGenerator
3
+ from contextlib import asynccontextmanager
4
+
5
+ from fastapi import FastAPI
6
+ from firebase_admin import credentials, initialize_app
7
+
8
+ from core_framework.api.users.shared.schemas import configure_user_media_urls
9
+ from core_framework.application.bootstrap import configure_application_dependencies
10
+ from core_framework.application.events.event_token import configure_event_token
11
+ from core_framework.core import configure_core_runtime
12
+ from core_framework.core.exception_handlers import setup_exception_handlers
13
+ from core_framework.core.logging import setup_logging
14
+ from core_framework.core.middleware import setup_middleware
15
+ from core_framework.core.observability import setup_observability
16
+ from core_framework.core.pagination import configure_pagination
17
+ from core_framework.core.runtime import CoreRuntime, build_core_runtime
18
+ from core_framework.core.settings import Settings, load_default_settings
19
+
20
+
21
+ def create_lifespan(runtime: CoreRuntime):
22
+ @asynccontextmanager
23
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
24
+ cred = None
25
+ options = {"projectId": "test"}
26
+ if not os.getenv("FIREBASE_AUTH_EMULATOR_HOST"):
27
+ cred = credentials.Certificate(runtime.settings.PROJECT_ROOT / "firebase_config.json")
28
+ options = None
29
+ initialize_app(cred, options)
30
+
31
+ async with (
32
+ runtime.write_postgres,
33
+ runtime.read_postgres,
34
+ runtime.redis_queue,
35
+ runtime.redis_cache,
36
+ runtime.general_http_client,
37
+ ):
38
+ yield
39
+
40
+ return lifespan
41
+
42
+
43
+ def init_app(settings: Settings) -> FastAPI:
44
+ runtime = build_core_runtime(settings)
45
+
46
+ setup_logging(settings)
47
+ configure_core_runtime(runtime)
48
+ configure_event_token(secret_key=settings.app.secret_key)
49
+ configure_pagination(secret_key=settings.app.secret_key)
50
+ configure_application_dependencies(runtime)
51
+ configure_user_media_urls(
52
+ avatar_default_url=settings.avatar.default_url.unicode_string(),
53
+ avatar_base_url=settings.avatar.base_url.unicode_string(),
54
+ avatar_extension=settings.avatar.extension,
55
+ banner_default_url=settings.banner.default_url.unicode_string(),
56
+ banner_base_url=settings.banner.base_url.unicode_string(),
57
+ banner_extension=settings.banner.extension,
58
+ )
59
+
60
+ app = FastAPI(
61
+ lifespan=create_lifespan(runtime),
62
+ **settings.app.fastapi_kwargs,
63
+ )
64
+ app.state.core_runtime = runtime
65
+
66
+ setup_observability(app, settings)
67
+
68
+ setup_middleware(app, settings)
69
+
70
+ setup_exception_handlers(app)
71
+
72
+ from core_framework.api.router import router
73
+
74
+ app.include_router(router)
75
+
76
+ return app
77
+
78
+
79
+ app = init_app(load_default_settings())
80
+
81
+ if __name__ == "__main__":
82
+ import anyio
83
+ from anyio.to_thread import current_default_thread_limiter
84
+
85
+ async def monitor_thread_limiter():
86
+ limiter = current_default_thread_limiter()
87
+ threads_in_use = limiter.borrowed_tokens
88
+ while True:
89
+ if threads_in_use != limiter.borrowed_tokens:
90
+ print(f"Threads in use: {limiter.borrowed_tokens}")
91
+ threads_in_use = limiter.borrowed_tokens
92
+ await anyio.sleep(0)
93
+
94
+ import uvicorn
95
+
96
+ config = uvicorn.Config(app="core_framework.main:app")
97
+ server = uvicorn.Server(config)
98
+
99
+ async def test():
100
+ async with anyio.create_task_group() as tg:
101
+ tg.start_soon(monitor_thread_limiter)
102
+ await server.serve()
103
+
104
+ anyio.run(test)
File without changes
@@ -0,0 +1,56 @@
1
+ import os
2
+
3
+ from firebase_admin import credentials, get_app, initialize_app
4
+ from saq import Queue
5
+ from saq.types import Context
6
+
7
+ from core_framework.application.bootstrap import configure_application_dependencies
8
+ from core_framework.core import configure_core_runtime
9
+ from core_framework.core.runtime import build_core_runtime
10
+ from core_framework.core.settings import Settings, load_default_settings
11
+ from core_framework.worker.schedules import schedules
12
+ from core_framework.worker.tasks import tasks
13
+
14
+
15
+ def create_task_worker(settings: Settings) -> dict[str, object]:
16
+ runtime = build_core_runtime(settings)
17
+ configure_core_runtime(runtime)
18
+ configure_application_dependencies(runtime)
19
+
20
+ queue = Queue.from_url(settings.redis.redis_queue_url)
21
+
22
+ async def startup(ctx: Context) -> None:
23
+ ctx["queue"] = queue
24
+ try:
25
+ get_app()
26
+ except ValueError:
27
+ cred = None
28
+ options = {"projectId": "test"}
29
+ if not os.getenv("FIREBASE_AUTH_EMULATOR_HOST"):
30
+ cred = credentials.Certificate(settings.PROJECT_ROOT / "firebase_config.json")
31
+ options = None
32
+ initialize_app(cred, options)
33
+ await runtime.write_postgres.connect()
34
+ await runtime.read_postgres.connect()
35
+ await runtime.redis_cache.connect()
36
+ await runtime.redis_queue.connect()
37
+ await runtime.general_http_client.connect()
38
+
39
+ async def shutdown(ctx: Context) -> None:
40
+ await runtime.write_postgres.disconnect()
41
+ await runtime.read_postgres.disconnect()
42
+ await runtime.redis_cache.disconnect()
43
+ await runtime.redis_queue.disconnect()
44
+ await runtime.general_http_client.disconnect()
45
+
46
+ return {
47
+ "queue": queue,
48
+ "functions": tasks,
49
+ "cron_jobs": schedules,
50
+ "startup": startup,
51
+ "shutdown": shutdown,
52
+ }
53
+
54
+
55
+ def task_worker() -> dict[str, object]:
56
+ return create_task_worker(load_default_settings())
@@ -0,0 +1,35 @@
1
+ from saq.job import CronJob
2
+
3
+ from core_framework.worker.schedules.schedule_aggregate_comment_stats import (
4
+ schedule_aggregate_comment_stats,
5
+ )
6
+ from core_framework.worker.schedules.schedule_aggregate_post_view_counts import (
7
+ schedule_aggregate_post_view_counts,
8
+ )
9
+ from core_framework.worker.schedules.schedule_expired_account_deletions import (
10
+ schedule_expired_account_deletions,
11
+ )
12
+ from core_framework.worker.schedules.schedule_expired_mute_lifts import schedule_expired_mute_lifts
13
+
14
+ schedules = [
15
+ CronJob(
16
+ function=schedule_expired_account_deletions,
17
+ cron="0 2 * * *", # Run daily at 2:00 AM UTC
18
+ unique=True, # Prevent overlapping runs
19
+ ),
20
+ CronJob(
21
+ function=schedule_expired_mute_lifts,
22
+ cron="0 * * * *", # Run hourly
23
+ unique=True, # Prevent overlapping runs
24
+ ),
25
+ CronJob(
26
+ function=schedule_aggregate_post_view_counts,
27
+ cron="*/15 * * * *", # Run every 15 minutes
28
+ unique=True, # Prevent overlapping runs
29
+ ),
30
+ CronJob(
31
+ function=schedule_aggregate_comment_stats,
32
+ cron="*/5 * * * *", # Run every 5 minutes
33
+ unique=True, # Prevent overlapping runs
34
+ ),
35
+ ]
@@ -0,0 +1,32 @@
1
+ from loguru import logger
2
+ from saq.types import Context
3
+
4
+ from core_framework.application.comments.aggregation_service import claim_comment_ids_dirty, mark_comment_stats_dirty
5
+
6
+
7
+ async def schedule_aggregate_comment_stats(ctx: Context) -> None:
8
+ try:
9
+ comment_ids = await claim_comment_ids_dirty()
10
+
11
+ if not comment_ids:
12
+ return
13
+
14
+ queue = ctx["queue"]
15
+
16
+ for comment_id in comment_ids:
17
+ try:
18
+ await queue.enqueue(
19
+ "process_aggregate_comment_stats",
20
+ comment_id=comment_id,
21
+ key=f"aggregate_comment_stats:{comment_id}",
22
+ )
23
+ except Exception as e:
24
+ logger.error(f"Failed to enqueue aggregate_comment_stats for comment_id={comment_id}: {e}")
25
+ try:
26
+ await mark_comment_stats_dirty(comment_id=comment_id)
27
+ except Exception as insert_exc:
28
+ logger.error(f"Failed to re-mark comment_id={comment_id} dirty after enqueue failure: {insert_exc}")
29
+
30
+ except Exception as e:
31
+ logger.error(f"Error in schedule_aggregate_comment_stats: {e}")
32
+ raise
@@ -0,0 +1,28 @@
1
+ from loguru import logger
2
+ from saq.types import Context
3
+
4
+ from core_framework.application.posts.aggregation_service import aggregate_post_view_counts
5
+
6
+
7
+ async def schedule_aggregate_post_view_counts(ctx: Context) -> None:
8
+ try:
9
+ post_ids = await aggregate_post_view_counts()
10
+
11
+ if not post_ids:
12
+ return
13
+
14
+ queue = ctx["queue"]
15
+
16
+ for post_id in post_ids:
17
+ try:
18
+ await queue.enqueue(
19
+ "process_aggregate_post_stats",
20
+ post_id=post_id,
21
+ key=f"aggregate_post_stats:{post_id}",
22
+ )
23
+ except Exception as e:
24
+ logger.error(f"Failed to enqueue aggregate_post_stats for post_id={post_id}: {e}")
25
+
26
+ except Exception as e:
27
+ logger.error(f"Error in schedule_aggregate_post_view_counts: {e}")
28
+ raise