Compare commits

...

13 Commits

Author SHA1 Message Date
debug
7a87e288c2 Debug 2 2025-01-01 22:18:59 +00:00
Sebastián Ramírez
8aa879ea47 Merge 29b24209b6 into dd649ff814 2025-01-01 22:14:46 +00:00
Sebastián Ramírez
29b24209b6 👷 Debug CI 2025-01-01 22:14:32 +00:00
Sebastián Ramírez
13dc329207 🔧 Update tmate token 2025-01-01 22:06:56 +00:00
Sebastián Ramírez
43dccdda9d 👷 Update CI, permissions, debug 2025-01-01 22:03:57 +00:00
Sebastián Ramírez
1a29cb841d ♻️ Update config to github_token 2025-01-01 21:55:22 +00:00
Sebastián Ramírez
343d0e6221 👷 Update GitHub action 2025-01-01 21:51:46 +00:00
Sebastián Ramírez
bf6c96f417 👷 Add workflow for FastAPI People Contributors 2025-01-01 21:48:12 +00:00
Sebastián Ramírez
7a913806d6 📝 Update FastAPI People docs with new data 2025-01-01 21:43:13 +00:00
Sebastián Ramírez
56090f4db8 🔧 Update MkDocs, include new yaml files 2025-01-01 21:42:30 +00:00
Sebastián Ramírez
206633f5a0 🔧 Add new empty config files 2025-01-01 21:42:06 +00:00
Sebastián Ramírez
1e89b4f2c3 🔨 Add new contributors script 2025-01-01 21:41:20 +00:00
Sebastián Ramírez
e55f0e0688 Add pyyaml to GitHub Actions dependencies 2025-01-01 18:16:32 +00:00
9 changed files with 410 additions and 21 deletions

56
.github/workflows/contributors.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: FastAPI People Contributors
on:
schedule:
- cron: "0 3 1 * *"
workflow_dispatch:
inputs:
debug_enabled:
description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)"
required: false
default: "false"
# TODO: fix this
pull_request:
env:
UV_SYSTEM_PYTHON: 1
jobs:
job:
if: github.repository_owner == 'fastapi'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Setup uv
uses: astral-sh/setup-uv@v5
with:
version: "0.4.15"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
pyproject.toml
- name: Install Dependencies
run: uv pip install -r requirements-github-actions.txt
# Allow debugging with tmate
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
# TODO: fix this
# if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with:
limit-access-to-actor: true
env:
GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }}
- name: FastAPI People Contributors
run: python ./scripts/contributors.py
env:
GITHUB_TOKEN: ${{ secrets.FASTAPI_PEOPLE }}

0
debug2.txt Normal file
View File

View File

View File

View File

View File

@@ -13,15 +13,13 @@ Hey! 👋
This is me:
{% if people %}
<div class="user-list user-list-center">
{% for user in people.maintainers %}
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Answers: {{ user.answers }}</div><div class="count">Pull Requests: {{ user.prs }}</div></div>
<div class="user"><a href="{{ contributors.tiangolo.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ contributors.tiangolo.avatarUrl }}"/></div><div class="title">@{{ contributors.tiangolo.login }}</div></a> <div class="count">Answers: {{ user.answers }}</div><div class="count">Pull Requests: {{ contributors.tiangolo.count }}</div></div>
{% endfor %}
</div>
{% endif %}
I'm the creator of **FastAPI**. You can read more about that in [Help FastAPI - Get Help - Connect with the author](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}.
@@ -84,7 +82,6 @@ You can see the **FastAPI Experts** for:
These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last month. 🤓
{% if people %}
<div class="user-list user-list-center">
{% for user in people.last_month_experts[:10] %}
@@ -92,13 +89,11 @@ These are the users that have been [helping others the most with questions in Gi
{% endfor %}
</div>
{% endif %}
### FastAPI Experts - 3 Months
These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 3 months. 😎
{% if people %}
<div class="user-list user-list-center">
{% for user in people.three_months_experts[:10] %}
@@ -106,13 +101,11 @@ These are the users that have been [helping others the most with questions in Gi
{% endfor %}
</div>
{% endif %}
### FastAPI Experts - 6 Months
These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last 6 months. 🧐
{% if people %}
<div class="user-list user-list-center">
{% for user in people.six_months_experts[:10] %}
@@ -120,13 +113,11 @@ These are the users that have been [helping others the most with questions in Gi
{% endfor %}
</div>
{% endif %}
### FastAPI Experts - 1 Year
These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last year. 🧑‍🔬
{% if people %}
<div class="user-list user-list-center">
{% for user in people.one_year_experts[:20] %}
@@ -134,7 +125,6 @@ These are the users that have been [helping others the most with questions in Gi
{% endfor %}
</div>
{% endif %}
### FastAPI Experts - All Time
@@ -142,7 +132,6 @@ Here are the all time **FastAPI Experts**. 🤓🤯
These are the users that have [helped others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} through *all time*. 🧙
{% if people %}
<div class="user-list user-list-center">
{% for user in people.experts[:50] %}
@@ -150,7 +139,6 @@ These are the users that have [helped others the most with questions in GitHub](
{% endfor %}
</div>
{% endif %}
## Top Contributors
@@ -158,19 +146,42 @@ Here are the **Top Contributors**. 👷
These users have [created the most Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} that have been *merged*.
They have contributed source code, documentation, translations, etc. 📦
They have contributed source code, documentation, etc. 📦
{% if people %}
<div class="user-list user-list-center">
{% for user in people.top_contributors[:50] %}
{% for user in (contributors.values() | list)[:50] %}
{% if user.login not in skip_users %}
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Pull Requests: {{ user.count }}</div></div>
{% endif %}
{% endfor %}
</div>
There are hundreds of other contributors, you can see them all in the <a href="https://github.com/fastapi/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷
## Top Translators
These are the **Top Translators**. 🌐
These users have created the most Pull Requests with [translations to other languages](contributing.md#translations){.internal-link target=_blank} that have been *merged*.
<div class="user-list user-list-center">
{% for user in (translators.values() | list)[:50] %}
{% if user.login not in skip_users %}
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Translations: {{ user.count }}</div></div>
{% endif %}
There are many other contributors (more than a hundred), you can see them all in the <a href="https://github.com/fastapi/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷
{% endfor %}
</div>
## Top Translation Reviewers
@@ -178,15 +189,18 @@ These users are the **Top Translation Reviewers**. 🕵️
I only speak a few languages (and not very well 😅). So, the reviewers are the ones that have the [**power to approve translations**](contributing.md#translations){.internal-link target=_blank} of the documentation. Without them, there wouldn't be documentation in several other languages.
{% if people %}
<div class="user-list user-list-center">
{% for user in people.top_translations_reviewers[:50] %}
{% for user in (translation_reviewers.values() | list)[:50] %}
{% if user.login not in skip_users %}
<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Reviews: {{ user.count }}</div></div>
{% endif %}
{% endfor %}
</div>
{% endif %}
## Sponsors
@@ -251,7 +265,7 @@ The main intention of this page is to highlight the effort of the community to h
Especially including efforts that are normally less visible, and in many cases more arduous, like helping others with questions and reviewing Pull Requests with translations.
The data is calculated each month, you can read the <a href="https://github.com/fastapi/fastapi/blob/master/.github/actions/people/app/main.py" class="external-link" target="_blank">source code here</a>.
The data is calculated each month, you can read the <a href="https://github.com/fastapi/fastapi/blob/master/scripts/" class="external-link" target="_blank">source code here</a>.
Here I'm also highlighting contributions from sponsors.

View File

@@ -65,6 +65,10 @@ plugins:
- external_links: ../en/data/external_links.yml
- github_sponsors: ../en/data/github_sponsors.yml
- people: ../en/data/people.yml
- contributors: ../en/data/contributors.yml
- translators: ../en/data/translators.yml
- translation_reviewers: ../en/data/translation_reviewers.yml
- skip_users: ../en/data/skip_users.yml
- members: ../en/data/members.yml
- sponsors_badge: ../en/data/sponsors_badge.yml
- sponsors: ../en/data/sponsors.yml

View File

@@ -2,4 +2,5 @@ PyGithub>=2.3.0,<3.0.0
pydantic>=2.5.3,<3.0.0
pydantic-settings>=2.1.0,<3.0.0
httpx>=0.27.0,<0.28.0
pyyaml >=5.3.1,<7.0.0
smokeshow

314
scripts/contributors.py Normal file
View File

@@ -0,0 +1,314 @@
import logging
import subprocess
from collections import Counter
from datetime import datetime
from pathlib import Path
from typing import Any
import httpx
import yaml
from github import Github
from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings
github_graphql_url = "https://api.github.com/graphql"
prs_query = """
query Q($after: String) {
repository(name: "fastapi", owner: "fastapi") {
pullRequests(first: 100, after: $after) {
edges {
cursor
node {
number
labels(first: 100) {
nodes {
name
}
}
author {
login
avatarUrl
url
}
title
createdAt
lastEditedAt
updatedAt
state
reviews(first:100) {
nodes {
author {
login
avatarUrl
url
}
state
}
}
}
}
}
}
}
"""
class Author(BaseModel):
login: str
avatarUrl: str
url: str
class LabelNode(BaseModel):
name: str
class Labels(BaseModel):
nodes: list[LabelNode]
class ReviewNode(BaseModel):
author: Author | None = None
state: str
class Reviews(BaseModel):
nodes: list[ReviewNode]
class PullRequestNode(BaseModel):
number: int
labels: Labels
author: Author | None = None
title: str
createdAt: datetime
lastEditedAt: datetime | None = None
updatedAt: datetime | None = None
state: str
reviews: Reviews
class PullRequestEdge(BaseModel):
cursor: str
node: PullRequestNode
class PullRequests(BaseModel):
edges: list[PullRequestEdge]
class PRsRepository(BaseModel):
pullRequests: PullRequests
class PRsResponseData(BaseModel):
repository: PRsRepository
class PRsResponse(BaseModel):
data: PRsResponseData
class Settings(BaseSettings):
github_token: SecretStr
github_repository: str
httpx_timeout: int = 30
def get_graphql_response(
*,
settings: Settings,
query: str,
after: str | None = None,
) -> dict[str, Any]:
headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"}
variables = {"after": after}
response = httpx.post(
github_graphql_url,
headers=headers,
timeout=settings.httpx_timeout,
json={"query": query, "variables": variables, "operationName": "Q"},
)
if response.status_code != 200:
logging.error(f"Response was not 200, after: {after}")
logging.error(response.text)
raise RuntimeError(response.text)
data = response.json()
if "errors" in data:
logging.error(f"Errors in response, after: {after}")
logging.error(data["errors"])
logging.error(response.text)
raise RuntimeError(response.text)
return data
def get_graphql_pr_edges(
*, settings: Settings, after: str | None = None
) -> list[PullRequestEdge]:
data = get_graphql_response(settings=settings, query=prs_query, after=after)
graphql_response = PRsResponse.model_validate(data)
return graphql_response.data.repository.pullRequests.edges
def get_pr_nodes(settings: Settings) -> list[PullRequestNode]:
pr_nodes: list[PullRequestNode] = []
pr_edges = get_graphql_pr_edges(settings=settings)
while pr_edges:
for edge in pr_edges:
pr_nodes.append(edge.node)
last_edge = pr_edges[-1]
pr_edges = get_graphql_pr_edges(settings=settings, after=last_edge.cursor)
return pr_nodes
class ContributorsResults(BaseModel):
contributors: Counter[str]
translation_reviewers: Counter[str]
translators: Counter[str]
authors: dict[str, Author]
def get_contributors(pr_nodes: list[PullRequestNode]) -> ContributorsResults:
contributors = Counter[str]()
translation_reviewers = Counter[str]()
translators = Counter[str]()
authors: dict[str, Author] = {}
for pr in pr_nodes:
if pr.author:
authors[pr.author.login] = pr.author
is_lang = False
for label in pr.labels.nodes:
if label.name == "lang-all":
is_lang = True
break
for review in pr.reviews.nodes:
if review.author:
authors[review.author.login] = review.author
if is_lang:
translation_reviewers[review.author.login] += 1
if pr.state == "MERGED" and pr.author:
if is_lang:
translators[pr.author.login] += 1
else:
contributors[pr.author.login] += 1
return ContributorsResults(
contributors=contributors,
translation_reviewers=translation_reviewers,
translators=translators,
authors=authors,
)
def get_users_to_write(
*,
counter: Counter[str],
authors: dict[str, Author],
min_count: int = 2,
) -> dict[str, Any]:
users: dict[str, Any] = {}
for user, count in counter.most_common():
if count >= min_count:
author = authors[user]
users[user] = {
"login": user,
"count": count,
"avatarUrl": author.avatarUrl,
"url": author.url,
}
return users
def update_content(*, content_path: Path, new_content: Any) -> bool:
old_content = content_path.read_text(encoding="utf-8")
new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True)
if old_content == new_content:
logging.info(f"The content hasn't changed for {content_path}")
return False
content_path.write_text(new_content, encoding="utf-8")
logging.info(f"Updated {content_path}")
return True
def main() -> None:
logging.basicConfig(level=logging.INFO)
settings = Settings()
logging.info(f"Using config: {settings.model_dump_json()}")
g = Github(settings.github_token.get_secret_value())
repo = g.get_repo(settings.github_repository)
pr_nodes = get_pr_nodes(settings=settings)
contributors_results = get_contributors(pr_nodes=pr_nodes)
authors = contributors_results.authors
top_contributors = get_users_to_write(
counter=contributors_results.contributors,
authors=authors,
)
top_translators = get_users_to_write(
counter=contributors_results.translators,
authors=authors,
)
top_translations_reviewers = get_users_to_write(
counter=contributors_results.translation_reviewers,
authors=authors,
)
# For local development
# contributors_path = Path("../docs/en/data/contributors.yml")
contributors_path = Path("./docs/en/data/contributors.yml")
# translators_path = Path("../docs/en/data/translators.yml")
translators_path = Path("./docs/en/data/translators.yml")
# translation_reviewers_path = Path("../docs/en/data/translation_reviewers.yml")
translation_reviewers_path = Path("./docs/en/data/translation_reviewers.yml")
updated = [
update_content(content_path=contributors_path, new_content=top_contributors),
update_content(content_path=translators_path, new_content=top_translators),
update_content(
content_path=translation_reviewers_path,
new_content=top_translations_reviewers,
),
]
if not any(updated):
logging.info("The data hasn't changed, finishing.")
return
logging.info("Setting up GitHub Actions git user")
subprocess.run(["git", "config", "user.name", "github-actions"], check=True)
subprocess.run(
["git", "config", "user.email", "github-actions@github.com"], check=True
)
branch_name = "fastapi-people-contributors"
logging.info(f"Creating a new branch {branch_name}")
subprocess.run(["git", "checkout", "-b", branch_name], check=True)
logging.info("Adding updated file")
subprocess.run(
[
"git",
"add",
str(contributors_path),
str(translators_path),
str(translation_reviewers_path),
],
check=True,
)
logging.info("Committing updated file")
message = "👥 Update FastAPI People - Contributors and Translators"
subprocess.run(["git", "commit", "-m", message], check=True)
logging.info("Pushing branch")
subprocess.run(["git", "push", "origin", branch_name], check=True)
logging.info("Creating PR")
pr = repo.create_pull(title=message, body=message, base="master", head=branch_name)
logging.info(f"Created PR: {pr.number}")
logging.info("Finished")
if __name__ == "__main__":
main()