From b8e595e968577537ed30d88a553c1d85b77274a7 Mon Sep 17 00:00:00 2001 From: Andrii Ostapenko Date: Sun, 31 Aug 2025 16:58:10 +0300 Subject: [PATCH] added product analitics --- Dockerfile | 2 +- docker-compose.yml | 2 ++ env.example | 3 +++ src/reviews/models/mongo.py | 8 +++++++ src/reviews/repositories.py | 5 ++++- src/reviews/services.py | 16 +++++++++++++- src/reviews/views/product_reviews.py | 31 +++++++++++++++++++++++++++- 7 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1410224..9fda0358 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY pyproject.toml pyproject.toml RUN pip install poetry RUN poetry config virtualenvs.create false -RUN poetry install --no-dev +RUN poetry install --no-root COPY . /app diff --git a/docker-compose.yml b/docker-compose.yml index c12fd799..f71b3dc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: - mongo-db networks: - web_network + env_file: + - env.example mongo-db: image: mongo:latest diff --git a/env.example b/env.example index 68a0f760..480c74fa 100644 --- a/env.example +++ b/env.example @@ -4,3 +4,6 @@ POSTGRES_DB=fastapi_shop API_KEY=some-api-key AUTH__SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +AUTH__ALGORITHM=HS256 +AUTH__ACCESS_TOKEN_EXPIRE_MINUTES=30 +AUTH__CRYPT_SCHEMA=bcrypt \ No newline at end of file diff --git a/src/reviews/models/mongo.py b/src/reviews/models/mongo.py index f19b77ff..db8b7be3 100644 --- a/src/reviews/models/mongo.py +++ b/src/reviews/models/mongo.py @@ -1,5 +1,6 @@ import uuid from typing import Optional +from datetime import datetime from beanie import Document from pydantic import ( @@ -28,3 +29,10 @@ class ProductReview(Document, BaseProductReview): class Settings: name = 'productReviews' + +class ProductAnalytics(Document): + product_id: int + timestamp: datetime = Field(default_factory=datetime.utcnow) + + class Settings: + name = "product_analytics" \ No newline at end of file diff --git a/src/reviews/repositories.py b/src/reviews/repositories.py index 9a96d08c..f2c9355f 100644 --- a/src/reviews/repositories.py +++ b/src/reviews/repositories.py @@ -1,6 +1,9 @@ from src.common.repository.beanie import BaseMongoRepository -from src.reviews.models.mongo import ProductReview +from src.reviews.models.mongo import ProductReview, ProductAnalytics class ProductReviewRepository(BaseMongoRepository[ProductReview]): __model__ = ProductReview + +class ProductAnalyticsRepository(BaseMongoRepository[ProductAnalytics]): + __model__ = ProductAnalytics \ No newline at end of file diff --git a/src/reviews/services.py b/src/reviews/services.py index 13afdef1..d4cb996c 100644 --- a/src/reviews/services.py +++ b/src/reviews/services.py @@ -2,13 +2,16 @@ from fastapi import Depends +from datetime import datetime + from src.common.exceptions.base import ObjectDoesNotExistException from src.common.service import BaseService from src.reviews.models.mongo import ( ProductReview, Reply, + ProductAnalytics, ) -from src.reviews.repositories import ProductReviewRepository +from src.reviews.repositories import ProductReviewRepository, ProductAnalyticsRepository class ProductReviewService(BaseService): @@ -44,3 +47,14 @@ async def add_reply(self, pk: str, reply: Reply) -> ProductReview: review.replies.append(reply.model_dump()) return await review.save() + +class ProductAnalyticsService(BaseService): + def __init__(self, repository: Annotated[ProductAnalyticsRepository, Depends(ProductAnalyticsRepository)]): + super().__init__(repository=repository) + + async def log_visit(self, product_id: int) -> ProductAnalytics: + visit = ProductAnalytics( + product_id=product_id, + timestamp=datetime.utcnow() + ) + return await self.repository.create(visit) \ No newline at end of file diff --git a/src/reviews/views/product_reviews.py b/src/reviews/views/product_reviews.py index b934ec73..d44c371a 100644 --- a/src/reviews/views/product_reviews.py +++ b/src/reviews/views/product_reviews.py @@ -8,6 +8,7 @@ Depends, Response, status, + HTTPException, ) from src.common.exceptions.base import ObjectDoesNotExistException @@ -21,7 +22,8 @@ ProductReviewRoutesPrefixes, ReviewsRoutesPrefixes, ) -from src.reviews.services import ProductReviewService +from src.reviews.services import ProductReviewService, ProductAnalyticsService +from src.reviews.repositories import ProductAnalyticsRepository, ProductReviewRepository router = APIRouter(prefix=ReviewsRoutesPrefixes.product_reviews) @@ -113,3 +115,30 @@ async def add_reply_to_review( return ErrorResponse(message=exc.message) return response + +def get_review_service() -> ProductReviewService: + return ProductReviewService(ProductReviewRepository()) + + +def get_analytics_service() -> ProductAnalyticsService: + return ProductAnalyticsService(ProductAnalyticsRepository()) + + +@router.get("/{product_id}") +async def product_detail( + product_id: int, + review_service: Annotated[ProductReviewService, Depends(get_review_service)], + analytics_service: Annotated[ProductAnalyticsService, Depends(get_analytics_service)], +): + try: + product: ProductReview = await review_service.repository.get(pk=str(product_id)) + except ObjectDoesNotExistException as exc: + raise HTTPException(status_code=404, detail=exc.message) + + await analytics_service.log_visit(product_id) + + return { + "id": str(product.id), + "title": product.title, + "description": product.description, + } \ No newline at end of file