Skip to content

Commit 43d9983

Browse files
authored
Merge pull request #19 from KillrVideo/ci/container-release
fix: Data API table compatibility — ratings, views, activity, warning suppression
2 parents 76538e2 + bb9e0e7 commit 43d9983

21 files changed

Lines changed: 1855 additions & 195 deletions

.dockerignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.git
2+
.github
3+
.claude
4+
.venv
5+
.env
6+
.env.*
7+
__pycache__
8+
*.pyc
9+
*.pyo
10+
certs/
11+
data/
12+
logs/
13+
docs/
14+
scripts/
15+
tests/
16+
*.log
17+
.DS_Store
18+
.vscode
19+
VIDEO_DESCRIPTION_FIX.md
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Container Release
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
tags: ["v*"]
9+
10+
permissions:
11+
contents: read
12+
packages: write
13+
14+
jobs:
15+
build-and-push:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Docker Buildx
22+
uses: docker/setup-buildx-action@v3
23+
24+
- name: Log in to ghcr.io
25+
uses: docker/login-action@v3
26+
with:
27+
registry: ghcr.io
28+
username: ${{ github.actor }}
29+
password: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Extract metadata
32+
id: meta
33+
uses: docker/metadata-action@v5
34+
with:
35+
images: ghcr.io/killrvideo/kv-be-python-fastapi-dataapi-table
36+
tags: |
37+
type=sha,prefix=sha-
38+
type=raw,value=latest,enable={{is_default_branch}}
39+
type=semver,pattern={{version}}
40+
type=semver,pattern={{major}}.{{minor}}
41+
type=semver,pattern={{major}}
42+
43+
- name: Build and push
44+
uses: docker/build-push-action@v6
45+
with:
46+
context: .
47+
push: ${{ github.event_name != 'pull_request' }}
48+
tags: ${{ steps.meta.outputs.tags }}
49+
labels: ${{ steps.meta.outputs.labels }}
50+
cache-from: type=gha
51+
cache-to: type=gha,mode=max

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.env
22
.python-version
3-
.DS_Store
3+
.DS_Store
44

55
# Python
66
__pycache__/
@@ -15,6 +15,11 @@ __pycache__/
1515

1616
*.log
1717

18+
# Data files (source of truth is in killrvideo-data project)
19+
data/
20+
logs/
21+
dsbulk.conf
22+
1823
certs/
1924

2025
.vscode/

Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
FROM python:3.12-slim AS builder
2+
3+
WORKDIR /app
4+
5+
RUN pip install --no-cache-dir poetry && \
6+
poetry config virtualenvs.in-project true
7+
8+
COPY pyproject.toml poetry.lock ./
9+
RUN poetry install --only main --no-root --no-interaction
10+
11+
COPY README.md ./
12+
COPY app/ app/
13+
RUN poetry install --only main --no-interaction
14+
15+
# ---------------------------------------------------------------------------
16+
17+
FROM python:3.12-slim
18+
19+
WORKDIR /app
20+
21+
COPY --from=builder /app/.venv /app/.venv
22+
COPY --from=builder /app/app /app/app
23+
24+
ENV PATH="/app/.venv/bin:$PATH" \
25+
PYTHONUNBUFFERED=1
26+
27+
EXPOSE 8000
28+
29+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""API endpoint for querying user activity timelines."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated, Literal, Optional
6+
from uuid import UUID
7+
8+
from fastapi import APIRouter, Depends, Query
9+
10+
from app.api.v1.dependencies import PaginationParams
11+
from app.models.common import PaginatedResponse, Pagination
12+
from app.models.user_activity import UserActivityResponse
13+
from app.services import user_activity_service
14+
15+
router = APIRouter(tags=["User Activity"])
16+
17+
18+
@router.get(
19+
"/users/{user_id_path}/activity",
20+
response_model=PaginatedResponse[UserActivityResponse],
21+
summary="Get user activity timeline",
22+
)
23+
async def get_user_activity(
24+
user_id_path: UUID,
25+
pagination: Annotated[PaginationParams, Depends()],
26+
activity_type: Optional[Literal["view", "comment", "rate"]] = Query(
27+
None, description="Filter by activity type (view, comment, rate)"
28+
),
29+
):
30+
"""Return a paginated timeline of a user's activity over the last 30 days."""
31+
32+
activities, total = await user_activity_service.list_user_activity(
33+
userid=user_id_path,
34+
page=pagination.page,
35+
page_size=pagination.pageSize,
36+
activity_type=activity_type,
37+
)
38+
39+
total_pages = (total + pagination.pageSize - 1) // pagination.pageSize
40+
41+
response_items = [UserActivityResponse.model_validate(a) for a in activities]
42+
43+
return PaginatedResponse[UserActivityResponse](
44+
data=response_items,
45+
pagination=Pagination(
46+
currentPage=pagination.page,
47+
pageSize=pagination.pageSize,
48+
totalItems=total,
49+
totalPages=total_pages,
50+
),
51+
)

