~/marcelotryle

blog / · open source

2024 Open Source Report

This is my first yearly report on Open Source! 🎉

I dedicate a lot of my free time doing Open Source work, and I would like to share with you some numbers. I hope you find them interesting!

ProjectDownloads/monthTime spentReleasesClosed issuesMerged PRsClosed unmerged PRsAnswered discussions
Starlette57 million70 hrs 29 mins29761826493
Uvicorn49 million48 hrs 3 mins20611006538
Python Multipart25 million17 hrs 29 mins133787120
Total131 million136 hrs 1 min62174369141131

Most of the time dedicated in maintaining open source projects is actually not spent coding, as most of people think. It’s mainly on interacting with people: answering questions, reviewing pull requests, and investigating issues.

Sponsors

I would like to thank all the sponsors that supported me in 2024! ❤️

Data Analysis

I got this data from a script I created that queries the GitHub API and WakaTime API.

??? success “Click here to see the script…”

Most of the script was created with the help of [Claude AI](https://claude.ai/new),
but I had to tweak it a bit to get the data I wanted.

If you want to use it, make sure you have the following environment variables set:

- `WAKATIME_API_KEY`: Your WakaTime API key.
- `GH_TOKEN`: Your GitHub token.

```py
import os
import httpx
from datetime import datetime, timedelta
from wakatime_client import WakatimeClient


def main():
    client = WakatimeClient(api_key=os.getenv("WAKATIME_API_KEY"))
    for project in client.stats(range="last_year")["data"]["projects"]:
        if project["name"] in ("starlette", "uvicorn", "python-multipart"):
            print(f': ')
    print()

    print(f"starlette releases: ")
    print(f"uvicorn releases: ")
    print(f"python-multipart releases: ")
    print()
    print(f"starlette stats: ")
    print(f"uvicorn stats: ")
    print(f"python-multipart stats: ")
    print()
    print(f"starlette activity: ")
    print(f"uvicorn activity: ")
    print(f"python-multipart activity: ")


def count_releases(owner: str, repo: str):
    url = f"https://api.github.com/repos///releases"
    headers = "}

    with httpx.Client() as client:
        response = client.get(url, headers=headers)
        response.raise_for_status()

        one_year_ago = datetime.now() - timedelta(days=365)
        return sum(
            1
            for release in response.json()
            if datetime.strptime(release["published_at"], "%Y-%m-%dT%H:%M:%SZ") > one_year_ago
        )


def get_repo_stats(owner: str, repo: str):
    headers = "}

    base_url = f"https://api.github.com/repos//"
    since = (datetime.now() - timedelta(days=365)).isoformat()

    try:
        with httpx.Client() as client:
            # Get issues (excluding PRs)
            issues_count = 0
            issues_url = f"/issues"
            issues_params = 

            issues_response = client.get(issues_url, headers=headers, params=issues_params)
            issues_response.raise_for_status()

            while issues_response.status_code == 200:
                issues_count += sum(1 for issue in issues_response.json() if "pull_request" not in issue)

                if "Link" in issues_response.headers and 'rel="next"' in issues_response.headers["Link"]:
                    next_url = [
                        link.split(";")[0].strip("<> ")
                        for link in issues_response.headers["Link"].split(",")
                        if 'rel="next"' in link
                    ][0]
                    issues_response = client.get(next_url, headers=headers)
                else:
                    break

            # Get PRs
            prs_url = f"/pulls"
            merged_count = 0
            closed_count = 0

            # First get merged PRs
            pr_params = 
            pr_response = client.get(prs_url, headers=headers, params=pr_params)
            pr_response.raise_for_status()

            while pr_response.status_code == 200:
                for pr in pr_response.json():
                    # Check if PR was updated in the last year
                    if datetime.strptime(pr["updated_at"], "%Y-%m-%dT%H:%M:%SZ") < datetime.now() - timedelta(days=365):
                        break

                    if pr["merged_at"]:
                        merged_count += 1
                    else:
                        closed_count += 1

                if "Link" in pr_response.headers and 'rel="next"' in pr_response.headers["Link"]:
                    next_url = [
                        link.split(";")[0].strip("<> ")
                        for link in pr_response.headers["Link"].split(",")
                        if 'rel="next"' in link
                    ][0]
                    pr_response = client.get(next_url, headers=headers)
                else:
                    break

            return 

    except httpx.HTTPError as e:
        print(f"Error fetching repository stats: ")
        return None


def get_repo_activity(owner: str, repo: str):
    headers = "}

    # GraphQL query for discussions (REST API doesn't support discussions)
    graphql_url = "https://api.github.com/graphql"
    query = """
    query($owner:String!, $repo:String!) ) 
        }
    }
}
"""

    with httpx.Client() as client:
        # Get discussions via GraphQL
        response = client.post(
            graphql_url, json=}, headers=headers
        )
        response.raise_for_status()

        one_year_ago = datetime.now() - timedelta(days=365)
        data = response.json()

        return sum(
            1
            for discussion in data["data"]["repository"]["discussions"]["nodes"]
            if datetime.strptime(discussion["answerChosenAt"], "%Y-%m-%dT%H:%M:%SZ") > one_year_ago
        )


main()
```

Was this useful? Consider sponsoring my open source work. ❤️