core-framework 1.2.0__tar.gz → 1.4.0__tar.gz

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 (376) hide show
  1. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/constants-final.mdc +1 -1
  2. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/database-triggers.md +1 -24
  3. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/domain-input-guards.mdc +3 -2
  4. {core_framework-1.2.0 → core_framework-1.4.0}/.dockerignore +2 -3
  5. {core_framework-1.2.0 → core_framework-1.4.0}/.github/workflows/_deploy.yml +2 -2
  6. core_framework-1.4.0/.github/workflows/publish-pypi.yml +34 -0
  7. {core_framework-1.2.0 → core_framework-1.4.0}/CHANGELOG.md +47 -1
  8. core_framework-1.4.0/PKG-INFO +115 -0
  9. core_framework-1.4.0/README.md +81 -0
  10. core_framework-1.4.0/alembic/user/alembic/versions/user_add_date_of_birth_to_profiles.py +27 -0
  11. {core_framework-1.2.0 → core_framework-1.4.0}/config.toml +2 -2
  12. {core_framework-1.2.0 → core_framework-1.4.0}/config.toml.template +2 -2
  13. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/users/router.py +2 -2
  14. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/users/schemas.py +8 -5
  15. core_framework-1.4.0/core_framework/api/comments/authenticated/mappers.py +10 -0
  16. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/authenticated/router.py +2 -2
  17. core_framework-1.4.0/core_framework/api/posts/authenticated/mappers.py +14 -0
  18. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/authenticated/router.py +2 -2
  19. core_framework-1.4.0/core_framework/api/users/authenticated/mappers.py +21 -0
  20. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/router.py +17 -6
  21. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/schemas.py +3 -0
  22. core_framework-1.4.0/core_framework/api/users/mappers.py +77 -0
  23. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/shared/schemas.py +35 -6
  24. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/comments/authenticated_service.py +3 -9
  25. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/posts/authenticated_service.py +3 -9
  26. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/admin_service.py +17 -12
  27. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/authenticated_service.py +19 -23
  28. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/firebase.py +8 -1
  29. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/settings.py +16 -3
  30. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/__init__.py +2 -0
  31. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/models.py +30 -1
  32. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/repository.py +4 -5
  33. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/service.py +7 -8
  34. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/__init__.py +2 -1
  35. core_framework-1.4.0/core_framework/domains/post/models.py +98 -0
  36. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/repository.py +5 -12
  37. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/service.py +7 -12
  38. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/__init__.py +6 -0
  39. core_framework-1.4.0/core_framework/domains/user/constants.py +11 -0
  40. core_framework-1.4.0/core_framework/domains/user/models.py +269 -0
  41. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/repository.py +23 -25
  42. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/service.py +14 -38
  43. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/utils.py +0 -13
  44. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/main.py +2 -2
  45. core_framework-1.4.0/docker-compose.dev.yaml +24 -0
  46. {core_framework-1.2.0 → core_framework-1.4.0}/dockerfile +1 -1
  47. {core_framework-1.2.0 → core_framework-1.4.0}/docs/conventions.md +8 -1
  48. core_framework-1.4.0/docs/flows/media/upload_pipeline.md +454 -0
  49. core_framework-1.4.0/docs/flows/users/avatar.md +91 -0
  50. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/change_history.md +2 -1
  51. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/profile.md +10 -11
  52. core_framework-1.4.0/docs/patch-update-typed-payload.md +186 -0
  53. core_framework-1.4.0/docs/todo.md +11 -0
  54. core_framework-1.4.0/firebase_config.example.json +13 -0
  55. {core_framework-1.2.0 → core_framework-1.4.0}/pyproject.toml +1 -1
  56. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/users/router_test.py +42 -2
  57. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/users/authenticated/router_test.py +90 -12
  58. {core_framework-1.2.0 → core_framework-1.4.0}/uv.lock +1 -1
  59. core_framework-1.2.0/.logfire/.gitignore +0 -1
  60. core_framework-1.2.0/.logfire/logfire_credentials.json +0 -6
  61. core_framework-1.2.0/PKG-INFO +0 -131
  62. core_framework-1.2.0/README.md +0 -97
  63. core_framework-1.2.0/core_framework/domains/post/models.py +0 -61
  64. core_framework-1.2.0/core_framework/domains/user/constants.py +0 -41
  65. core_framework-1.2.0/core_framework/domains/user/models.py +0 -153
  66. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/api-layer.mdc +0 -0
  67. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/api-reference-docs.mdc +0 -0
  68. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/api-security.mdc +0 -0
  69. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/api-validation.mdc +0 -0
  70. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/application-layer.mdc +0 -0
  71. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/domain-caller-context.mdc +0 -0
  72. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/domain-imports.mdc +0 -0
  73. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/domain-repository-exceptions.mdc +0 -0
  74. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/flow-documentation.mdc +0 -0
  75. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/implementation-workflow.mdc +0 -0
  76. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/integration-test-strategy.mdc +0 -0
  77. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/layer-boundaries.mdc +0 -0
  78. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/no-code-in-docs.mdc +0 -0
  79. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/no-docstrings.mdc +0 -0
  80. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/postgres-config-conventions.mdc +0 -0
  81. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/repository-read-consistency.mdc +0 -0
  82. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/strong-read-opt-in.mdc +0 -0
  83. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/rules/tech-stack.mdc +0 -0
  84. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/skills/add-config/SKILL.md +0 -0
  85. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/skills/add-domain/SKILL.md +0 -0
  86. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/skills/code-review/SKILL.md +0 -0
  87. {core_framework-1.2.0 → core_framework-1.4.0}/.cursor/skills/recommend-features/SKILL.md +0 -0
  88. {core_framework-1.2.0 → core_framework-1.4.0}/.github/workflows/dev-ci-cd.yaml +0 -0
  89. {core_framework-1.2.0 → core_framework-1.4.0}/.github/workflows/manual-deployment.yaml +0 -0
  90. {core_framework-1.2.0 → core_framework-1.4.0}/.github/workflows/test.yaml +0 -0
  91. {core_framework-1.2.0 → core_framework-1.4.0}/.gitignore +0 -0
  92. {core_framework-1.2.0 → core_framework-1.4.0}/.pre-commit-config.yaml +0 -0
  93. {core_framework-1.2.0 → core_framework-1.4.0}/.python-version +0 -0
  94. {core_framework-1.2.0 → core_framework-1.4.0}/LICENSE +0 -0
  95. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/comment/alembic/README +0 -0
  96. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/comment/alembic/env.py +0 -0
  97. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/comment/alembic/script.py.mako +0 -0
  98. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/comment/alembic/versions/v1_comment_init_baseline.py +0 -0
  99. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/comment/alembic.ini +0 -0
  100. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/extension/alembic/README +0 -0
  101. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/extension/alembic/env.py +0 -0
  102. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/extension/alembic/script.py.mako +0 -0
  103. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/extension/alembic/versions/v1_ext_init_baseline.py +0 -0
  104. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/extension/alembic.ini +0 -0
  105. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/moderation/alembic/README +0 -0
  106. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/moderation/alembic/env.py +0 -0
  107. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/moderation/alembic/script.py.mako +0 -0
  108. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/moderation/alembic/versions/v1_mod_init_baseline.py +0 -0
  109. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/moderation/alembic.ini +0 -0
  110. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/notification/alembic/README +0 -0
  111. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/notification/alembic/env.py +0 -0
  112. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/notification/alembic/script.py.mako +0 -0
  113. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/notification/alembic/versions/v1_notif_init_baseline.py +0 -0
  114. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/notification/alembic.ini +0 -0
  115. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/post/alembic/README +0 -0
  116. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/post/alembic/env.py +0 -0
  117. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/post/alembic/script.py.mako +0 -0
  118. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/post/alembic/versions/v1_post_init_baseline.py +0 -0
  119. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/post/alembic.ini +0 -0
  120. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/user/alembic/README +0 -0
  121. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/user/alembic/env.py +0 -0
  122. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/user/alembic/script.py.mako +0 -0
  123. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/user/alembic/versions/v1_user_init_baseline.py +0 -0
  124. {core_framework-1.2.0 → core_framework-1.4.0}/alembic/user/alembic.ini +0 -0
  125. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/__init__.py +0 -0
  126. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/__init__.py +0 -0
  127. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/__init__.py +0 -0
  128. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/comments/router.py +0 -0
  129. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/comments/schemas.py +0 -0
  130. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/__init__.py +0 -0
  131. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/router.py +0 -0
  132. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/schemas.py +0 -0
  133. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/posts/router.py +0 -0
  134. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/posts/schemas.py +0 -0
  135. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/router.py +0 -0
  136. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/admin/users/__init__.py +0 -0
  137. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/auth/__init__.py +0 -0
  138. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/auth/router.py +0 -0
  139. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/auth/schemas.py +0 -0
  140. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/authenticated/schemas.py +0 -0
  141. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/public/router.py +0 -0
  142. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/public/schemas.py +0 -0
  143. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/router.py +0 -0
  144. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/comments/schemas.py +0 -0
  145. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/constants.py +0 -0
  146. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/dependencies.py +0 -0
  147. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/events/router.py +0 -0
  148. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/events/schemas.py +0 -0
  149. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/notifications/authenticated/router.py +0 -0
  150. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/notifications/authenticated/schemas.py +0 -0
  151. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/notifications/router.py +0 -0
  152. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/authenticated/schemas.py +0 -0
  153. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/public/router.py +0 -0
  154. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/public/schemas.py +0 -0
  155. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/router.py +0 -0
  156. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/posts/schemas.py +0 -0
  157. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/router.py +0 -0
  158. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/schemas.py +0 -0
  159. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/system/__init__.py +0 -0
  160. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/system/router.py +0 -0
  161. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/__init__.py +0 -0
  162. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/__init__.py +0 -0
  163. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/public/__init__.py +0 -0
  164. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/public/router.py +0 -0
  165. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/public/schemas.py +0 -0
  166. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/api/users/router.py +0 -0
  167. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/__init__.py +0 -0
  168. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/auth/__init__.py +0 -0
  169. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/auth/access_service.py +0 -0
  170. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/auth/auth_service.py +0 -0
  171. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/auth/models.py +0 -0
  172. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/bootstrap.py +0 -0
  173. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/comments/admin_service.py +0 -0
  174. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/comments/aggregation_service.py +0 -0
  175. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/comments/public_service.py +0 -0
  176. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/events/README.md +0 -0
  177. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/events/event_service.py +0 -0
  178. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/events/event_token.py +0 -0
  179. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/events/models.py +0 -0
  180. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/__init__.py +0 -0
  181. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/appeal_service.py +0 -0
  182. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/moderator_service.py +0 -0
  183. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/report_service.py +0 -0
  184. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/scheduled_service.py +0 -0
  185. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/moderation/user_service.py +0 -0
  186. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/notifications/README.md +0 -0
  187. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/notifications/enums.py +0 -0
  188. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/notifications/inbox_service.py +0 -0
  189. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/notifications/mute_service.py +0 -0
  190. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/notifications/notification_service.py +0 -0
  191. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/posts/admin_service.py +0 -0
  192. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/posts/aggregation_service.py +0 -0
  193. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/posts/public_service.py +0 -0
  194. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/shared/__init__.py +0 -0
  195. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/shared/enums.py +0 -0
  196. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/shared/exceptions.py +0 -0
  197. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/shared/user_agent.py +0 -0
  198. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/__init__.py +0 -0
  199. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/aggregation_service.py +0 -0
  200. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/public_service.py +0 -0
  201. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/application/users/scheduled_service.py +0 -0
  202. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/asgi.py +0 -0
  203. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/bundled_alembic.py +0 -0
  204. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/constants.py +0 -0
  205. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/__init__.py +0 -0
  206. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/cache.py +0 -0
  207. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/context.py +0 -0
  208. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/database.py +0 -0
  209. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/__init__.py +0 -0
  210. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/comment.py +0 -0
  211. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/common.py +0 -0
  212. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/moderation.py +0 -0
  213. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/notification.py +0 -0
  214. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/post.py +0 -0
  215. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/setup.py +0 -0
  216. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/user.py +0 -0
  217. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/http_client.py +0 -0
  218. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/logging.py +0 -0
  219. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/middleware.py +0 -0
  220. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/observability.py +0 -0
  221. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/pagination.py +0 -0
  222. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/redis.py +0 -0
  223. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/core/runtime.py +0 -0
  224. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/__init__.py +0 -0
  225. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/README.md +0 -0
  226. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/constants.py +0 -0
  227. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/dependencies.py +0 -0
  228. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/enums.py +0 -0
  229. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/comment/exceptions.py +0 -0
  230. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/exceptions.py +0 -0
  231. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/README.md +0 -0
  232. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/__init__.py +0 -0
  233. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/dependencies.py +0 -0
  234. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/enums.py +0 -0
  235. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/exceptions.py +0 -0
  236. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/models.py +0 -0
  237. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/repository.py +0 -0
  238. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/moderation/service.py +0 -0
  239. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/README.md +0 -0
  240. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/__init__.py +0 -0
  241. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/dependencies.py +0 -0
  242. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/enums.py +0 -0
  243. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/exceptions.py +0 -0
  244. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/models.py +0 -0
  245. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/repository.py +0 -0
  246. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/notification/service.py +0 -0
  247. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/README.md +0 -0
  248. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/constants.py +0 -0
  249. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/dependencies.py +0 -0
  250. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/enums.py +0 -0
  251. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/post/exceptions.py +0 -0
  252. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/README.md +0 -0
  253. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/dependencies.py +0 -0
  254. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/enums.py +0 -0
  255. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/exceptions.py +0 -0
  256. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/domains/user/utils.py +0 -0
  257. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/migrate_cli.py +0 -0
  258. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/__init__.py +0 -0
  259. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/arq.py +0 -0
  260. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/auth.py +0 -0
  261. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/config.py +0 -0
  262. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/containers.py +0 -0
  263. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/firebase.py +0 -0
  264. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/hookspecs.py +0 -0
  265. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/httpx_test_client.py +0 -0
  266. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/migrations.py +0 -0
  267. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/testing/plugin.py +0 -0
  268. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/__init__.py +0 -0
  269. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/main.py +0 -0
  270. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/__init__.py +0 -0
  271. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_comment_stats.py +0 -0
  272. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_post_stats.py +0 -0
  273. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_user_stats.py +0 -0
  274. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_expired_account_deletions.py +0 -0
  275. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_expired_mute_lifts.py +0 -0
  276. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/__init__.py +0 -0
  277. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_account_deletion.py +0 -0
  278. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_comment_stats.py +0 -0
  279. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_post_stats.py +0 -0
  280. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_user_stats.py +0 -0
  281. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_mute_lift.py +0 -0
  282. {core_framework-1.2.0 → core_framework-1.4.0}/core_framework/worker/worker_context.py +0 -0
  283. {core_framework-1.2.0 → core_framework-1.4.0}/docker-compose.yml +0 -0
  284. {core_framework-1.2.0 → core_framework-1.4.0}/docs/api.md +0 -0
  285. {core_framework-1.2.0 → core_framework-1.4.0}/docs/architecture-decisions.md +0 -0
  286. {core_framework-1.2.0 → core_framework-1.4.0}/docs/architecture.md +0 -0
  287. {core_framework-1.2.0 → core_framework-1.4.0}/docs/core-framework-migration.md +0 -0
  288. {core_framework-1.2.0 → core_framework-1.4.0}/docs/database-triggers.md +0 -0
  289. {core_framework-1.2.0 → core_framework-1.4.0}/docs/event-outbox-design.md +0 -0
  290. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/auth/access_control.md +0 -0
  291. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/auth/registration.md +0 -0
  292. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/admin_comments.md +0 -0
  293. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/comment_report.md +0 -0
  294. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/comment_stats_aggregation.md +0 -0
  295. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/create_comment.md +0 -0
  296. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/delete_comment.md +0 -0
  297. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/edit_comment.md +0 -0
  298. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/comments/retrieve_comments.md +0 -0
  299. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/events/events.md +0 -0
  300. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/mentions/mentions_in_content.md +0 -0
  301. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/moderation/appeals.md +0 -0
  302. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/moderation/internal_notes.md +0 -0
  303. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/moderation/moderator_actions.md +0 -0
  304. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/moderation/reports.md +0 -0
  305. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/moderation/restrictions.md +0 -0
  306. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/notifications/notification_inbox.md +0 -0
  307. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/posts/admin_posts.md +0 -0
  308. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/posts/author_context.md +0 -0
  309. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/posts/hashtag_discovery.md +0 -0
  310. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/posts/post_like.md +0 -0
  311. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/posts/post_stats_aggregation.md +0 -0
  312. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/account.md +0 -0
  313. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/account_deletion.md +0 -0
  314. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/blocks.md +0 -0
  315. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/check_username_exists.md +0 -0
  316. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/follow.md +0 -0
  317. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/my_posts_and_comments.md +0 -0
  318. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/preferences.md +0 -0
  319. {core_framework-1.2.0 → core_framework-1.4.0}/docs/flows/users/user_removal.md +0 -0
  320. {core_framework-1.2.0 → core_framework-1.4.0}/docs/follow-system-design.md +0 -0
  321. {core_framework-1.2.0 → core_framework-1.4.0}/docs/package-api.md +0 -0
  322. {core_framework-1.2.0 → core_framework-1.4.0}/docs/testing-plugin-design.md +0 -0
  323. {core_framework-1.2.0 → core_framework-1.4.0}/makefile +0 -0
  324. {core_framework-1.2.0 → core_framework-1.4.0}/tests/__init__.py +0 -0
  325. {core_framework-1.2.0 → core_framework-1.4.0}/tests/conftest.py +0 -0
  326. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/__init__.py +0 -0
  327. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/__init__.py +0 -0
  328. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/_http_helpers.py +0 -0
  329. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/__init__.py +0 -0
  330. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/comments/__init__.py +0 -0
  331. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/comments/router_test.py +0 -0
  332. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/moderation/__init__.py +0 -0
  333. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/moderation/router_test.py +0 -0
  334. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/posts/__init__.py +0 -0
  335. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/posts/router_test.py +0 -0
  336. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/admin/users/__init__.py +0 -0
  337. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/auth/__init__.py +0 -0
  338. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/auth/router_test.py +0 -0
  339. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/comments/__init__.py +0 -0
  340. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/comments/authenticated/comment_writes_integration_test.py +0 -0
  341. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/comments/public/__init__.py +0 -0
  342. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/comments/public/router_test.py +0 -0
  343. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/events/router_test.py +0 -0
  344. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/notifications/__init__.py +0 -0
  345. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/notifications/router_test.py +0 -0
  346. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/__init__.py +0 -0
  347. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/authenticated/post_writes_integration_test.py +0 -0
  348. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/comment_count_aggregation_test.py +0 -0
  349. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/followers_visibility_test.py +0 -0
  350. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/post_stats_dirty_marking_test.py +0 -0
  351. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/public/__init__.py +0 -0
  352. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/posts/public/router_test.py +0 -0
  353. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/system/__init__.py +0 -0
  354. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/system/router_test.py +0 -0
  355. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/users/__init__.py +0 -0
  356. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/users/authenticated/__init__.py +0 -0
  357. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/users/public/__init__.py +0 -0
  358. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/api/users/public/router_test.py +0 -0
  359. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/__init__.py +0 -0
  360. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/account_deletion_test.py +0 -0
  361. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_comment_stats_test.py +0 -0
  362. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_post_stats_test.py +0 -0
  363. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_user_stats_test.py +0 -0
  364. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/conftest.py +0 -0
  365. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/mute_lift_test.py +0 -0
  366. {core_framework-1.2.0 → core_framework-1.4.0}/tests/integration/worker/utils_test.py +0 -0
  367. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/__init__.py +0 -0
  368. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/application/comments/__init__.py +0 -0
  369. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/application/events/event_service_test.py +0 -0
  370. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/application/notifications/inbox_service_test.py +0 -0
  371. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/core/bundled_alembic_test.py +0 -0
  372. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/core/migrate_cli_test.py +0 -0
  373. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/core/pagination_test.py +0 -0
  374. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/domains/__init__.py +0 -0
  375. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/domains/comment/__init__.py +0 -0
  376. {core_framework-1.2.0 → core_framework-1.4.0}/tests/unit/domains/user/service_test.py +0 -0
