core-framework 1.3.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.
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/constants-final.mdc +1 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/database-triggers.md +1 -24
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/domain-input-guards.mdc +3 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/.dockerignore +2 -3
- {core_framework-1.3.0 → core_framework-1.4.0}/.github/workflows/_deploy.yml +2 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/CHANGELOG.md +31 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/PKG-INFO +1 -1
- core_framework-1.4.0/alembic/user/alembic/versions/user_add_date_of_birth_to_profiles.py +27 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/config.toml +2 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/config.toml.template +2 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/users/router.py +2 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/users/schemas.py +8 -5
- core_framework-1.4.0/core_framework/api/comments/authenticated/mappers.py +10 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/authenticated/router.py +2 -2
- core_framework-1.4.0/core_framework/api/posts/authenticated/mappers.py +14 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/authenticated/router.py +2 -2
- core_framework-1.4.0/core_framework/api/users/authenticated/mappers.py +21 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/router.py +17 -6
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/schemas.py +3 -0
- core_framework-1.4.0/core_framework/api/users/mappers.py +77 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/shared/schemas.py +35 -6
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/comments/authenticated_service.py +3 -9
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/posts/authenticated_service.py +3 -9
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/admin_service.py +17 -12
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/authenticated_service.py +19 -23
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/settings.py +16 -3
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/__init__.py +2 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/models.py +30 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/repository.py +4 -5
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/service.py +7 -8
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/__init__.py +2 -1
- core_framework-1.4.0/core_framework/domains/post/models.py +98 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/repository.py +5 -12
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/service.py +7 -12
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/__init__.py +6 -0
- core_framework-1.4.0/core_framework/domains/user/constants.py +11 -0
- core_framework-1.4.0/core_framework/domains/user/models.py +269 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/repository.py +23 -25
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/service.py +14 -38
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/utils.py +0 -13
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/main.py +2 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/dockerfile +1 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/conventions.md +8 -1
- core_framework-1.4.0/docs/flows/media/upload_pipeline.md +454 -0
- core_framework-1.4.0/docs/flows/users/avatar.md +91 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/change_history.md +2 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/profile.md +10 -11
- core_framework-1.4.0/docs/patch-update-typed-payload.md +186 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/todo.md +1 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/pyproject.toml +1 -1
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/users/router_test.py +42 -2
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/users/authenticated/router_test.py +90 -12
- {core_framework-1.3.0 → core_framework-1.4.0}/uv.lock +1 -1
- core_framework-1.3.0/core_framework/domains/post/models.py +0 -61
- core_framework-1.3.0/core_framework/domains/user/constants.py +0 -41
- core_framework-1.3.0/core_framework/domains/user/models.py +0 -153
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/api-layer.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/api-reference-docs.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/api-security.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/api-validation.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/application-layer.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/domain-caller-context.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/domain-imports.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/domain-repository-exceptions.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/flow-documentation.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/implementation-workflow.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/integration-test-strategy.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/layer-boundaries.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/no-code-in-docs.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/no-docstrings.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/postgres-config-conventions.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/repository-read-consistency.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/strong-read-opt-in.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/rules/tech-stack.mdc +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/skills/add-config/SKILL.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/skills/add-domain/SKILL.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/skills/code-review/SKILL.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.cursor/skills/recommend-features/SKILL.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.github/workflows/dev-ci-cd.yaml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.github/workflows/manual-deployment.yaml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.github/workflows/publish-pypi.yml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.github/workflows/test.yaml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.gitignore +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.pre-commit-config.yaml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/.python-version +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/LICENSE +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/comment/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/comment/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/comment/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/comment/alembic/versions/v1_comment_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/comment/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/extension/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/extension/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/extension/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/extension/alembic/versions/v1_ext_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/extension/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/moderation/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/moderation/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/moderation/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/moderation/alembic/versions/v1_mod_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/moderation/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/notification/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/notification/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/notification/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/notification/alembic/versions/v1_notif_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/notification/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/post/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/post/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/post/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/post/alembic/versions/v1_post_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/post/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/user/alembic/README +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/user/alembic/env.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/user/alembic/script.py.mako +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/user/alembic/versions/v1_user_init_baseline.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/alembic/user/alembic.ini +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/comments/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/comments/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/moderation/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/posts/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/posts/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/admin/users/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/auth/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/auth/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/auth/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/authenticated/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/public/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/public/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/constants.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/events/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/events/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/notifications/authenticated/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/notifications/authenticated/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/notifications/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/authenticated/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/public/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/public/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/system/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/system/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/public/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/public/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/public/schemas.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/router.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/auth/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/auth/access_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/auth/auth_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/auth/models.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/bootstrap.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/comments/admin_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/comments/aggregation_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/comments/public_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/events/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/events/event_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/events/event_token.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/events/models.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/appeal_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/moderator_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/report_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/scheduled_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/moderation/user_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/notifications/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/notifications/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/notifications/inbox_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/notifications/mute_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/notifications/notification_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/posts/admin_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/posts/aggregation_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/posts/public_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/shared/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/shared/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/shared/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/shared/user_agent.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/aggregation_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/public_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/application/users/scheduled_service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/asgi.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/bundled_alembic.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/constants.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/cache.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/context.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/database.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/comment.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/common.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/moderation.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/notification.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/post.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/setup.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/exception_handlers/user.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/firebase.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/http_client.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/logging.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/middleware.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/observability.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/pagination.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/redis.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/core/runtime.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/constants.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/comment/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/models.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/repository.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/moderation/service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/models.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/repository.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/notification/service.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/constants.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/post/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/README.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/dependencies.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/enums.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/exceptions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/domains/user/utils.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/migrate_cli.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/arq.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/auth.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/config.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/containers.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/firebase.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/hookspecs.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/httpx_test_client.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/migrations.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/testing/plugin.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/main.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_comment_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_post_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_aggregate_user_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_expired_account_deletions.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/schedules/schedule_expired_mute_lifts.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_account_deletion.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_comment_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_post_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_aggregate_user_stats.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/tasks/process_mute_lift.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/core_framework/worker/worker_context.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docker-compose.dev.yaml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docker-compose.yml +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/api.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/architecture-decisions.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/architecture.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/core-framework-migration.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/database-triggers.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/event-outbox-design.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/auth/access_control.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/auth/registration.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/admin_comments.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/comment_report.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/comment_stats_aggregation.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/create_comment.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/delete_comment.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/edit_comment.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/comments/retrieve_comments.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/events/events.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/mentions/mentions_in_content.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/moderation/appeals.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/moderation/internal_notes.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/moderation/moderator_actions.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/moderation/reports.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/moderation/restrictions.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/notifications/notification_inbox.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/posts/admin_posts.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/posts/author_context.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/posts/hashtag_discovery.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/posts/post_like.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/posts/post_stats_aggregation.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/account.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/account_deletion.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/blocks.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/check_username_exists.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/follow.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/my_posts_and_comments.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/preferences.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/flows/users/user_removal.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/follow-system-design.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/package-api.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/docs/testing-plugin-design.md +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/firebase_config.example.json +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/makefile +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/conftest.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/_http_helpers.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/comments/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/comments/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/moderation/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/moderation/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/posts/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/posts/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/admin/users/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/auth/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/auth/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/comments/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/comments/authenticated/comment_writes_integration_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/comments/public/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/comments/public/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/events/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/notifications/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/notifications/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/authenticated/post_writes_integration_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/comment_count_aggregation_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/followers_visibility_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/post_stats_dirty_marking_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/public/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/posts/public/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/system/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/system/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/users/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/users/authenticated/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/users/public/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/api/users/public/router_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/account_deletion_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_comment_stats_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_post_stats_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/aggregate_user_stats_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/conftest.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/mute_lift_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/integration/worker/utils_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/application/comments/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/application/events/event_service_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/application/notifications/inbox_service_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/core/bundled_alembic_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/core/migrate_cli_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/core/pagination_test.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/domains/__init__.py +0 -0
- {core_framework-1.3.0 → core_framework-1.4.0}/tests/unit/domains/comment/__init__.py +0 -0
- {core_framework-1.3.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. **`
|
|
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`**.
|
|
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.
|
|
@@ -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
|
-
|
|
43
|
-
|
|
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 }}
|
|
@@ -4,6 +4,35 @@ 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
|
+
|
|
7
36
|
## [1.3.0] - 2026-05-16
|
|
8
37
|
|
|
9
38
|
### Added
|
|
@@ -140,4 +169,5 @@ First **SemVer-stable** release per **`docs/package-api.md`**.
|
|
|
140
169
|
[1.1.1]: https://github.com/NepNepFFXIV/core-framework/compare/v1.1.0...v1.1.1
|
|
141
170
|
[1.2.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.1.1...v1.2.0
|
|
142
171
|
[1.3.0]: https://github.com/NepNepFFXIV/core-framework/compare/v1.2.0...v1.3.0
|
|
143
|
-
[
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-framework
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Core framework package (import as core_framework)
|
|
5
5
|
Project-URL: Homepage, https://github.com/NepNepFFXIV/core-framework
|
|
6
6
|
Project-URL: Repository, https://github.com/NepNepFFXIV/core-framework
|
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
72
|
+
email: str | None
|
|
72
73
|
email_verified: bool
|
|
73
|
-
deletion_scheduled_for: datetime | None
|
|
74
|
+
deletion_scheduled_for: datetime | None
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
class AdminProfileResponse(AvatarMixin, BannerMixin):
|
|
77
|
-
display_name: DisplayName | None
|
|
78
|
-
bio: Bio | None
|
|
79
|
-
status: UserStatus | 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
|
+
)
|
{core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/comments/authenticated/router.py
RENAMED
|
@@ -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
|
-
|
|
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
|
+
)
|
{core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/posts/authenticated/router.py
RENAMED
|
@@ -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
|
-
|
|
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
|
+
)
|
{core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/router.py
RENAMED
|
@@ -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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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,
|
{core_framework-1.3.0 → core_framework-1.4.0}/core_framework/api/users/authenticated/schemas.py
RENAMED
|
@@ -9,6 +9,7 @@ from core_framework.api.users.shared.schemas import (
|
|
|
9
9
|
AvatarMixin,
|
|
10
10
|
BannerMixin,
|
|
11
11
|
Bio,
|
|
12
|
+
DateOfBirth,
|
|
12
13
|
DisplayName,
|
|
13
14
|
UserID,
|
|
14
15
|
Username,
|
|
@@ -37,6 +38,7 @@ class ProfileResponse(AvatarMixin, BannerMixin):
|
|
|
37
38
|
status: UserStatus | None
|
|
38
39
|
social_links: dict[str, str]
|
|
39
40
|
profile_visibility: ProfileVisibility
|
|
41
|
+
date_of_birth: DateOfBirth | None
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class AccountResponse(BaseModel):
|
|
@@ -58,6 +60,7 @@ class ProfileUpdateRequest(BasePatchRequest):
|
|
|
58
60
|
status: UserStatus | None = None
|
|
59
61
|
social_links: dict[str, str] | None = None
|
|
60
62
|
profile_visibility: ProfileVisibility | None = None
|
|
63
|
+
date_of_birth: DateOfBirth | None = None
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
class UserReportRequest(BaseModel):
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
from ulid import ULID
|
|
4
|
+
|
|
5
|
+
from core_framework.api.admin.users.schemas import AdminProfileUpdateRequest
|
|
6
|
+
from core_framework.api.users.authenticated.schemas import ProfileUpdateRequest
|
|
7
|
+
from core_framework.domains.user import ProfileUpdate
|
|
8
|
+
|
|
9
|
+
ProfilePatchRequest = ProfileUpdateRequest | AdminProfileUpdateRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def profile_update_from_request(*, request: ProfilePatchRequest) -> ProfileUpdate:
|
|
13
|
+
fields_set = frozenset(request.model_fields_set)
|
|
14
|
+
return ProfileUpdate(
|
|
15
|
+
fields_set=fields_set,
|
|
16
|
+
display_name=_display_name_from_request(request=request, fields_set=fields_set),
|
|
17
|
+
avatar_id=_ulid_string_from_request(
|
|
18
|
+
value=request.avatar_id,
|
|
19
|
+
field="avatar_id",
|
|
20
|
+
fields_set=fields_set,
|
|
21
|
+
),
|
|
22
|
+
banner_id=_ulid_string_from_request(
|
|
23
|
+
value=request.banner_id,
|
|
24
|
+
field="banner_id",
|
|
25
|
+
fields_set=fields_set,
|
|
26
|
+
),
|
|
27
|
+
bio=_bio_from_request(request=request, fields_set=fields_set),
|
|
28
|
+
status=_status_from_request(request=request, fields_set=fields_set),
|
|
29
|
+
social_links=request.social_links if "social_links" in fields_set else None,
|
|
30
|
+
profile_visibility=request.profile_visibility if "profile_visibility" in fields_set else None,
|
|
31
|
+
date_of_birth=_date_of_birth_from_request(request=request, fields_set=fields_set),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _display_name_from_request(*, request: ProfilePatchRequest, fields_set: frozenset[str]) -> str | None:
|
|
36
|
+
if "display_name" not in fields_set:
|
|
37
|
+
return None
|
|
38
|
+
if request.display_name is None:
|
|
39
|
+
return None
|
|
40
|
+
return request.display_name.root
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _bio_from_request(*, request: ProfilePatchRequest, fields_set: frozenset[str]) -> str | None:
|
|
44
|
+
if "bio" not in fields_set:
|
|
45
|
+
return None
|
|
46
|
+
if request.bio is None:
|
|
47
|
+
return None
|
|
48
|
+
return request.bio.root
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _status_from_request(*, request: ProfilePatchRequest, fields_set: frozenset[str]) -> str | None:
|
|
52
|
+
if "status" not in fields_set:
|
|
53
|
+
return None
|
|
54
|
+
if request.status is None:
|
|
55
|
+
return None
|
|
56
|
+
return request.status.root
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _date_of_birth_from_request(*, request: ProfilePatchRequest, fields_set: frozenset[str]) -> date | None:
|
|
60
|
+
if "date_of_birth" not in fields_set:
|
|
61
|
+
return None
|
|
62
|
+
if request.date_of_birth is None:
|
|
63
|
+
return None
|
|
64
|
+
return request.date_of_birth.root
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _ulid_string_from_request(
|
|
68
|
+
*,
|
|
69
|
+
value: ULID | None,
|
|
70
|
+
field: str,
|
|
71
|
+
fields_set: frozenset[str],
|
|
72
|
+
) -> str | None:
|
|
73
|
+
if field not in fields_set:
|
|
74
|
+
return None
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
return str(value)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from datetime import date
|
|
1
2
|
from typing import Annotated
|
|
2
3
|
|
|
3
4
|
from fastapi.exceptions import RequestValidationError
|
|
@@ -75,6 +76,12 @@ def _get_user_media_urls() -> dict[str, str]:
|
|
|
75
76
|
raise RuntimeError(msg)
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class AvatarUrls(BaseModel):
|
|
80
|
+
size_128: HttpUrl
|
|
81
|
+
size_500: HttpUrl
|
|
82
|
+
fallback: HttpUrl
|
|
83
|
+
|
|
84
|
+
|
|
78
85
|
class UserID(RootModel[str]):
|
|
79
86
|
model_config = ConfigDict(frozen=True)
|
|
80
87
|
root: Annotated[
|
|
@@ -140,14 +147,28 @@ class DisplayName(RootModel[str]):
|
|
|
140
147
|
]
|
|
141
148
|
|
|
142
149
|
|
|
150
|
+
class DateOfBirth(RootModel[date]):
|
|
151
|
+
model_config = ConfigDict(frozen=True)
|
|
152
|
+
root: Annotated[
|
|
153
|
+
date,
|
|
154
|
+
Field(description="User date of birth (calendar date)"),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
@field_validator("root", mode="after")
|
|
158
|
+
@classmethod
|
|
159
|
+
def not_future(cls, v: date) -> date:
|
|
160
|
+
if v > date.today():
|
|
161
|
+
raise ValueError("date_of_birth cannot be in the future")
|
|
162
|
+
return v
|
|
163
|
+
|
|
164
|
+
|
|
143
165
|
class AvatarMixin(BaseModel):
|
|
144
166
|
avatar_id: Annotated[str | None, Field(exclude=True)]
|
|
145
167
|
|
|
146
168
|
@computed_field
|
|
147
169
|
@property
|
|
148
|
-
def avatar(self) ->
|
|
149
|
-
|
|
150
|
-
return _http_url_adapter.validate_python(url_string)
|
|
170
|
+
def avatar(self) -> AvatarUrls:
|
|
171
|
+
return _construct_avatar_urls(avatar_id=self.avatar_id)
|
|
151
172
|
|
|
152
173
|
|
|
153
174
|
class BannerMixin(BaseModel):
|
|
@@ -179,11 +200,19 @@ def validate_optional_user_id(*, user_id: str | None) -> UserID | None:
|
|
|
179
200
|
return validate_user_id(user_id=user_id)
|
|
180
201
|
|
|
181
202
|
|
|
182
|
-
def
|
|
203
|
+
def _construct_avatar_urls(*, avatar_id: str | None) -> AvatarUrls:
|
|
183
204
|
media_urls = _get_user_media_urls()
|
|
205
|
+
fallback = media_urls["avatar_default_url"]
|
|
206
|
+
base = media_urls["avatar_base_url"]
|
|
207
|
+
ext = media_urls["avatar_extension"]
|
|
184
208
|
if avatar_id is None:
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
fb = _http_url_adapter.validate_python(fallback)
|
|
210
|
+
return AvatarUrls(size_128=fb, size_500=fb, fallback=fb)
|
|
211
|
+
return AvatarUrls(
|
|
212
|
+
size_128=_http_url_adapter.validate_python(f"{base}/128/{avatar_id}.{ext}"),
|
|
213
|
+
size_500=_http_url_adapter.validate_python(f"{base}/500/{avatar_id}.{ext}"),
|
|
214
|
+
fallback=_http_url_adapter.validate_python(fallback),
|
|
215
|
+
)
|
|
187
216
|
|
|
188
217
|
|
|
189
218
|
def _construct_banner_url(*, banner_id: str | None) -> str:
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
1
|
import core_framework.application.notifications.notification_service as notification_service
|
|
4
2
|
import core_framework.domains.comment.dependencies as comment_deps
|
|
5
3
|
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
6
4
|
from core_framework.application.comments import aggregation_service as comment_aggregation_service
|
|
7
5
|
from core_framework.application.posts import aggregation_service as post_aggregation_service
|
|
8
|
-
from core_framework.domains.comment import CommentStatsImpact, CommentSubjectType
|
|
6
|
+
from core_framework.domains.comment import CommentStatsImpact, CommentSubjectType, CommentUpdate
|
|
9
7
|
from core_framework.domains.moderation import ReportCategory
|
|
10
8
|
|
|
11
9
|
|
|
@@ -63,13 +61,9 @@ async def edit_comment(
|
|
|
63
61
|
*,
|
|
64
62
|
comment_id: str,
|
|
65
63
|
author_id: str,
|
|
66
|
-
|
|
64
|
+
update: CommentUpdate,
|
|
67
65
|
) -> None:
|
|
68
|
-
await comment_deps.comment_service.edit_comment(
|
|
69
|
-
comment_id=comment_id,
|
|
70
|
-
author_id=author_id,
|
|
71
|
-
validated_update_request=validated_update_request,
|
|
72
|
-
)
|
|
66
|
+
await comment_deps.comment_service.edit_comment(comment_id=comment_id, author_id=author_id, update=update)
|
|
73
67
|
|
|
74
68
|
|
|
75
69
|
async def like_comment(*, comment_id: str, user_id: str) -> bool:
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
1
|
import core_framework.application.notifications.notification_service as notification_service
|
|
4
2
|
import core_framework.domains.moderation.dependencies as moderation_deps
|
|
5
3
|
import core_framework.domains.post.dependencies as post_deps
|
|
6
4
|
from core_framework.domains.moderation import ReportCategory
|
|
7
|
-
from core_framework.domains.post import PostStatus, PostVisibility
|
|
5
|
+
from core_framework.domains.post import PostStatus, PostUpdate, PostVisibility
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
async def create_post(
|
|
@@ -33,13 +31,9 @@ async def edit_post(
|
|
|
33
31
|
*,
|
|
34
32
|
post_id: str,
|
|
35
33
|
author_id: str,
|
|
36
|
-
|
|
34
|
+
update: PostUpdate,
|
|
37
35
|
) -> None:
|
|
38
|
-
await post_deps.post_service.edit_post(
|
|
39
|
-
post_id=post_id,
|
|
40
|
-
author_id=author_id,
|
|
41
|
-
validated_update_request=validated_update_request,
|
|
42
|
-
)
|
|
36
|
+
await post_deps.post_service.edit_post(post_id=post_id, author_id=author_id, update=update)
|
|
43
37
|
|
|
44
38
|
|
|
45
39
|
async def delete_post(*, post_id: str, author_id: str) -> None:
|