app/api/v1/endpoints/video_catalog.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,10 @@ async def record_view(
256256
)
257257

258258
# READY – record the view
259-
await video_service.record_video_view(video_id_path)
259+
await video_service.record_video_view(
260+
video_id_path,
261+
viewer_user_id=current_user.userid if current_user else None,
262+
)
260263
return Response(status_code=status.HTTP_204_NO_CONTENT)
261264

262265

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
reco_internal,
2929
flags,
3030
moderation,
31+
user_activity,
3132
)
3233

3334
# --------------------------------------------------------------
@@ -67,6 +68,7 @@
6768
api_router_v1.include_router(reco_internal.router)
6869
api_router_v1.include_router(flags.router)
6970
api_router_v1.include_router(moderation.router)
71+
api_router_v1.include_router(user_activity.router)
7072

7173
app.include_router(api_router_v1)
7274

app/models/user_activity.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Pydantic models for user activity tracking."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime, timezone
6+
from typing import Literal, Optional
7+
from uuid import UUID
8+
9+
from pydantic import BaseModel, Field, ConfigDict
10+
11+
from app.models.common import UserID
12+
13+
ACTIVITY_TYPES = Literal["view", "comment", "rate"]
14+
15+
16+
class UserActivity(BaseModel):
17+
"""Internal representation of a user activity row.
18+
19+
Field names match DB column names (snake_case) exactly — no aliases needed.
20+
"""
21+
22+
model_config = ConfigDict(populate_by_name=True, from_attributes=True)
23+
24+
userid: UserID
25+
day: str
26+
activity_type: ACTIVITY_TYPES
27+
activity_id: UUID
28+
activity_timestamp: datetime
29+
30+
31+
class UserActivityResponse(BaseModel):
32+
"""API response representation for a single user activity item."""
33+
34+
model_config = ConfigDict(populate_by_name=True, from_attributes=True)
35+
36+
userId: UserID = Field(..., validation_alias="userid")
37+
activityType: str = Field(..., validation_alias="activity_type")
38+
activityId: UUID = Field(..., validation_alias="activity_id")
39+
activityTimestamp: datetime = Field(..., validation_alias="activity_timestamp")
40+
41+
42+
__all__ = [
43+
"ACTIVITY_TYPES",
44+
"UserActivity",
45+
"UserActivityResponse",
46+
]

app/services/comment_service.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
from typing import Optional, List, Tuple
67
from uuid import UUID, uuid1
78

@@ -15,10 +16,13 @@
1516
from app.external_services.sentiment_mock import MockSentimentAnalyzer
1617
import inspect # local import to avoid new dependency
1718
from app.utils.db_helpers import safe_count
19+
from app.services.user_activity_service import record_user_activity
1820

1921
# testing mocks
2022
from unittest.mock import AsyncMock, MagicMock
2123

24+
logger = logging.getLogger(__name__)
25+
2226
COMMENTS_BY_VIDEO_TABLE_NAME = "comments"
2327
COMMENTS_BY_USER_TABLE_NAME = "comments_by_user"
2428

@@ -90,6 +94,18 @@ async def add_comment_to_video(
9094
await comments_by_video_table.insert_one(document=comment_doc)
9195
await comments_by_user_table.insert_one(document=comment_doc)
9296

97+
# Track in user_activity (never fail the comment operation)
98+
try:
99+
await record_user_activity(
100+
userid=current_user.userid,
101+
activity_type="comment",
102+
activity_id=comment_id,
103+
)
104+
except Exception:
105+
logger.warning(
106+
"user_activity insert failed for comment; ignoring", exc_info=True
107+
)
108+
93109
return new_comment
94110

95111

0 commit comments

Comments
 (0)