@@ -9,7 +9,7 @@ alwaysApply: false
9
9
  When introducing or editing **module-level constants** (values that are fixed for the lifetime of the module and not meant to be reassigned):
10
10
 
11
11
  - Annotate them with **`typing.Final`** and an explicit type, e.g. **`NAME: Final[int] = 42`** or **`_INTERNAL: Final[str] = "henry"`**.
12
- - Prefer **`FrozenSet`**, **`frozenset` literals**, **`MappingProxyType`**, or immutable collections for grouped constants when mutation must be prevented — e.g. **`USER_PREFERENCE_UPDATE_COLUMNS`** in **`core_framework/domains/user/constants.py`**.
12
+ - Prefer **`FrozenSet`**, **`frozenset` literals**, **`MappingProxyType`**, or immutable collections for grouped constants when mutation must be prevented — e.g. **`ProfileUpdate.UPDATE_COLUMNS`** on domain update types.
13
13
  - **`Final`** is for **assignment** semantics (no rebinding): it does not freeze mutable object contents unless the type/container is immutable.
14
14
 
15
15
  Do not refactor unrelated legacy constants only to add **`Final`** unless you are already touching that symbol.
@@ -124,30 +124,7 @@ $$ language plpgsql;
124
124
  - Error message isn't localized
125
125
  - Makes assumptions about business rules
