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.
- core_framework/__init__.py +0 -0
- core_framework/alembic/comment/alembic/README +1 -0
- core_framework/alembic/comment/alembic/env.py +72 -0
- core_framework/alembic/comment/alembic/script.py.mako +28 -0
- core_framework/alembic/comment/alembic/versions/30334fd1347b_init.py +59 -0
- core_framework/alembic/comment/alembic/versions/a2b3c4d5e6f7_improve_comment_indexes.py +54 -0
- core_framework/alembic/comment/alembic/versions/bcc8e00cfc8b_add_extra_tables.py +64 -0
- core_framework/alembic/comment/alembic/versions/d1e2f3a4b5c6_add_comment_stats_dirty_table.py +29 -0
- core_framework/alembic/comment/alembic/versions/e3f4a5b6c7d8_cascade_delete_comment_descendants.py +49 -0
- core_framework/alembic/comment/alembic/versions/f7e6d5c4b3a2_comments_path_to_ltree.py +60 -0
- core_framework/alembic/comment/alembic.ini +52 -0
- core_framework/alembic/extension/alembic/README +1 -0
- core_framework/alembic/extension/alembic/env.py +98 -0
- core_framework/alembic/extension/alembic/script.py.mako +28 -0
- core_framework/alembic/extension/alembic/versions/0389226049cb_add_pg_trgm_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/5dc58b016cf5_add_citext_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/b0ba0d8a284e_add_pg_stat_statements_extension.py +25 -0
- core_framework/alembic/extension/alembic/versions/c9d0e1f2a3b4_add_ltree_extension.py +25 -0
- core_framework/alembic/extension/alembic.ini +147 -0
- core_framework/alembic/moderation/alembic/README +1 -0
- core_framework/alembic/moderation/alembic/env.py +98 -0
- core_framework/alembic/moderation/alembic/script.py.mako +28 -0
- core_framework/alembic/moderation/alembic/versions/085ba9021850_add_category_to_user_restrictions.py +93 -0
- core_framework/alembic/moderation/alembic/versions/5f9e4fc14a41_create_moderation_appeals_table.py +69 -0
- core_framework/alembic/moderation/alembic/versions/63e37381e73b_add_user_reports_table.py +33 -0
- core_framework/alembic/moderation/alembic/versions/6a2ae31b7ac6_add_moderation_actions_table.py +34 -0
- core_framework/alembic/moderation/alembic/versions/716aa1735c03_improve_indexes.py +36 -0
- core_framework/alembic/moderation/alembic/versions/7d243ddbfde1_add_post_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/8fba1f72dd46_add_indexes.py +64 -0
- core_framework/alembic/moderation/alembic/versions/95cc35a51984_update_restriction_history.py +91 -0
- core_framework/alembic/moderation/alembic/versions/9ad79d0af730_add_unique_constraint_user_reports_.py +28 -0
- core_framework/alembic/moderation/alembic/versions/a5e569f5df1a_create_user_restrictions_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/b2c3d4e5f6a7_add_indexes.py +42 -0
- core_framework/alembic/moderation/alembic/versions/c3d4e5f6a7b8_improve_report_indexes.py +48 -0
- core_framework/alembic/moderation/alembic/versions/d4af74643ff5_add_internal_notes_table.py +38 -0
- core_framework/alembic/moderation/alembic/versions/db20f2fb7390_add_comment_reports_table.py +35 -0
- core_framework/alembic/moderation/alembic/versions/e66226952ea6_add_report_category_to_user_reports_.py +54 -0
- core_framework/alembic/moderation/alembic/versions/f5e8cb275c30_enforce_1_pending_appeal.py +29 -0
- core_framework/alembic/moderation/alembic/versions/fe1faad2832d_create_restriction_history_table.py +69 -0
- core_framework/alembic/moderation/alembic.ini +147 -0
- core_framework/alembic/post/alembic/README +1 -0
- core_framework/alembic/post/alembic/env.py +97 -0
- core_framework/alembic/post/alembic/script.py.mako +28 -0
- core_framework/alembic/post/alembic/versions/51542673f5c8_add_tables_for_muted_banned_users.py +41 -0
- core_framework/alembic/post/alembic/versions/5beeeae40a4a_add_post_views_table.py +45 -0
- core_framework/alembic/post/alembic/versions/620971509a8b_init.py +55 -0
- core_framework/alembic/post/alembic/versions/a1b2c3d4e5f6_add_indexes.py +44 -0
- core_framework/alembic/post/alembic/versions/c1d2e3f4a5b6_add_post_hashtags_table.py +36 -0
- core_framework/alembic/post/alembic/versions/e56723f2afff_add_post_stats_table.py +39 -0
- core_framework/alembic/post/alembic/versions/fbc723ac58cc_add_post_likes_table.py +32 -0
- core_framework/alembic/post/alembic.ini +149 -0
- core_framework/alembic/user/alembic/README +1 -0
- core_framework/alembic/user/alembic/env.py +98 -0
- core_framework/alembic/user/alembic/script.py.mako +28 -0
- core_framework/alembic/user/alembic/versions/1a8bb99726ed_remove_avatar_id_from_users.py +81 -0
- core_framework/alembic/user/alembic/versions/2ccacf455941_improve_indexes.py +34 -0
- core_framework/alembic/user/alembic/versions/47f47ce2110e_create_user_deletions_table.py +31 -0
- core_framework/alembic/user/alembic/versions/5976db3f0175_drop_user_states.py +26 -0
- core_framework/alembic/user/alembic/versions/62417002cf32_add_indexes.py +46 -0
- core_framework/alembic/user/alembic/versions/6f7ccf3c226b_refactor_user_login_events.py +66 -0
- core_framework/alembic/user/alembic/versions/73432817015b_add_user_preferences_table.py +33 -0
- core_framework/alembic/user/alembic/versions/765bc01a7a59_create_user_blocks_table.py +33 -0
- core_framework/alembic/user/alembic/versions/7a56631f9927_create_user_login_events_table.py +49 -0
- core_framework/alembic/user/alembic/versions/831611e589bc_create_user_state.py +31 -0
- core_framework/alembic/user/alembic/versions/83c98ab2a779_add_user_profiles_table.py +88 -0
- core_framework/alembic/user/alembic/versions/8a94362cad6d_create_user_role.py +31 -0
- core_framework/alembic/user/alembic/versions/94b973923895_add_user_change_history_table.py +97 -0
- core_framework/alembic/user/alembic/versions/cbc0f4efe84f_add_avatar_id_column_to_users_table.py +31 -0
- core_framework/alembic/user/alembic/versions/d8b98ac6b073_add_index_for_get_admin_user_ids_query.py +29 -0
- core_framework/alembic/user/alembic/versions/ddb70cc09d16_create_user_states_table.py +34 -0
- core_framework/alembic/user/alembic/versions/f9ba10815ecd_add_users_table.py +33 -0
- core_framework/alembic/user/alembic.ini +147 -0
- core_framework/api/__init__.py +0 -0
- core_framework/api/admin/__init__.py +0 -0
- core_framework/api/admin/comments/router.py +69 -0
- core_framework/api/admin/comments/schemas.py +53 -0
- core_framework/api/admin/moderation/__init__.py +0 -0
- core_framework/api/admin/moderation/router.py +205 -0
- core_framework/api/admin/moderation/schemas.py +110 -0
- core_framework/api/admin/posts/router.py +62 -0
- core_framework/api/admin/posts/schemas.py +29 -0
- core_framework/api/admin/router.py +17 -0
- core_framework/api/admin/users/__init__.py +0 -0
- core_framework/api/admin/users/router.py +181 -0
- core_framework/api/admin/users/schemas.py +137 -0
- core_framework/api/auth/__init__.py +0 -0
- core_framework/api/auth/router.py +21 -0
- core_framework/api/auth/schemas.py +28 -0
- core_framework/api/comments/authenticated/router.py +126 -0
- core_framework/api/comments/authenticated/schemas.py +27 -0
- core_framework/api/comments/public/router.py +103 -0
- core_framework/api/comments/public/schemas.py +36 -0
- core_framework/api/comments/router.py +9 -0
- core_framework/api/comments/schemas.py +17 -0
- core_framework/api/dependencies.py +168 -0
- core_framework/api/events/router.py +39 -0
- core_framework/api/events/schemas.py +20 -0
- core_framework/api/posts/authenticated/router.py +83 -0
- core_framework/api/posts/authenticated/schemas.py +37 -0
- core_framework/api/posts/public/router.py +100 -0
- core_framework/api/posts/public/schemas.py +39 -0
- core_framework/api/posts/router.py +9 -0
- core_framework/api/posts/schemas.py +39 -0
- core_framework/api/router.py +19 -0
- core_framework/api/schemas.py +9 -0
- core_framework/api/system/__init__.py +0 -0
- core_framework/api/system/router.py +108 -0
- core_framework/api/users/__init__.py +0 -0
- core_framework/api/users/authenticated/__init__.py +0 -0
- core_framework/api/users/authenticated/router.py +244 -0
- core_framework/api/users/authenticated/schemas.py +81 -0
- core_framework/api/users/public/__init__.py +0 -0
- core_framework/api/users/public/router.py +25 -0
- core_framework/api/users/public/schemas.py +7 -0
- core_framework/api/users/router.py +9 -0
- core_framework/api/users/shared/schemas.py +174 -0
- core_framework/application/__init__.py +0 -0
- core_framework/application/auth/__init__.py +0 -0
- core_framework/application/auth/access_service.py +26 -0
- core_framework/application/auth/auth_service.py +10 -0
- core_framework/application/auth/models.py +10 -0
- core_framework/application/bootstrap.py +19 -0
- core_framework/application/comments/admin_service.py +236 -0
- core_framework/application/comments/aggregation_service.py +28 -0
- core_framework/application/comments/authenticated_service.py +89 -0
- core_framework/application/comments/public_service.py +218 -0
- core_framework/application/events/README.md +26 -0
- core_framework/application/events/event_service.py +51 -0
- core_framework/application/events/event_token.py +46 -0
- core_framework/application/events/models.py +9 -0
- core_framework/application/moderation/__init__.py +0 -0
- core_framework/application/moderation/appeal_service.py +98 -0
- core_framework/application/moderation/moderator_service.py +46 -0
- core_framework/application/moderation/report_service.py +180 -0
- core_framework/application/moderation/scheduled_service.py +5 -0
- core_framework/application/moderation/user_service.py +180 -0
- core_framework/application/posts/admin_service.py +104 -0
- core_framework/application/posts/aggregation_service.py +28 -0
- core_framework/application/posts/authenticated_service.py +72 -0
- core_framework/application/posts/public_service.py +197 -0
- core_framework/application/shared/__init__.py +0 -0
- core_framework/application/shared/enums.py +16 -0
- core_framework/application/shared/exceptions.py +16 -0
- core_framework/application/shared/user_agent.py +24 -0
- core_framework/application/users/__init__.py +0 -0
- core_framework/application/users/admin_service.py +298 -0
- core_framework/application/users/authenticated_service.py +179 -0
- core_framework/application/users/public_service.py +7 -0
- core_framework/application/users/scheduled_service.py +5 -0
- core_framework/bundled_alembic.py +57 -0
- core_framework/core/__init__.py +37 -0
- core_framework/core/cache.py +234 -0
- core_framework/core/context.py +14 -0
- core_framework/core/database.py +111 -0
- core_framework/core/exception_handlers/__init__.py +3 -0
- core_framework/core/exception_handlers/comment.py +99 -0
- core_framework/core/exception_handlers/common.py +5 -0
- core_framework/core/exception_handlers/moderation.py +104 -0
- core_framework/core/exception_handlers/post.py +54 -0
- core_framework/core/exception_handlers/setup.py +80 -0
- core_framework/core/exception_handlers/user.py +72 -0
- core_framework/core/http_client.py +64 -0
- core_framework/core/logging.py +99 -0
- core_framework/core/middleware.py +64 -0
- core_framework/core/observability.py +36 -0
- core_framework/core/pagination.py +203 -0
- core_framework/core/redis.py +135 -0
- core_framework/core/runtime.py +66 -0
- core_framework/core/settings.py +189 -0
- core_framework/domains/__init__.py +0 -0
- core_framework/domains/comment/README.md +243 -0
- core_framework/domains/comment/__init__.py +25 -0
- core_framework/domains/comment/constants.py +3 -0
- core_framework/domains/comment/dependencies.py +29 -0
- core_framework/domains/comment/enums.py +11 -0
- core_framework/domains/comment/exceptions.py +31 -0
- core_framework/domains/comment/models.py +54 -0
- core_framework/domains/comment/repository.py +947 -0
- core_framework/domains/comment/service.py +259 -0
- core_framework/domains/moderation/README.md +138 -0
- core_framework/domains/moderation/__init__.py +47 -0
- core_framework/domains/moderation/dependencies.py +29 -0
- core_framework/domains/moderation/enums.py +62 -0
- core_framework/domains/moderation/exceptions.py +31 -0
- core_framework/domains/moderation/models.py +94 -0
- core_framework/domains/moderation/repository.py +828 -0
- core_framework/domains/moderation/service.py +334 -0
- core_framework/domains/post/README.md +182 -0
- core_framework/domains/post/__init__.py +22 -0
- core_framework/domains/post/constants.py +3 -0
- core_framework/domains/post/dependencies.py +29 -0
- core_framework/domains/post/enums.py +18 -0
- core_framework/domains/post/exceptions.py +21 -0
- core_framework/domains/post/models.py +53 -0
- core_framework/domains/post/repository.py +791 -0
- core_framework/domains/post/service.py +204 -0
- core_framework/domains/user/README.md +74 -0
- core_framework/domains/user/__init__.py +39 -0
- core_framework/domains/user/constants.py +8 -0
- core_framework/domains/user/dependencies.py +29 -0
- core_framework/domains/user/enums.py +19 -0
- core_framework/domains/user/exceptions.py +31 -0
- core_framework/domains/user/models.py +124 -0
- core_framework/domains/user/repository.py +612 -0
- core_framework/domains/user/service.py +257 -0
- core_framework/domains/user/utils.py +182 -0
- core_framework/main.py +104 -0
- core_framework/worker/__init__.py +0 -0
- core_framework/worker/main.py +56 -0
- core_framework/worker/schedules/__init__.py +35 -0
- core_framework/worker/schedules/schedule_aggregate_comment_stats.py +32 -0
- core_framework/worker/schedules/schedule_aggregate_post_view_counts.py +28 -0
- core_framework/worker/schedules/schedule_expired_account_deletions.py +24 -0
- core_framework/worker/schedules/schedule_expired_mute_lifts.py +24 -0
- core_framework/worker/tasks/__init__.py +11 -0
- core_framework/worker/tasks/process_account_deletion.py +13 -0
- core_framework/worker/tasks/process_aggregate_comment_stats.py +19 -0
- core_framework/worker/tasks/process_aggregate_post_stats.py +12 -0
- core_framework/worker/tasks/process_mute_lift.py +13 -0
- core_framework-0.3.0.dist-info/METADATA +22 -0
- core_framework-0.3.0.dist-info/RECORD +222 -0
- 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
|