Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions analytics_service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM python:3.11-slim AS base

WORKDIR /app/analytics_service

COPY requirements.txt .
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

COPY app ./app

COPY docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh

ENV PYTHONPATH=/app

ENTRYPOINT ["./docker-entrypoint.sh"]

FROM base AS dev

COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt

ENV ENV=DEV

# TEST
FROM dev AS test

# COPY pytest.ini .
COPY tests ./tests/

RUN pip install --no-cache-dir pytest-cov
ENV ENV=TEST

# ---------- production ----------
FROM base AS prod

ENV ENV=PROD
Empty file.
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions analytics_service/app/api/v1/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import APIRouter


router = APIRouter(
prefix="/health",
tags=["health"],
)

@router.get("/")
def get_health():
return {"status": "OK"}
37 changes: 37 additions & 0 deletions analytics_service/app/api/v1/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from datetime import date
from fastapi import APIRouter
from analytics_service.app.service import analytics_service
from analytics_service.app.schemas.analytics import (
SummaryResponse, RevenueResponse, OrderStatsResponse,
DistributionResponse, DashboardResponse,
)

router = APIRouter(
prefix="/analytics",
tags=["stats"],
)


@router.get("/summary", response_model=SummaryResponse)
def get_summary(start_date: date | None = None, end_date: date | None = None):
return analytics_service.get_summary(start_date, end_date)


@router.get("/orders", response_model=OrderStatsResponse)
def get_order_stats(start_date: date | None = None, end_date: date | None = None):
return analytics_service.get_order_stats(start_date, end_date)


@router.get("/revenue", response_model=RevenueResponse)
def get_revenue_stats(start_date: date | None = None, end_date: date | None = None):
return analytics_service.get_revenue_stats(start_date, end_date)


@router.get("/distribution", response_model=DistributionResponse)
def get_distribution(start_date: date | None = None, end_date: date | None = None):
return analytics_service.get_distribution(start_date, end_date)


@router.get("/dashboard", response_model=DashboardResponse)
def get_dashboard(start_date: date | None = None, end_date: date | None = None):
return analytics_service.get_dashboard(start_date, end_date)
Empty file.
35 changes: 35 additions & 0 deletions analytics_service/app/clients/orders_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from cachetools import TTLCache
from fastapi import HTTPException
import requests as r

cache = TTLCache(maxsize=1, ttl=300)

ORDERS_URL = "http://orders_service/orders"
PAGE_SIZE = 100

def get_orders() -> dict:
if "orders" in cache:
return cache["orders"]

print("Fetching orders from Orders Service...")

all_items = []
offset = 0

try:
while True:
response = r.get(ORDERS_URL, params={"limit": PAGE_SIZE, "offset": offset})
response.raise_for_status()
data = response.json()

all_items.extend(data["items"])
offset += len(data["items"])

if offset >= data["total"] or not data["items"]:
break
except r.exceptions.RequestException:
raise HTTPException(status_code=503, detail="Orders service unavailable")

result = {"items": all_items, "total": len(all_items)}
cache["orders"] = result
return result
20 changes: 20 additions & 0 deletions analytics_service/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from fastapi import FastAPI
from .api.v1 import health, stats


def create_app():
fastapi_app = FastAPI(
title="Analytics Service",
description="Microservice 2",
version="1.0.0",
)
@fastapi_app.get("/")
async def root():
return {"message": "analytics_service"}

fastapi_app.include_router(stats.router)
fastapi_app.include_router(health.router)

return fastapi_app

app = create_app()
Empty file.
42 changes: 42 additions & 0 deletions analytics_service/app/schemas/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pydantic import BaseModel, field_serializer


class SummaryResponse(BaseModel):
total_orders: int
total_revenue: float
avg_order_value: float

@field_serializer('total_revenue', 'avg_order_value')
def round_currency(self, v: float) -> float:
return round(v, 2)


class RevenueResponse(BaseModel):
total_revenue: float
per_day: dict[str, float]
per_week: dict[str, float]
per_month: dict[str, float]
growth_rate: float | None


class OrderStatsResponse(BaseModel):
total_orders: int
per_day: dict[str, int]
per_week: dict[str, int]
per_month: dict[str, int]
growth_rate: float | None


class DistributionResponse(BaseModel):
largest: float
smallest: float
median: float
avg_order_value: float


class DashboardResponse(BaseModel):
summary: SummaryResponse
revenue: RevenueResponse
orders: OrderStatsResponse
distribution: DistributionResponse

