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,39 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, BackgroundTasks, status
|
|
4
|
+
|
|
5
|
+
from core_framework.api.dependencies import RequestContext
|
|
6
|
+
from core_framework.api.events.schemas import EventRequest, EventTokenResponse
|
|
7
|
+
from core_framework.application.events.event_service import EVENT_HANDLERS
|
|
8
|
+
from core_framework.application.events.event_token import generate_token, verify_token
|
|
9
|
+
from core_framework.application.events.models import Event
|
|
10
|
+
|
|
11
|
+
router = APIRouter(
|
|
12
|
+
prefix="/events",
|
|
13
|
+
tags=["events"],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/token", response_model=EventTokenResponse)
|
|
18
|
+
async def get_event_token() -> Any:
|
|
19
|
+
return {"token": generate_token()}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@router.post("", status_code=status.HTTP_202_ACCEPTED)
|
|
23
|
+
async def post_events(
|
|
24
|
+
request_body: EventRequest,
|
|
25
|
+
background_tasks: BackgroundTasks,
|
|
26
|
+
request_context: RequestContext,
|
|
27
|
+
) -> None:
|
|
28
|
+
if not verify_token(request_body.token, event_type=request_body.type):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
events = [Event(**e.model_dump()) for e in request_body.events]
|
|
32
|
+
handler = EVENT_HANDLERS.get(request_body.type)
|
|
33
|
+
if handler:
|
|
34
|
+
background_tasks.add_task(
|
|
35
|
+
handler,
|
|
36
|
+
events=events,
|
|
37
|
+
token=request_body.token,
|
|
38
|
+
request_context=request_context,
|
|
39
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from core_framework.application.shared.enums import EventType, SubjectType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventItem(BaseModel):
|
|
9
|
+
subject_type: SubjectType
|
|
10
|
+
subject: Annotated[str, Field(min_length=1, max_length=128)]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventRequest(BaseModel):
|
|
14
|
+
type: EventType
|
|
15
|
+
token: Annotated[str, Field(min_length=1)]
|
|
16
|
+
events: list[EventItem] = Field(default_factory=list, max_length=100)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EventTokenResponse(BaseModel):
|
|
20
|
+
token: str
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, status
|
|
2
|
+
from ulid import ULID
|
|
3
|
+
|
|
4
|
+
from core_framework.api.dependencies import RequiredUserID, check_not_banned
|
|
5
|
+
from core_framework.api.posts.authenticated.schemas import CreatePostRequest, PostReportRequest, UpdatePostRequest
|
|
6
|
+
from core_framework.application.posts.authenticated_service import (
|
|
7
|
+
add_post_report,
|
|
8
|
+
create_post,
|
|
9
|
+
delete_post,
|
|
10
|
+
edit_post,
|
|
11
|
+
like_post,
|
|
12
|
+
remove_post_report,
|
|
13
|
+
unlike_post,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
router = APIRouter(
|
|
17
|
+
prefix="/posts",
|
|
18
|
+
tags=["posts - authenticated"],
|
|
19
|
+
dependencies=[Depends(check_not_banned)],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
24
|
+
async def post_post(user_id: RequiredUserID, request_body: CreatePostRequest) -> None:
|
|
25
|
+
await create_post(
|
|
26
|
+
author_id=user_id.root,
|
|
27
|
+
content=request_body.content.root,
|
|
28
|
+
visibility=request_body.visibility,
|
|
29
|
+
hashtags=[h.root for h in request_body.hashtags],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.patch("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
34
|
+
async def patch_post(post_id: ULID, user_id: RequiredUserID, request_body: UpdatePostRequest) -> None:
|
|
35
|
+
validated_update_request = request_body.model_dump(mode="json", exclude_unset=True)
|
|
36
|
+
await edit_post(
|
|
37
|
+
post_id=str(post_id),
|
|
38
|
+
author_id=user_id.root,
|
|
39
|
+
validated_update_request=validated_update_request,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
44
|
+
async def delete_post_route(post_id: ULID, user_id: RequiredUserID) -> None:
|
|
45
|
+
await delete_post(
|
|
46
|
+
post_id=str(post_id),
|
|
47
|
+
author_id=user_id.root,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.put("/{post_id}/like", status_code=status.HTTP_204_NO_CONTENT)
|
|
52
|
+
async def put_post_like(post_id: ULID, user_id: RequiredUserID) -> None:
|
|
53
|
+
await like_post(post_id=str(post_id), user_id=user_id.root)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.delete("/{post_id}/like", status_code=status.HTTP_204_NO_CONTENT)
|
|
57
|
+
async def delete_post_like(post_id: ULID, user_id: RequiredUserID) -> None:
|
|
58
|
+
await unlike_post(post_id=str(post_id), user_id=user_id.root)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.post("/{post_id}/report", status_code=status.HTTP_204_NO_CONTENT)
|
|
62
|
+
async def post_post_report(
|
|
63
|
+
post_id: ULID,
|
|
64
|
+
user_id: RequiredUserID,
|
|
65
|
+
report_request: PostReportRequest,
|
|
66
|
+
) -> None:
|
|
67
|
+
await add_post_report(
|
|
68
|
+
reporter_id=user_id.root,
|
|
69
|
+
post_id=str(post_id),
|
|
70
|
+
category=report_request.category,
|
|
71
|
+
reason=report_request.reason,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.delete("/{post_id}/report", status_code=status.HTTP_204_NO_CONTENT)
|
|
76
|
+
async def delete_post_report(
|
|
77
|
+
post_id: ULID,
|
|
78
|
+
user_id: RequiredUserID,
|
|
79
|
+
) -> None:
|
|
80
|
+
await remove_post_report(
|
|
81
|
+
reporter_id=user_id.root,
|
|
82
|
+
post_id=str(post_id),
|
|
83
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
|
|
5
|
+
from core_framework.api.posts.schemas import MAX_HASHTAGS, Hashtag, PostContent
|
|
6
|
+
from core_framework.api.schemas import BasePatchRequest
|
|
7
|
+
from core_framework.domains.moderation import ReportCategory
|
|
8
|
+
from core_framework.domains.post import PostVisibility
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PostReportRequest(BaseModel):
|
|
12
|
+
category: ReportCategory
|
|
13
|
+
reason: Annotated[str, Field(default="", max_length=250)]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CreatePostRequest(BaseModel):
|
|
17
|
+
content: PostContent
|
|
18
|
+
visibility: PostVisibility = PostVisibility.PUBLIC
|
|
19
|
+
hashtags: list[Hashtag] = Field(default_factory=list, max_length=MAX_HASHTAGS)
|
|
20
|
+
|
|
21
|
+
@field_validator("hashtags", mode="after")
|
|
22
|
+
@classmethod
|
|
23
|
+
def deduplicate_hashtags(cls, v: list[Hashtag]) -> list[Hashtag]:
|
|
24
|
+
return list({h.root: h for h in v}.values())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UpdatePostRequest(BasePatchRequest):
|
|
28
|
+
content: PostContent | None = None
|
|
29
|
+
visibility: PostVisibility | None = None
|
|
30
|
+
hashtags: list[Hashtag] | None = None
|
|
31
|
+
|
|
32
|
+
@field_validator("hashtags", mode="after")
|
|
33
|
+
@classmethod
|
|
34
|
+
def deduplicate_hashtags(cls, v: list[Hashtag] | None) -> list[Hashtag] | None:
|
|
35
|
+
if v is None:
|
|
36
|
+
return None
|
|
37
|
+
return list({h.root: h for h in v}.values())
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
4
|
+
from ulid import ULID
|
|
5
|
+
|
|
6
|
+
from core_framework.api.dependencies import OptionalUserID
|
|
7
|
+
from core_framework.api.posts.public.schemas import PostResponse
|
|
8
|
+
from core_framework.api.posts.schemas import validate_hashtag
|
|
9
|
+
from core_framework.api.users.shared.schemas import validate_user_id
|
|
10
|
+
from core_framework.application.posts.public_service import (
|
|
11
|
+
retrieve_post_by_id,
|
|
12
|
+
retrieve_posts,
|
|
13
|
+
retrieve_posts_by_hashtag,
|
|
14
|
+
retrieve_posts_by_user_id,
|
|
15
|
+
)
|
|
16
|
+
from core_framework.core.pagination import (
|
|
17
|
+
PaginationCursorType,
|
|
18
|
+
PaginationResponse,
|
|
19
|
+
TokenCursorQueryParams,
|
|
20
|
+
paginate_cursor,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
router = APIRouter(
|
|
24
|
+
prefix="/posts",
|
|
25
|
+
tags=["posts - public"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("", response_model=PaginationResponse[PostResponse])
|
|
30
|
+
async def get_posts(
|
|
31
|
+
request: Request,
|
|
32
|
+
viewer_id: OptionalUserID,
|
|
33
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
34
|
+
) -> Any:
|
|
35
|
+
posts = await retrieve_posts(
|
|
36
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
37
|
+
cursor=pagination_params.cursor,
|
|
38
|
+
limit=pagination_params.limit,
|
|
39
|
+
)
|
|
40
|
+
return paginate_cursor(
|
|
41
|
+
request=request,
|
|
42
|
+
items=posts,
|
|
43
|
+
limit=pagination_params.limit,
|
|
44
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
45
|
+
retrieved=pagination_params.retrieved,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get("/users/{user_id}", response_model=PaginationResponse[PostResponse])
|
|
50
|
+
async def get_posts_by_user_id(
|
|
51
|
+
user_id: str,
|
|
52
|
+
request: Request,
|
|
53
|
+
viewer_id: OptionalUserID,
|
|
54
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
55
|
+
) -> Any:
|
|
56
|
+
validated_user_id = validate_user_id(user_id=user_id)
|
|
57
|
+
posts = await retrieve_posts_by_user_id(
|
|
58
|
+
user_id=validated_user_id.root,
|
|
59
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
60
|
+
cursor=pagination_params.cursor,
|
|
61
|
+
limit=pagination_params.limit,
|
|
62
|
+
)
|
|
63
|
+
return paginate_cursor(
|
|
64
|
+
request=request,
|
|
65
|
+
items=posts,
|
|
66
|
+
limit=pagination_params.limit,
|
|
67
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
68
|
+
retrieved=pagination_params.retrieved,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get("/hashtags/{hashtag}", response_model=PaginationResponse[PostResponse])
|
|
73
|
+
async def get_posts_by_hashtag(
|
|
74
|
+
hashtag: str,
|
|
75
|
+
request: Request,
|
|
76
|
+
viewer_id: OptionalUserID,
|
|
77
|
+
pagination_params: Annotated[TokenCursorQueryParams, Depends()],
|
|
78
|
+
) -> Any:
|
|
79
|
+
validated_hashtag = validate_hashtag(hashtag=hashtag)
|
|
80
|
+
posts = await retrieve_posts_by_hashtag(
|
|
81
|
+
hashtag=validated_hashtag.root,
|
|
82
|
+
viewer_id=viewer_id.root if viewer_id else None,
|
|
83
|
+
cursor=pagination_params.cursor,
|
|
84
|
+
limit=pagination_params.limit,
|
|
85
|
+
)
|
|
86
|
+
return paginate_cursor(
|
|
87
|
+
request=request,
|
|
88
|
+
items=posts,
|
|
89
|
+
limit=pagination_params.limit,
|
|
90
|
+
cursor_field=PaginationCursorType.CREATED_AT,
|
|
91
|
+
retrieved=pagination_params.retrieved,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.get("/{post_id}", response_model=PostResponse)
|
|
96
|
+
async def get_post(post_id: ULID, viewer_id: OptionalUserID) -> Any:
|
|
97
|
+
post = await retrieve_post_by_id(post_id=str(post_id), viewer_id=viewer_id.root if viewer_id else None)
|
|
98
|
+
if post is None:
|
|
99
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Post not found")
|
|
100
|
+
return post
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from ulid import ULID
|
|
6
|
+
|
|
7
|
+
from core_framework.api.posts.schemas import Hashtag, PostContent
|
|
8
|
+
from core_framework.api.users.shared.schemas import UserReference
|
|
9
|
+
from core_framework.domains.post import PostVisibility
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PostStatsResponse(BaseModel):
|
|
13
|
+
like_count: Annotated[int, Field(ge=0)] = 0
|
|
14
|
+
comment_count: Annotated[int, Field(ge=0)] = 0
|
|
15
|
+
view_count: Annotated[int, Field(ge=0)] = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PostViewerContext(BaseModel):
|
|
19
|
+
is_liked: bool
|
|
20
|
+
is_reported: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthorContext(BaseModel):
|
|
24
|
+
can_edit: bool
|
|
25
|
+
can_delete: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PostResponse(BaseModel):
|
|
29
|
+
id: ULID
|
|
30
|
+
author: UserReference
|
|
31
|
+
content: PostContent
|
|
32
|
+
hashtags: list[Hashtag]
|
|
33
|
+
visibility: PostVisibility
|
|
34
|
+
stats: PostStatsResponse
|
|
35
|
+
engagement_allowed: bool
|
|
36
|
+
edited_at: datetime | None
|
|
37
|
+
created_at: datetime
|
|
38
|
+
viewer_context: PostViewerContext | None
|
|
39
|
+
author_context: AuthorContext | None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core_framework.api.posts.authenticated.router import router as authenticated_router
|
|
4
|
+
from core_framework.api.posts.public.router import router as public_router
|
|
5
|
+
|
|
6
|
+
router = APIRouter()
|
|
7
|
+
|
|
8
|
+
router.include_router(authenticated_router)
|
|
9
|
+
router.include_router(public_router)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi.exceptions import RequestValidationError
|
|
4
|
+
from pydantic import Field, RootModel, ValidationError, field_validator
|
|
5
|
+
|
|
6
|
+
MAX_HASHTAGS: int = 10
|
|
7
|
+
MAX_HASHTAG_LENGTH: int = 50
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Hashtag(RootModel[str]):
|
|
11
|
+
root: Annotated[str, Field(max_length=MAX_HASHTAG_LENGTH)]
|
|
12
|
+
|
|
13
|
+
@field_validator("root", mode="after")
|
|
14
|
+
@classmethod
|
|
15
|
+
def normalize_and_validate(cls, v: str) -> str:
|
|
16
|
+
s = v.strip().lstrip("#").lower()
|
|
17
|
+
if not s:
|
|
18
|
+
raise ValueError("Hashtag must not be blank")
|
|
19
|
+
if len(s) > MAX_HASHTAG_LENGTH:
|
|
20
|
+
raise ValueError(f"Hashtag must be at most {MAX_HASHTAG_LENGTH} characters")
|
|
21
|
+
return s
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PostContent(RootModel[str]):
|
|
25
|
+
root: Annotated[str, Field(min_length=1, max_length=10000)]
|
|
26
|
+
|
|
27
|
+
@field_validator("root", mode="after")
|
|
28
|
+
@classmethod
|
|
29
|
+
def validate_not_blank(cls, v: str) -> str:
|
|
30
|
+
if not v.strip():
|
|
31
|
+
raise ValueError("Post content must not be blank")
|
|
32
|
+
return v
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_hashtag(*, hashtag: str) -> Hashtag:
|
|
36
|
+
try:
|
|
37
|
+
return Hashtag(root=hashtag)
|
|
38
|
+
except ValidationError as e:
|
|
39
|
+
raise RequestValidationError(e.errors()) from e
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from core_framework.api.admin.router import router as admin_router
|
|
4
|
+
from core_framework.api.auth.router import router as auth_router
|
|
5
|
+
from core_framework.api.comments.router import router as comments_router
|
|
6
|
+
from core_framework.api.events.router import router as events_router
|
|
7
|
+
from core_framework.api.posts.router import router as posts_router
|
|
8
|
+
from core_framework.api.system.router import router as system_router
|
|
9
|
+
from core_framework.api.users.router import router as users_router
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
router.include_router(admin_router)
|
|
14
|
+
router.include_router(auth_router)
|
|
15
|
+
router.include_router(comments_router)
|
|
16
|
+
router.include_router(events_router)
|
|
17
|
+
router.include_router(posts_router)
|
|
18
|
+
router.include_router(system_router)
|
|
19
|
+
router.include_router(users_router)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pydantic import BaseModel, model_validator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BasePatchRequest(BaseModel):
|
|
5
|
+
@model_validator(mode="after")
|
|
6
|
+
def validate_non_empty_patch(self) -> BasePatchRequest:
|
|
7
|
+
if not self.model_fields_set:
|
|
8
|
+
raise ValueError("At least one field must be provided for update")
|
|
9
|
+
return self
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from asyncio import TaskGroup
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Request, Response, status
|
|
5
|
+
|
|
6
|
+
from core_framework.core.runtime import CoreRuntime
|
|
7
|
+
|
|
8
|
+
router = APIRouter(
|
|
9
|
+
tags=["system"],
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
OK_STATUS: Final[str] = "OK"
|
|
13
|
+
ERROR_STATUS: Final[str] = "ERROR"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_runtime(request: Request) -> CoreRuntime:
|
|
17
|
+
runtime = getattr(request.app.state, "core_runtime", None)
|
|
18
|
+
if runtime is not None:
|
|
19
|
+
return runtime
|
|
20
|
+
|
|
21
|
+
msg = "Core runtime is not configured on app.state. Build the app via init_app() or assign app.state.core_runtime during bootstrap."
|
|
22
|
+
raise RuntimeError(msg)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def _probe_write_postgres(runtime: CoreRuntime) -> str:
|
|
26
|
+
try:
|
|
27
|
+
async with runtime.write_postgres.get_connection() as connection:
|
|
28
|
+
await connection.fetchval("select 1")
|
|
29
|
+
except Exception:
|
|
30
|
+
return ERROR_STATUS
|
|
31
|
+
return OK_STATUS
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _probe_read_postgres(runtime: CoreRuntime) -> str:
|
|
35
|
+
try:
|
|
36
|
+
async with runtime.read_postgres.get_connection() as connection:
|
|
37
|
+
await connection.fetchval("select 1")
|
|
38
|
+
except Exception:
|
|
39
|
+
return ERROR_STATUS
|
|
40
|
+
return OK_STATUS
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _probe_redis_cache(runtime: CoreRuntime) -> str:
|
|
44
|
+
try:
|
|
45
|
+
await runtime.redis_cache.ping()
|
|
46
|
+
except Exception:
|
|
47
|
+
return ERROR_STATUS
|
|
48
|
+
return OK_STATUS
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _probe_redis_queue(runtime: CoreRuntime) -> str:
|
|
52
|
+
try:
|
|
53
|
+
await runtime.redis_queue.redis_queue.version()
|
|
54
|
+
except Exception:
|
|
55
|
+
return ERROR_STATUS
|
|
56
|
+
return OK_STATUS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _probe_http_client(runtime: CoreRuntime) -> str:
|
|
60
|
+
try:
|
|
61
|
+
await runtime.general_http_client.client.get("https://www.google.com/generate_204")
|
|
62
|
+
except Exception:
|
|
63
|
+
return ERROR_STATUS
|
|
64
|
+
return OK_STATUS
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.get("/health")
|
|
68
|
+
async def health_check() -> dict[str, str]:
|
|
69
|
+
return {"status": OK_STATUS}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get("/readiness")
|
|
73
|
+
async def readiness_check(request: Request, response: Response) -> dict[str, str]:
|
|
74
|
+
runtime = _get_runtime(request)
|
|
75
|
+
|
|
76
|
+
async with TaskGroup() as tg:
|
|
77
|
+
write_task = tg.create_task(_probe_write_postgres(runtime))
|
|
78
|
+
read_task = tg.create_task(_probe_read_postgres(runtime))
|
|
79
|
+
cache_task = tg.create_task(_probe_redis_cache(runtime))
|
|
80
|
+
queue_task = tg.create_task(_probe_redis_queue(runtime))
|
|
81
|
+
http_task = tg.create_task(_probe_http_client(runtime))
|
|
82
|
+
|
|
83
|
+
write_postgres_status = write_task.result()
|
|
84
|
+
read_postgres_status = read_task.result()
|
|
85
|
+
redis_cache_status = cache_task.result()
|
|
86
|
+
redis_queue_status = queue_task.result()
|
|
87
|
+
http_client_status = http_task.result()
|
|
88
|
+
|
|
89
|
+
service_statuses = (
|
|
90
|
+
write_postgres_status,
|
|
91
|
+
read_postgres_status,
|
|
92
|
+
redis_cache_status,
|
|
93
|
+
redis_queue_status,
|
|
94
|
+
http_client_status,
|
|
95
|
+
)
|
|
96
|
+
app_status = ERROR_STATUS if any(s == ERROR_STATUS for s in service_statuses) else OK_STATUS
|
|
97
|
+
|
|
98
|
+
if app_status == ERROR_STATUS:
|
|
99
|
+
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"app_status": app_status,
|
|
103
|
+
"write_postgres_status": write_postgres_status,
|
|
104
|
+
"read_postgres_status": read_postgres_status,
|
|
105
|
+
"redis_cache_status": redis_cache_status,
|
|
106
|
+
"redis_queue_status": redis_queue_status,
|
|
107
|
+
"http_client_status": http_client_status,
|
|
108
|
+
}
|
|
File without changes
|
|
File without changes
|