126
126
 
127
- **Correct approach:**
128
-
129
- ```python
130
- # In service layer
131
- async def change_profile(
132
- self,
133
- *,
134
- actor_id: str,
135
- user_id: str,
136
- validated_update_request: dict[str, Any],
137
- ) -> None:
138
- if "username" in validated_update_request:
139
- recent_changes = await self.repository.select_recent_username_changes(
140
- user_id=user_id, since=datetime.now(timezone.utc) - timedelta(days=7)
141
- )
142
- if recent_changes:
143
- raise UsernameCooldownException()
144
-
145
- await self.repository.update_profile(
146
- user_id=user_id,
147
- validated_update_request=validated_update_request,
148
- actor_id=actor_id,
149
- )
150
- ```
127
+ **Correct approach:** enforce cooldown (or other policy) in the service, then call the repository with a typed **`ProfileUpdate`** (or the relevant `*Update`).
151
128
 
152
129
  ### ❌ Bad: Automatic State Transitions
153
130
 
@@ -43,13 +43,14 @@ Use these across domains so validation stays consistent and call sites stay read
43
43
  | **`validate_pagination_limit(limit=...)`** | Paginated reads: `limit` in `[1, MAX_PAGE_FETCH_SIZE]` (aligned with `core_framework.core.pagination`). |
