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,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