Empty file.
105 changes: 105 additions & 0 deletions analytics_service/app/service/analytics_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from datetime import date, datetime
from analytics_service.app.clients import orders_client
from analytics_service.app.service import metrics_util
from analytics_service.app.schemas.analytics import (
SummaryResponse, RevenueResponse, OrderStatsResponse,
DistributionResponse, DashboardResponse,
)


def _filter_orders(orders: dict, start_date: date | None, end_date: date | None) -> dict:
if not start_date and not end_date:
return orders
filtered = [
o for o in orders['items']
if (start_date is None or datetime.fromisoformat(o['created_at']).date() >= start_date)
and (end_date is None or datetime.fromisoformat(o['created_at']).date() <= end_date)
]
return {'items': filtered, 'total': len(filtered)}


def get_summary(start_date: date | None = None, end_date: date | None = None) -> SummaryResponse:
orders = _filter_orders(orders_client.get_orders(), start_date, end_date)
total = metrics_util.calc_total_order_count(orders)
revenue = metrics_util.calc_total_revenue(orders)
return SummaryResponse(
total_orders=total,
total_revenue=revenue,
avg_order_value=revenue / total if total > 0 else 0.0,
)


def get_revenue_stats(start_date: date | None = None, end_date: date | None = None) -> RevenueResponse:
orders = _filter_orders(orders_client.get_orders(), start_date, end_date)
per_month = metrics_util.calc_revenue_per_month(orders)
return RevenueResponse(
total_revenue=metrics_util.calc_total_revenue(orders),
per_day=metrics_util.calc_revenue_per_day(orders),
per_week=metrics_util.calc_revenue_per_week(orders),
per_month=per_month,
growth_rate=metrics_util.calc_growth_rate(per_month),
)


def get_order_stats(start_date: date | None = None, end_date: date | None = None) -> OrderStatsResponse:
orders = _filter_orders(orders_client.get_orders(), start_date, end_date)
per_month = metrics_util.calc_orders_per_month(orders)
return OrderStatsResponse(
total_orders=metrics_util.calc_total_order_count(orders),
per_day=metrics_util.calc_orders_per_day(orders),
per_week=metrics_util.calc_orders_per_week(orders),
per_month=per_month,
growth_rate=metrics_util.calc_growth_rate(per_month),
)


def get_distribution(start_date: date | None = None, end_date: date | None = None) -> DistributionResponse:
orders = _filter_orders(orders_client.get_orders(), start_date, end_date)
largest = metrics_util.largest_order_by_revenue(orders)
smallest = metrics_util.smallest_order_by_revenue(orders)
return DistributionResponse(
largest=largest['total'] if largest else 0.0,
smallest=smallest['total'] if smallest else 0.0,
median=metrics_util.median_order_total(orders),
avg_order_value=metrics_util.avg_order_value(orders),
)


def get_dashboard(start_date: date | None = None, end_date: date | None = None) -> DashboardResponse:
orders = _filter_orders(orders_client.get_orders(), start_date, end_date)

total_orders = metrics_util.calc_total_order_count(orders)
total_revenue = metrics_util.calc_total_revenue(orders)
aov = metrics_util.avg_order_value(orders)
per_revenue_month = metrics_util.calc_revenue_per_month(orders)
per_orders_month = metrics_util.calc_orders_per_month(orders)
largest = metrics_util.largest_order_by_revenue(orders)
smallest = metrics_util.smallest_order_by_revenue(orders)

return DashboardResponse(
summary=SummaryResponse(
total_orders=total_orders,
total_revenue=total_revenue,
avg_order_value=aov,
),
revenue=RevenueResponse(
total_revenue=total_revenue,
per_day=metrics_util.calc_revenue_per_day(orders),
per_week=metrics_util.calc_revenue_per_week(orders),
per_month=per_revenue_month,
growth_rate=metrics_util.calc_growth_rate(per_revenue_month),
),
orders=OrderStatsResponse(
total_orders=total_orders,
per_day=metrics_util.calc_orders_per_day(orders),
per_week=metrics_util.calc_orders_per_week(orders),
per_month=per_orders_month,
growth_rate=metrics_util.calc_growth_rate(per_orders_month),
),
distribution=DistributionResponse(
largest=largest['total'] if largest else 0.0,
smallest=smallest['total'] if smallest else 0.0,
median=metrics_util.median_order_total(orders),
avg_order_value=aov,
),
)
Loading
Loading