44
44
  | **`validate_pagination_offset(offset=...)`** | Paginated reads: `offset` must be **≥ 0** (aligned with `OffsetQueryParams`). |
45
45
  | **`require_positive_int(value=..., field=...)`** | **Integer** surrogate keys only (e.g. moderation `report_id`, `appeal_id`, `note_id`). Must be **≥ 1**. Domains that identify entities with **string** IDs (ULIDs) use **`require_non_blank_*`** instead—do not add this helper there. |
46
- | **`require_validated_update_has_allowed_fields(validated_update_request=..., allowed_fields=...)`** | PATCH-style dict updates: reject empty bodies and bodies with **no** recognized keys (use domain constants such as **`USER_*_UPDATE_FIELDS`**). |
47
46
 
48
47
  Prefer **`require_non_blank_kwargs`** / **`require_non_blank_if_not_none`** over many repeated **`require_non_blank`** lines when a method checks several fields.
49
48
 
49
+ **PATCH partial updates:** domain `*Update` dataclasses (e.g. **`ProfileUpdate`**) expose **`require_has_updates()`** (non-empty body with at least one allowed field) and **`fields_set`** (which keys the client sent). API mappers build these from Pydantic **`model_fields_set`**; services call **`update.require_has_updates()`** before repository writes. Empty PATCH bodies remain rejected at the API via **`BasePatchRequest`**.
50
+
50
51
  ## Repository (thin data access; no duplicate validation)
51
52
 
52
- - **Call path**: API and application code must not import repositories directly (see **`domain-imports.mdc`**); they use **`{domain}_service`** from **`dependencies.py`**. So e.g. **`UserRepository.update_preferences` / `update_profile` / `update_account`** are only reached through **`UserService`**, which runs **`require_validated_update_has_allowed_fields`**—there is no supported bypass for empty or all-unknown PATCH bodies in product code.
53
+ - **Call path**: API and application code must not import repositories directly (see **`domain-imports.mdc`**); they use **`{domain}_service`** from **`dependencies.py`**. PATCH flows reach **`UserRepository.update_*`** (and post/comment equivalents) only through **`UserService.change_*`**, which runs **`require_has_updates()`** on the typed update—there is no supported bypass for empty or all-unknown PATCH bodies in product code.
53
54
  - **Prefer no input-validation guards** in repositories (empty batches, non-blank strings, pagination limits, positive int IDs): the **domain service** owns those contracts. The repository runs SQL; **`WHERE … = any($1)`** with an empty array is valid in Postgres and returns no rows.
54
55
  - Only add a repository guard for **SQL-specific** hazards that are awkward to express as service policy (rare); **do not** duplicate service-level “skip if empty” logic.
55
56
  - **Do not** use the repository as the **main** home for product rules that belong in the service.
@@ -25,9 +25,8 @@ htmlcov/
25
25
  *.swo
26
26
  *~
27
27
 
28
- # Documentation
29
- README.md
30
- *.md
28
+ # Documentation (wheel build needs root README.md; skip large doc tree)
29
+ docs/
31
30
 
