mirror of https://github.com/tiangolo/fastapi.git
✨ Compute FastAPI Experts including GitHub Discussions (#5941)
This commit is contained in:
parent
7c23bbd96f
commit
9530defba8
|
|
@ -4,7 +4,7 @@ import sys
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Container, DefaultDict, Dict, List, Set, Union
|
from typing import Any, Container, DefaultDict, Dict, List, Set, Union
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -12,6 +12,50 @@ from github import Github
|
||||||
from pydantic import BaseModel, BaseSettings, SecretStr
|
from pydantic import BaseModel, BaseSettings, SecretStr
|
||||||
|
|
||||||
github_graphql_url = "https://api.github.com/graphql"
|
github_graphql_url = "https://api.github.com/graphql"
|
||||||
|
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
|
||||||
|
|
||||||
|
discussions_query = """
|
||||||
|
query Q($after: String, $category_id: ID) {
|
||||||
|
repository(name: "fastapi", owner: "tiangolo") {
|
||||||
|
discussions(first: 100, after: $after, categoryId: $category_id) {
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
number
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
avatarUrl
|
||||||
|
url
|
||||||
|
}
|
||||||
|
title
|
||||||
|
createdAt
|
||||||
|
comments(first: 100) {
|
||||||
|
nodes {
|
||||||
|
createdAt
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
avatarUrl
|
||||||
|
url
|
||||||
|
}
|
||||||
|
isAnswer
|
||||||
|
replies(first: 10) {
|
||||||
|
nodes {
|
||||||
|
createdAt
|
||||||
|
author {
|
||||||
|
login
|
||||||
|
avatarUrl
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
issues_query = """
|
issues_query = """
|
||||||
query Q($after: String) {
|
query Q($after: String) {
|
||||||
|
|
@ -131,15 +175,30 @@ class Author(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
# Issues and Discussions
|
||||||
|
|
||||||
|
|
||||||
class CommentsNode(BaseModel):
|
class CommentsNode(BaseModel):
|
||||||
createdAt: datetime
|
createdAt: datetime
|
||||||
author: Union[Author, None] = None
|
author: Union[Author, None] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Replies(BaseModel):
|
||||||
|
nodes: List[CommentsNode]
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsCommentsNode(CommentsNode):
|
||||||
|
replies: Replies
|
||||||
|
|
||||||
|
|
||||||
class Comments(BaseModel):
|
class Comments(BaseModel):
|
||||||
nodes: List[CommentsNode]
|
nodes: List[CommentsNode]
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsComments(BaseModel):
|
||||||
|
nodes: List[DiscussionsCommentsNode]
|
||||||
|
|
||||||
|
|
||||||
class IssuesNode(BaseModel):
|
class IssuesNode(BaseModel):
|
||||||
number: int
|
number: int
|
||||||
author: Union[Author, None] = None
|
author: Union[Author, None] = None
|
||||||
|
|
@ -149,27 +208,59 @@ class IssuesNode(BaseModel):
|
||||||
comments: Comments
|
comments: Comments
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsNode(BaseModel):
|
||||||
|
number: int
|
||||||
|
author: Union[Author, None] = None
|
||||||
|
title: str
|
||||||
|
createdAt: datetime
|
||||||
|
comments: DiscussionsComments
|
||||||
|
|
||||||
|
|
||||||
class IssuesEdge(BaseModel):
|
class IssuesEdge(BaseModel):
|
||||||
cursor: str
|
cursor: str
|
||||||
node: IssuesNode
|
node: IssuesNode
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsEdge(BaseModel):
|
||||||
|
cursor: str
|
||||||
|
node: DiscussionsNode
|
||||||
|
|
||||||
|
|
||||||
class Issues(BaseModel):
|
class Issues(BaseModel):
|
||||||
edges: List[IssuesEdge]
|
edges: List[IssuesEdge]
|
||||||
|
|
||||||
|
|
||||||
|
class Discussions(BaseModel):
|
||||||
|
edges: List[DiscussionsEdge]
|
||||||
|
|
||||||
|
|
||||||
class IssuesRepository(BaseModel):
|
class IssuesRepository(BaseModel):
|
||||||
issues: Issues
|
issues: Issues
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsRepository(BaseModel):
|
||||||
|
discussions: Discussions
|
||||||
|
|
||||||
|
|
||||||
class IssuesResponseData(BaseModel):
|
class IssuesResponseData(BaseModel):
|
||||||
repository: IssuesRepository
|
repository: IssuesRepository
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsResponseData(BaseModel):
|
||||||
|
repository: DiscussionsRepository
|
||||||
|
|
||||||
|
|
||||||
class IssuesResponse(BaseModel):
|
class IssuesResponse(BaseModel):
|
||||||
data: IssuesResponseData
|
data: IssuesResponseData
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionsResponse(BaseModel):
|
||||||
|
data: DiscussionsResponseData
|
||||||
|
|
||||||
|
|
||||||
|
# PRs
|
||||||
|
|
||||||
|
|
||||||
class LabelNode(BaseModel):
|
class LabelNode(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
@ -219,6 +310,9 @@ class PRsResponse(BaseModel):
|
||||||
data: PRsResponseData
|
data: PRsResponseData
|
||||||
|
|
||||||
|
|
||||||
|
# Sponsors
|
||||||
|
|
||||||
|
|
||||||
class SponsorEntity(BaseModel):
|
class SponsorEntity(BaseModel):
|
||||||
login: str
|
login: str
|
||||||
avatarUrl: str
|
avatarUrl: str
|
||||||
|
|
@ -264,10 +358,16 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_response(
|
def get_graphql_response(
|
||||||
*, settings: Settings, query: str, after: Union[str, None] = None
|
*,
|
||||||
):
|
settings: Settings,
|
||||||
|
query: str,
|
||||||
|
after: Union[str, None] = None,
|
||||||
|
category_id: Union[str, None] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
|
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
|
||||||
variables = {"after": after}
|
# category_id is only used by one query, but GraphQL allows unused variables, so
|
||||||
|
# keep it here for simplicity
|
||||||
|
variables = {"after": after, "category_id": category_id}
|
||||||
response = httpx.post(
|
response = httpx.post(
|
||||||
github_graphql_url,
|
github_graphql_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
|
@ -275,7 +375,9 @@ def get_graphql_response(
|
||||||
json={"query": query, "variables": variables, "operationName": "Q"},
|
json={"query": query, "variables": variables, "operationName": "Q"},
|
||||||
)
|
)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logging.error(f"Response was not 200, after: {after}")
|
logging.error(
|
||||||
|
f"Response was not 200, after: {after}, category_id: {category_id}"
|
||||||
|
)
|
||||||
logging.error(response.text)
|
logging.error(response.text)
|
||||||
raise RuntimeError(response.text)
|
raise RuntimeError(response.text)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
@ -288,6 +390,21 @@ def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = Non
|
||||||
return graphql_response.data.repository.issues.edges
|
return graphql_response.data.repository.issues.edges
|
||||||
|
|
||||||
|
|
||||||
|
def get_graphql_question_discussion_edges(
|
||||||
|
*,
|
||||||
|
settings: Settings,
|
||||||
|
after: Union[str, None] = None,
|
||||||
|
):
|
||||||
|
data = get_graphql_response(
|
||||||
|
settings=settings,
|
||||||
|
query=discussions_query,
|
||||||
|
after=after,
|
||||||
|
category_id=questions_category_id,
|
||||||
|
)
|
||||||
|
graphql_response = DiscussionsResponse.parse_obj(data)
|
||||||
|
return graphql_response.data.repository.discussions.edges
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None):
|
def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None):
|
||||||
data = get_graphql_response(settings=settings, query=prs_query, after=after)
|
data = get_graphql_response(settings=settings, query=prs_query, after=after)
|
||||||
graphql_response = PRsResponse.parse_obj(data)
|
graphql_response = PRsResponse.parse_obj(data)
|
||||||
|
|
@ -300,7 +417,7 @@ def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = N
|
||||||
return graphql_response.data.user.sponsorshipsAsMaintainer.edges
|
return graphql_response.data.user.sponsorshipsAsMaintainer.edges
|
||||||
|
|
||||||
|
|
||||||
def get_experts(settings: Settings):
|
def get_issues_experts(settings: Settings):
|
||||||
issue_nodes: List[IssuesNode] = []
|
issue_nodes: List[IssuesNode] = []
|
||||||
issue_edges = get_graphql_issue_edges(settings=settings)
|
issue_edges = get_graphql_issue_edges(settings=settings)
|
||||||
|
|
||||||
|
|
@ -326,13 +443,74 @@ def get_experts(settings: Settings):
|
||||||
for comment in issue.comments.nodes:
|
for comment in issue.comments.nodes:
|
||||||
if comment.author:
|
if comment.author:
|
||||||
authors[comment.author.login] = comment.author
|
authors[comment.author.login] = comment.author
|
||||||
if comment.author.login == issue_author_name:
|
if comment.author.login != issue_author_name:
|
||||||
continue
|
issue_commentors.add(comment.author.login)
|
||||||
issue_commentors.add(comment.author.login)
|
|
||||||
for author_name in issue_commentors:
|
for author_name in issue_commentors:
|
||||||
commentors[author_name] += 1
|
commentors[author_name] += 1
|
||||||
if issue.createdAt > one_month_ago:
|
if issue.createdAt > one_month_ago:
|
||||||
last_month_commentors[author_name] += 1
|
last_month_commentors[author_name] += 1
|
||||||
|
|
||||||
|
return commentors, last_month_commentors, authors
|
||||||
|
|
||||||
|
|
||||||
|
def get_discussions_experts(settings: Settings):
|
||||||
|
discussion_nodes: List[DiscussionsNode] = []
|
||||||
|
discussion_edges = get_graphql_question_discussion_edges(settings=settings)
|
||||||
|
|
||||||
|
while discussion_edges:
|
||||||
|
for discussion_edge in discussion_edges:
|
||||||
|
discussion_nodes.append(discussion_edge.node)
|
||||||
|
last_edge = discussion_edges[-1]
|
||||||
|
discussion_edges = get_graphql_question_discussion_edges(
|
||||||
|
settings=settings, after=last_edge.cursor
|
||||||
|
)
|
||||||
|
|
||||||
|
commentors = Counter()
|
||||||
|
last_month_commentors = Counter()
|
||||||
|
authors: Dict[str, Author] = {}
|
||||||
|
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
one_month_ago = now - timedelta(days=30)
|
||||||
|
|
||||||
|
for discussion in discussion_nodes:
|
||||||
|
discussion_author_name = None
|
||||||
|
if discussion.author:
|
||||||
|
authors[discussion.author.login] = discussion.author
|
||||||
|
discussion_author_name = discussion.author.login
|
||||||
|
discussion_commentors = set()
|
||||||
|
for comment in discussion.comments.nodes:
|
||||||
|
if comment.author:
|
||||||
|
authors[comment.author.login] = comment.author
|
||||||
|
if comment.author.login != discussion_author_name:
|
||||||
|
discussion_commentors.add(comment.author.login)
|
||||||
|
for reply in comment.replies.nodes:
|
||||||
|
if reply.author:
|
||||||
|
authors[reply.author.login] = reply.author
|
||||||
|
if reply.author.login != discussion_author_name:
|
||||||
|
discussion_commentors.add(reply.author.login)
|
||||||
|
for author_name in discussion_commentors:
|
||||||
|
commentors[author_name] += 1
|
||||||
|
if discussion.createdAt > one_month_ago:
|
||||||
|
last_month_commentors[author_name] += 1
|
||||||
|
return commentors, last_month_commentors, authors
|
||||||
|
|
||||||
|
|
||||||
|
def get_experts(settings: Settings):
|
||||||
|
(
|
||||||
|
issues_commentors,
|
||||||
|
issues_last_month_commentors,
|
||||||
|
issues_authors,
|
||||||
|
) = get_issues_experts(settings=settings)
|
||||||
|
(
|
||||||
|
discussions_commentors,
|
||||||
|
discussions_last_month_commentors,
|
||||||
|
discussions_authors,
|
||||||
|
) = get_discussions_experts(settings=settings)
|
||||||
|
commentors = issues_commentors + discussions_commentors
|
||||||
|
last_month_commentors = (
|
||||||
|
issues_last_month_commentors + discussions_last_month_commentors
|
||||||
|
)
|
||||||
|
authors = {**issues_authors, **discussions_authors}
|
||||||
return commentors, last_month_commentors, authors
|
return commentors, last_month_commentors, authors
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -425,13 +603,13 @@ if __name__ == "__main__":
|
||||||
logging.info(f"Using config: {settings.json()}")
|
logging.info(f"Using config: {settings.json()}")
|
||||||
g = Github(settings.input_standard_token.get_secret_value())
|
g = Github(settings.input_standard_token.get_secret_value())
|
||||||
repo = g.get_repo(settings.github_repository)
|
repo = g.get_repo(settings.github_repository)
|
||||||
issue_commentors, issue_last_month_commentors, issue_authors = get_experts(
|
question_commentors, question_last_month_commentors, question_authors = get_experts(
|
||||||
settings=settings
|
settings=settings
|
||||||
)
|
)
|
||||||
contributors, pr_commentors, reviewers, pr_authors = get_contributors(
|
contributors, pr_commentors, reviewers, pr_authors = get_contributors(
|
||||||
settings=settings
|
settings=settings
|
||||||
)
|
)
|
||||||
authors = {**issue_authors, **pr_authors}
|
authors = {**question_authors, **pr_authors}
|
||||||
maintainers_logins = {"tiangolo"}
|
maintainers_logins = {"tiangolo"}
|
||||||
bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"}
|
bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"}
|
||||||
maintainers = []
|
maintainers = []
|
||||||
|
|
@ -440,7 +618,7 @@ if __name__ == "__main__":
|
||||||
maintainers.append(
|
maintainers.append(
|
||||||
{
|
{
|
||||||
"login": login,
|
"login": login,
|
||||||
"answers": issue_commentors[login],
|
"answers": question_commentors[login],
|
||||||
"prs": contributors[login],
|
"prs": contributors[login],
|
||||||
"avatarUrl": user.avatarUrl,
|
"avatarUrl": user.avatarUrl,
|
||||||
"url": user.url,
|
"url": user.url,
|
||||||
|
|
@ -453,13 +631,13 @@ if __name__ == "__main__":
|
||||||
min_count_reviewer = 4
|
min_count_reviewer = 4
|
||||||
skip_users = maintainers_logins | bot_names
|
skip_users = maintainers_logins | bot_names
|
||||||
experts = get_top_users(
|
experts = get_top_users(
|
||||||
counter=issue_commentors,
|
counter=question_commentors,
|
||||||
min_count=min_count_expert,
|
min_count=min_count_expert,
|
||||||
authors=authors,
|
authors=authors,
|
||||||
skip_users=skip_users,
|
skip_users=skip_users,
|
||||||
)
|
)
|
||||||
last_month_active = get_top_users(
|
last_month_active = get_top_users(
|
||||||
counter=issue_last_month_commentors,
|
counter=question_last_month_commentors,
|
||||||
min_count=min_count_last_month,
|
min_count=min_count_last_month,
|
||||||
authors=authors,
|
authors=authors,
|
||||||
skip_users=skip_users,
|
skip_users=skip_users,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue