Skip to content

Commit

Permalink
build(backend): add itrm to dev deps for stats graphs in terminal
Browse files Browse the repository at this point in the history
  • Loading branch information
spwoodcock committed Feb 13, 2025
1 parent 83de3fa commit 431d3f9
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ license = {text = "GPL-3.0-only"}
dev = [
"pre-commit>=4.0.1",
"commitizen>=3.30.0",
"itrm>=1.4.15",
]
test = [
"pytest==8.3.3",
Expand Down
1 change: 1 addition & 0 deletions src/backend/stats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Scripts using the API to generate FMTM statistics."""
140 changes: 140 additions & 0 deletions src/backend/stats/timeseries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Get number of FMTM projects over time."""

import asyncio
import calendar
import os
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from typing import List, Optional

import httpx
import itrm
import numpy as np
from pydantic import AwareDatetime, BaseModel

# Access from the same machine: http://localhost:7052
# Access from the docker container: http://localhost:8000
# But we access from another container on same container network
API_BASE_URL = os.getenv("API_URL", "http://api:8000")
START_YEAR = os.getenv("START_YEAR", 2024)


class ProjectDetails(BaseModel):
id: int
updated_at: Optional[AwareDatetime] = None
created_at: Optional[AwareDatetime] = None


async def fetch_projects(client: httpx.AsyncClient) -> List[ProjectDetails]:
url = f"{API_BASE_URL}/projects"
response = await client.get(url)
response.raise_for_status()
return [ProjectDetails(**proj) for proj in response.json()]


async def process_projects(projects: list[ProjectDetails]):
now = datetime.now(timezone.utc)
current_year = now.year
one_year_ago = now - timedelta(days=365)
one_month_ago = now - timedelta(days=30)
one_week_ago = now - timedelta(days=7)

project_per_month = defaultdict(int) # Stores projects per month since START_YEAR
projects_per_year = {year: 0 for year in range(START_YEAR, current_year + 1)}
active_past_year = 0
active_past_month = 0
active_past_week = 0
total_projects = len(projects)

for project in projects:
if project.created_at:
project_year = project.created_at.year
project_month = project.created_at.month

if project_year >= START_YEAR:
projects_per_year[project_year] += 1
project_per_month[(project_year, project_month)] += (
1 # Key is (year, month)
)

if project.updated_at:
if project.updated_at >= one_year_ago:
active_past_year += 1
if project.updated_at >= one_month_ago:
active_past_month += 1
if project.updated_at >= one_week_ago:
active_past_week += 1

# Convert month integers to month names for the current year
projects_per_named_month = {
calendar.month_abbr[i]: project_per_month[(current_year, i)]
for i in range(1, 13)
}

# Calculate average project increase rate per month (only for months passed this year)
current_month = now.month
months_with_data = [
project_per_month[(current_year, i)] for i in range(1, current_month + 1)
]
project_monthly_average = (
sum(months_with_data) / len(months_with_data) if months_with_data else 0
)

# Generate discrete (x, y) data points
project_dates = []
project_count_over_time = []

for year in range(START_YEAR, current_year + 1):
for month in range(1, 13):
if year < current_year or (year == current_year and month <= current_month):
dt = datetime(year, month, 1)
timestamp = dt.timestamp() # Convert to Unix time
project_dates.append(timestamp)
project_count_over_time.append(project_per_month.get((year, month), 0))

# Print stats
print("\nFMTM Stats")
print("----------\n")
print(f"Total overall projects: {total_projects}\n")
print(f"Total project per year: {projects_per_year}\n")
print(f"Active projects in last year: {active_past_year}\n")
print(f"Active projects in last month: {active_past_month}\n")
print(f"Active projects in last week: {active_past_week}\n")
print(f"Average number of new projects per month: {project_monthly_average:.2f}\n")

# Bar chart: Total projects per month in the current year
print("Total project per month this year:")
itrm.bars(
list(projects_per_named_month.values()), list(projects_per_named_month.keys())
)
print()

# Line chart: Total projects over time since START_YEAR
# Interpolation: Create a smooth x-axis
x_axis_points_num = (
len(projects_per_year.keys()) * 1000
) # 1000 points on graph per year
x_smooth = np.linspace(
min(project_dates), max(project_dates), num=x_axis_points_num
)
# Interpolation: Smooth y-values (projects over time)
y_smooth = np.interp(x_smooth, project_dates, project_count_over_time)
print("Total project per month since START_YEAR:")
itrm.plot(x_smooth, y_smooth, "Projects Over Time")
print()


async def main():
print(f"Connecting to server: {API_BASE_URL}")

async with httpx.AsyncClient() as client:
projects = await fetch_projects(client)
if not projects:
print("No projects found.")
return

await process_projects(projects)


if __name__ == "__main__":
asyncio.run(main())
14 changes: 14 additions & 0 deletions src/backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 431d3f9

Please sign in to comment.