32
31
  # CI/CD
33
32
  .github/
@@ -39,8 +39,8 @@ jobs:
39
39
  REDIS_HOST: ${{ secrets.REDIS_HOST }}
40
40
  REDIS_PORT: ${{ secrets.REDIS_PORT }}
41
41
  ALLOWED_HOSTS: ${{ secrets.ALLOWED_HOSTS }}
42
- AVATAR_BASE_URL: ${{ secrets.AVATAR_BASE_URL }}
43
- AVATAR_DEFAULT_URL: ${{ secrets.AVATAR_DEFAULT_URL }}
42
+ AVATAR_HOST_DOMAIN: ${{ secrets.AVATAR_HOST_DOMAIN }}
43
+ AVATAR_DEFAULT_IMAGE: ${{ secrets.AVATAR_DEFAULT_IMAGE }}
44
44
  BANNER_BASE_URL: ${{ secrets.BANNER_BASE_URL }}
45
45
  BANNER_DEFAULT_URL: ${{ secrets.BANNER_DEFAULT_URL }}
46
46
  LOGFIRE_TOKEN: ${{ secrets.LOGFIRE_TOKEN }}
@@ -0,0 +1,34 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v6
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v7
18
+ with:
19
+ version: "0.7.9"
20
+ enable-cache: true
21
+ cache-dependency-glob: "uv.lock"
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v6
25
+ with:
26
+ python-version-file: ".python-version"
27
+
28
+ - name: Build
29
+ run: uv build
30
+
31
+ - name: Publish
32
+ run: uv publish
33
+ env:
34
+ UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
@@ -4,6 +4,50 @@ Notable changes to **core-framework** (import **`core_framework`**). Format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.4.0] - 2026-05-19
8
+
9
+ ### Changed
10
+
11
+ - **Breaking:** **`avatar`** on profile and related user responses is an object (**`size_128`**, **`size_500`**, **`fallback`**) instead of a single URL string. See **`docs/flows/users/avatar.md`**.
12
+ - **Breaking:** **`[avatar]`** config uses **`avatar_host_domain`** + **`default_avatar_image`** (deploy env **`AVATAR_HOST_DOMAIN`**, **`AVATAR_DEFAULT_IMAGE`**) instead of **`base_url`** / **`default_url`**.
13
+ - **Typed PATCH payloads** — user (preferences, account, profile + admin profile), post, and comment PATCH routes map Pydantic requests to domain `*Update` types at the API boundary; no `model_dump(mode="json")` or dict partial-update payloads through application/domain/repository. See `docs/patch-update-typed-payload.md` and `docs/conventions.md` (*PATCH partial updates*).
14
+ - Self-service profile PATCH invalidates **`USER_DETAIL`** only when **`display_name`** or **`avatar_id`** change (fields present in cached identity). **`profile_visibility`** is coerced to **`ProfileVisibility`** on repository read.
15
+
16
+ ### Added
17
+
18
+ - Optional **`date_of_birth`** on user profiles: calendar date (`YYYY-MM-DD`), not in the future, omit unchanged / **`null`** clears. Exposed on **`GET`** / **`PATCH /users/me/profile`** and on admin user detail plus **`PATCH /admin/users/{user_id}/profile`**. Not returned on other users’ public surfaces (follow lists, post author cards, etc.).
19
+
20
+ ### Database
21
+
22
+ - User domain migration **`user_add_date_of_birth`**: nullable **`date_of_birth`** column on **`user_profiles`**. Run **`uv run cf-alembic`** (or your host migration step) before deploying application code that reads or writes the field.
23
+
24
+ ### Documentation
25
+
26
+ - **`docs/flows/users/avatar.md`**: multi-size **`avatar`** response shape and CDN URL convention.
27
+ - **`docs/flows/media/upload_pipeline.md`**: upload pipeline design (implementation pending).
28
+ - **`docs/flows/users/profile.md`**: DOB on get/update flows and validation errors.
29
+ - **`docs/flows/users/change_history.md`**: profile **`date_of_birth`** changes are not written to change history (audit trigger unchanged).
30
+ - **`docs/patch-update-typed-payload.md`**: typed PATCH decisions and patterns for partial-update endpoints.
31
+
32
+ ### Fixed
33
+
34
+ - **Dockerfile**: include **`README.md`** in the build context so hatchling can resolve the package readme.
35
+
36
+ ## [1.3.0] - 2026-05-16
37
+
38
+ ### Added
39
+
40
+ - **`docker-compose.dev.yaml`** for local Postgres and Redis (aligned with repo **`config.toml`**).
41
+ - **`firebase_config.example.json`** and **`FileNotFoundError`** with next-step text when **`firebase_config.json`** is missing in **`init_firebase`**.
42
+
43
+ ### Documentation
44
+
45
+ - **`README.md`**: install (**`uv`**), local development, Firebase file copy, host / pytest integration examples.
46
+
47
+ ### Fixed
48
+
49
+ - Example Firebase JSON uses no PEM markers so **`detect-private-key`** pre-commit hooks do not false-positive.
50
+
7
51
  ## [1.2.0] - 2026-05-02
8
52
 
9
53
  ### Changed
@@ -124,4 +168,6 @@ First **SemVer-stable** release per **`docs/package-api.md`**.
124
168
  [1.1.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.0.0...v1.1.0
125
169
  [1.1.1]: https://github.com/NepNepFFXIV/core-framework/compare/v1.1.0...v1.1.1
126
170
  [1.2.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.1.1...v1.2.0
127
- [unreleased]: https://github.com/NepNepFFXIV/core-framework/compare/v1.2.0...HEAD
171
+ [1.3.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.2.0...v1.3.0
172
+ [1.4.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.3.0...v1.4.0
173
+ [unreleased]: https://github.com/NepNepFFXIV/core-framework/compare/v1.4.0...HEAD
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: core-framework
3
+ Version: 1.4.0
4
+ Summary: Core framework package (import as core_framework)
5
+ Project-URL: Homepage, https://github.com/NepNepFFXIV/core-framework
6
+ Project-URL: Repository, https://github.com/NepNepFFXIV/core-framework
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.14
10
+ Requires-Dist: alembic>=1.18.4
11
+ Requires-Dist: arq>=0.28.0
12
+ Requires-Dist: async-lru>=2.1.0
13
+ Requires-Dist: asyncer>=0.0.14
14
+ Requires-Dist: asyncpg>=0.31.0
15
+ Requires-Dist: brotli-asgi>=1.6.0
16
+ Requires-Dist: cashews[redis]>=7.5.0
17
+ Requires-Dist: device-detector>=6.2.0
18
+ Requires-Dist: fastapi[standard]>=0.136.1
19
+ Requires-Dist: firebase-admin>=7.4.0
20
+ Requires-Dist: httpx[http2]>=0.28.1
21
+ Requires-Dist: itsdangerous>=2.2.0
22
+ Requires-Dist: logfire[asyncpg,fastapi,httpx,redis]>=4.32.1
23
+ Requires-Dist: mashumaro[orjson]>=3.20
24
+ Requires-Dist: orjson>=3.11.7
25
+ Requires-Dist: python-ulid>=3.1.0
26
+ Requires-Dist: structlog>=25.5.0
27
+ Requires-Dist: tenacity>=9.1.4
28
+ Provides-Extra: testing
29
+ Requires-Dist: asgi-lifespan>=2.1.0; extra == 'testing'
30
+ Requires-Dist: pytest-xdist>=3.6.0; extra == 'testing'
31
+ Requires-Dist: pytest>=9.0.3; extra == 'testing'
32
+ Requires-Dist: testcontainers[postgres,redis]>=4.13.2; extra == 'testing'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Core framework
36
+
37
+ Install from PyPI as **`core-framework`**, import **`core_framework`**. MIT — [LICENSE](LICENSE). [Repository](https://github.com/NepNepFFXIV/core-framework), [PyPI](https://pypi.org/project/core-framework/).
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ uv add core-framework
43
+ ```
44
+
45
+ ## Local development
46
+
47
+ Use **Python 3.14**. Install **uv** and **Docker**.
48
+
49
+ ```bash
50
+ docker compose -f docker-compose.dev.yaml up -d
51
+ uv sync --locked --all-extras --dev
52
+ uv run cf-alembic
53
+ make run
54
+ ```
55
+
56
+ **Firebase:** copy **`firebase_config.example.json`** to **`firebase_config.json`** at the repo root and replace with your Firebase project’s **service account** JSON from the console.
57
+
58
+ For **`make test`**, use the Firebase CLI (Auth emulator). Stop DB/Redis: **`docker compose -f docker-compose.dev.yaml down`**.
59
+
60
+ ## Hosts
61
+
62
+ In your host app, load **`Settings`** or a **`Settings`** subclass from **`core_framework.core.settings`**, pass it to **`init_app`** from **`core_framework.main`**, then wire host-specific dependencies, exception handlers, and routers.
63
+
64
+ ```python
65
+ from core_framework.main import init_app
66
+ from fastapi import FastAPI
67
+
68
+ from myapp.settings import HostSettings, load_default_settings
69
+
70
+
71
+ def build_app(settings: HostSettings | None = None) -> FastAPI:
72
+ resolved = settings if settings is not None else load_default_settings()
73
+ app = init_app(resolved)
74
+
75
+ from myapp.bootstrap import configure_dependencies
76
+ from myapp.exception_handlers import setup_exception_handlers
77
+
78
+ configure_dependencies(runtime=app.state.core_runtime)
79
+ setup_exception_handlers(app)
80
+
81
+ from myapp.api.router import router
82
+
83
+ app.include_router(router)
84
+ return app
85
+ ```
86
+
87
+ From the host repository root, run **`uv run cf-alembic`**. See [core-framework-migration](docs/core-framework-migration.md).
88
+
89
+ ## Host pytest
90
+
91
+ In your host repository, add the **`core-framework[testing]`** extra to dev or test dependencies. In **`tests/conftest.py`**, register **`pytest_core_framework_config`** (returns **`TestConfig`**) and a session-scoped **`anyio_backend`** fixture that returns **`"asyncio"`**.
92
+
93
+ ```python
94
+ import pytest
95
+
96
+ from core_framework.testing import TestConfig
97
+
98
+ from myapp.settings import load_default_settings
99
+
100
+
101
+ def pytest_core_framework_config() -> TestConfig:
102
+ from myapp.main import build_app
103
+
104
+ return TestConfig(
105
+ settings_loader=load_default_settings,
106
+ app_factory=build_app,
107
+ )
108
+
109
+
110
+ @pytest.fixture(scope="session")
111
+ def anyio_backend() -> str:
112
+ return "asyncio"
113
+ ```
114
+
115
+ See [Package API — Testing](docs/package-api.md#testing-pytest-plugin) and [testing plugin design](docs/testing-plugin-design.md).
@@ -0,0 +1,81 @@
1
+ # Core framework
2
+
3
+ Install from PyPI as **`core-framework`**, import **`core_framework`**. MIT — [LICENSE](LICENSE). [Repository](https://github.com/NepNepFFXIV/core-framework), [PyPI](https://pypi.org/project/core-framework/).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uv add core-framework
9
+ ```
10
+
11
+ ## Local development
12
+
13
+ Use **Python 3.14**. Install **uv** and **Docker**.
14
+
15
+ ```bash
16
+ docker compose -f docker-compose.dev.yaml up -d
17
+ uv sync --locked --all-extras --dev
18
+ uv run cf-alembic
19
+ make run
20
+ ```
21
+
22
+ **Firebase:** copy **`firebase_config.example.json`** to **`firebase_config.json`** at the repo root and replace with your Firebase project’s **service account** JSON from the console.
23
+
24
+ For **`make test`**, use the Firebase CLI (Auth emulator). Stop DB/Redis: **`docker compose -f docker-compose.dev.yaml down`**.
25
+
26
+ ## Hosts
27
+
28
+ In your host app, load **`Settings`** or a **`Settings`** subclass from **`core_framework.core.settings`**, pass it to **`init_app`** from **`core_framework.main`**, then wire host-specific dependencies, exception handlers, and routers.
29
+
30
+ ```python
31
+ from core_framework.main import init_app
32
+ from fastapi import FastAPI
33
+
34
+ from myapp.settings import HostSettings, load_default_settings
35
+
36
+
37
+ def build_app(settings: HostSettings | None = None) -> FastAPI:
38
+ resolved = settings if settings is not None else load_default_settings()
39
+ app = init_app(resolved)
40
+
41
+ from myapp.bootstrap import configure_dependencies
42
+ from myapp.exception_handlers import setup_exception_handlers
43
+
44
+ configure_dependencies(runtime=app.state.core_runtime)
45
+ setup_exception_handlers(app)
46
+
47
+ from myapp.api.router import router
48
+
49
+ app.include_router(router)
50
+ return app
51
+ ```
52
+
53
+ From the host repository root, run **`uv run cf-alembic`**. See [core-framework-migration](docs/core-framework-migration.md).
54
+
55
+ ## Host pytest
56
+
57
+ In your host repository, add the **`core-framework[testing]`** extra to dev or test dependencies. In **`tests/conftest.py`**, register **`pytest_core_framework_config`** (returns **`TestConfig`**) and a session-scoped **`anyio_backend`** fixture that returns **`"asyncio"`**.
58
+
59
+ ```python
60
+ import pytest
61
+
62
+ from core_framework.testing import TestConfig
63
+
64
+ from myapp.settings import load_default_settings
65
+
66
+
67
+ def pytest_core_framework_config() -> TestConfig:
68
+ from myapp.main import build_app
69
+
70
+ return TestConfig(
71
+ settings_loader=load_default_settings,
72
+ app_factory=build_app,
73
+ )
74
+
75
+
76
+ @pytest.fixture(scope="session")
77
+ def anyio_backend() -> str:
78
+ return "asyncio"
79
+ ```
80
+
81
+ See [Package API — Testing](docs/package-api.md#testing-pytest-plugin) and [testing plugin design](docs/testing-plugin-design.md).
@@ -0,0 +1,27 @@
1
+ """add optional date_of_birth to user_profiles
2
+
3
+ Revision ID: user_add_date_of_birth
4
+ Revises: v1_user_init
5
+ Create Date: 2026-05-18 12:00:00.000000
6
+
7
+ """
8
+
9
+ from collections.abc import Sequence
10
+
11
+ from alembic import op
12
+
13
+ revision: str = "user_add_date_of_birth"
14
+ down_revision: str | Sequence[str] | None = "v1_user_init"
15
+ branch_labels: str | Sequence[str] | None = None
16
+ depends_on: str | Sequence[str] | None = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ op.execute("""
21
+ alter table user_profiles
22
+ add column date_of_birth date;
23
+ """)
24
+
25
+
26
+ def downgrade() -> None:
27
+ pass
@@ -11,8 +11,8 @@ allowed_hosts = [
11
11
  secret_key = "local-dev-secret-key-change-in-production"
12
12
 
13
13
  [avatar]
14
- base_url = "https://avatar.core-framework.com"
15
- default_url = "https://avatar.core-framework.com/default.webp"
14
+ avatar_host_domain = "https://avatar.core-framework.com"
15
+ default_avatar_image = "default.webp"
16
16
 
17
17
  [banner]
18
18
  base_url = "https://banner.core-framework.com"
@@ -7,8 +7,8 @@ allowed_hosts = $ALLOWED_HOSTS
7
7
  secret_key = "$SECRET_KEY"
8
8
 
9
9
  [avatar]
10
- base_url = "$AVATAR_BASE_URL"
11
- default_url = "$AVATAR_DEFAULT_URL"
10
+ avatar_host_domain = "$AVATAR_HOST_DOMAIN"
11
+ default_avatar_image = "$AVATAR_DEFAULT_IMAGE"
12
12
 
13
13
  [banner]
14
14
  base_url = "$BANNER_BASE_URL"
@@ -16,6 +16,7 @@ from core_framework.api.admin.users.schemas import (
16
16
  validate_note_id,
17
17
  )
18
18
  from core_framework.api.dependencies import RequiredUserID
19
+ from core_framework.api.users.mappers import profile_update_from_request
19
20
  from core_framework.api.users.shared.schemas import validate_user_id
20
21
  from core_framework.application.users.admin_service import (
21
22
  add_user_note,
@@ -104,11 +105,10 @@ async def patch_user_profile(
104
105
  request_body: AdminProfileUpdateRequest,
105
106
  ) -> None:
106
107
  validated_target_user_id = validate_user_id(user_id=target_user_id)
107
- validated_update_request = request_body.model_dump(mode="json", exclude_unset=True)
108
108
  await change_user_profile(
109
109
  actor_id=actor_id.root,
110
110
  target_user_id=validated_target_user_id.root,
111
- validated_update_request=validated_update_request,
111
+ update=profile_update_from_request(request=request_body),
112
112
  )
113
113
 
114
114
 
@@ -10,6 +10,7 @@ from core_framework.api.users.shared.schemas import (
10
10
  AvatarMixin,
11
11
  BannerMixin,
12
12
  Bio,
13
+ DateOfBirth,
13
14
  DisplayName,
14
15
  UserID,
15
16
  Username,
@@ -68,17 +69,18 @@ class AdminAccountResponse(BaseModel):
68
69
  user_id: UserID
69
70
  username: str
70
71
  role: UserRole
71
- email: str | None = None
72
+ email: str | None
72
73
  email_verified: bool
73
- deletion_scheduled_for: datetime | None = None
74
+ deletion_scheduled_for: datetime | None
74
75
 
75
76
 
76
77
  class AdminProfileResponse(AvatarMixin, BannerMixin):
77
- display_name: DisplayName | None = None
78
- bio: Bio | None = None
79
- status: UserStatus | None = None
78
+ display_name: DisplayName | None
79
+ bio: Bio | None
80
+ status: UserStatus | None
80
81
  social_links: Annotated[dict[str, str], Field(default_factory=dict)]
81
82
  profile_visibility: ProfileVisibility
83
+ date_of_birth: DateOfBirth | None
82
84
 
83
85
 
84
86
  class UserDetailResponse(BaseModel):
@@ -108,6 +110,7 @@ class AdminProfileUpdateRequest(BasePatchRequest):
108
110
  status: UserStatus | None = None
109
111
  social_links: dict[str, str] | None = None
110
112
  profile_visibility: ProfileVisibility | None = None
113
+ date_of_birth: DateOfBirth | None = None
111
114
 
112
115
 
113
116
  class UserChangeHistoryItem(BaseModel):
@@ -0,0 +1,10 @@
1
+ from core_framework.api.comments.authenticated.schemas import UpdateCommentRequest
2
+ from core_framework.domains.comment import CommentUpdate
3
+
4
+
5
+ def comment_update_from_request(*, request: UpdateCommentRequest) -> CommentUpdate:
6
+ fields_set = frozenset(request.model_fields_set)
7
+ return CommentUpdate(
8
+ fields_set=fields_set,
9
+ content=request.content.root if "content" in fields_set and request.content is not None else None,
10
+ )
@@ -1,6 +1,7 @@
1
1
  from fastapi import APIRouter, BackgroundTasks, Depends, status
2
2
  from ulid import ULID
3
3
 
4
+ from core_framework.api.comments.authenticated.mappers import comment_update_from_request
4
5
  from core_framework.api.comments.authenticated.schemas import (
5
6
  CommentReportRequest,
6
7
  CreateCommentRequest,
@@ -86,11 +87,10 @@ async def patch_comment(
86
87
  user_id: RequiredUserID,
87
88
  request_body: UpdateCommentRequest,
88
89
  ) -> None:
89
- validated_update_request = request_body.model_dump(mode="json", exclude_unset=True)
90
90
  await edit_comment(
91
91
  comment_id=str(comment_id),
92
92
  author_id=user_id.root,
93
- validated_update_request=validated_update_request,
93
+ update=comment_update_from_request(request=request_body),
94
94
  )
95
95
 
96
96
 
@@ -0,0 +1,14 @@
1
+ from core_framework.api.posts.authenticated.schemas import UpdatePostRequest
2
+ from core_framework.domains.post import PostUpdate
3
+
4
+
5
+ def post_update_from_request(*, request: UpdatePostRequest) -> PostUpdate:
6
+ fields_set = frozenset(request.model_fields_set)
7
+ return PostUpdate(
8
+ fields_set=fields_set,
9
+ content=request.content.root if "content" in fields_set and request.content is not None else None,
10
+ visibility=request.visibility if "visibility" in fields_set else None,
11
+ hashtags=(None if request.hashtags is None else [hashtag.root for hashtag in request.hashtags])
12
+ if "hashtags" in fields_set
13
+ else None,
14
+ )
@@ -2,6 +2,7 @@ from fastapi import APIRouter, BackgroundTasks, Depends, status
2
2
  from ulid import ULID
3
3
 
4
4
  from core_framework.api.dependencies import RequiredUserID, check_not_banned
5
+ from core_framework.api.posts.authenticated.mappers import post_update_from_request
5
6
  from core_framework.api.posts.authenticated.schemas import CreatePostRequest, PostReportRequest, UpdatePostRequest
6
7
  from core_framework.application.posts.aggregation_service import mark_post_stats_dirty
7
8
  from core_framework.application.posts.authenticated_service import (
@@ -34,11 +35,10 @@ async def post_post(user_id: RequiredUserID, request_body: CreatePostRequest) ->
34
35
 
35
36
  @router.patch("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
36
37
  async def patch_post(post_id: ULID, user_id: RequiredUserID, request_body: UpdatePostRequest) -> None:
37
- validated_update_request = request_body.model_dump(mode="json", exclude_unset=True)
38
38
  await edit_post(
39
39
  post_id=str(post_id),
40
40
  author_id=user_id.root,
41
- validated_update_request=validated_update_request,
41
+ update=post_update_from_request(request=request_body),
42
42
  )
43
43
 
44
44
 
@@ -0,0 +1,21 @@
1
+ from core_framework.api.users.authenticated.schemas import AccountUpdateRequest, PreferencesUpdateRequest
2
+ from core_framework.domains.user import AccountUpdate, PreferencesUpdate
3
+
4
+
5
+ def preferences_update_from_request(*, request: PreferencesUpdateRequest) -> PreferencesUpdate:
6
+ fields_set = frozenset(request.model_fields_set)
7
+ return PreferencesUpdate(
8
+ fields_set=fields_set,
9
+ theme=request.theme if "theme" in fields_set else None,
10
+ language=request.language if "language" in fields_set else None,
11
+ )
12
+
13
+
14
+ def account_update_from_request(*, request: AccountUpdateRequest) -> AccountUpdate:
15
+ fields_set = frozenset(request.model_fields_set)
16
+ return AccountUpdate(
17
+ fields_set=fields_set,
18
+ username=(request.username.root if request.username is not None else None)
19
+ if "username" in fields_set
20
+ else None,
21
+ )
@@ -5,6 +5,10 @@ from fastapi import APIRouter, BackgroundTasks, Depends, Query, Request, status
5
5
  from core_framework.api.comments.public.schemas import CommentResponse
6
6
  from core_framework.api.dependencies import RequiredFirebaseUser, RequiredUserID, check_not_banned
7
7
  from core_framework.api.posts.public.schemas import PostResponse
8
+ from core_framework.api.users.authenticated.mappers import (
9
+ account_update_from_request,
10
+ preferences_update_from_request,
11
+ )
8
12
  from core_framework.api.users.authenticated.schemas import (
9
13
  AccountResponse,
10
14
  AccountUpdateRequest,
@@ -18,6 +22,7 @@ from core_framework.api.users.authenticated.schemas import (
18
22
  ProfileUpdateRequest,
19
23
  UserReportRequest,
20
24
  )
25
+ from core_framework.api.users.mappers import profile_update_from_request
21
26
  from core_framework.api.users.shared.schemas import UsernameSuggestQuery, UserReference, validate_user_id
22
27
  from core_framework.application.comments.public_service import retrieve_my_comments
23
28
  from core_framework.application.posts.public_service import retrieve_my_posts
@@ -209,8 +214,10 @@ async def get_my_preferences(user_id: RequiredUserID) -> Any:
209
214
  dependencies=[Depends(check_not_banned)],
210
215
  )
211
216
  async def patch_my_preferences(user_id: RequiredUserID, update_request: PreferencesUpdateRequest) -> Any:
212
- validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
213
- return await change_my_preferences(user_id=user_id.root, validated_update_request=validated_update_request)
217
+ return await change_my_preferences(
218
+ user_id=user_id.root,
219
+ update=preferences_update_from_request(request=update_request),
220
+ )
214
221
 
215
222
 
216
223
  @router.get(
@@ -228,8 +235,10 @@ async def get_my_profile(user_id: RequiredUserID) -> Any:
228
235
  dependencies=[Depends(check_not_banned)],
229
236
  )
230
237
  async def patch_my_profile(user_id: RequiredUserID, update_request: ProfileUpdateRequest) -> Any:
231
- validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
232
- return await change_my_profile(user_id=user_id.root, validated_update_request=validated_update_request)
238
+ return await change_my_profile(
239
+ user_id=user_id.root,
240
+ update=profile_update_from_request(request=update_request),
241
+ )
233
242
 
234
243
 
235
244
  @router.get(
@@ -254,8 +263,10 @@ async def patch_my_account(
254
263
  firebase_user: RequiredFirebaseUser,
255
264
  update_request: AccountUpdateRequest,
256
265
  ) -> Any:
257
- validated_update_request = update_request.model_dump(mode="json", exclude_unset=True)
258
- await change_my_account(user_id=firebase_user.user_id.root, validated_update_request=validated_update_request)
266
+ await change_my_account(
267
+ user_id=firebase_user.user_id.root,
268
+ update=account_update_from_request(request=update_request),
269
+ )
259
270
  return await retrieve_my_account(
260
271
  user_id=firebase_user.user_id.root,
261
272
  email=firebase_user.email,