Ensure functions have docstrings for documentation
def get_graphql_response(
1import logging2import secrets3import subprocess4from collections import Counter5from datetime import datetime6from pathlib import Path7from typing import Any89import httpx10import yaml11from github import Github12from pydantic import BaseModel, SecretStr13from pydantic_settings import BaseSettings1415github_graphql_url = "https://api.github.com/graphql"161718prs_query = """19query Q($after: String) {20 repository(name: "fastapi", owner: "fastapi") {21 pullRequests(first: 100, after: $after) {22 edges {23 cursor24 node {25 number26 labels(first: 100) {27 nodes {28 name29 }30 }31 author {32 login33 avatarUrl34 url35 }36 title37 createdAt38 lastEditedAt39 updatedAt40 state41 reviews(first:100) {42 nodes {43 author {44 login45 avatarUrl46 url47 }48 state49 }50 }51 }52 }53 }54 }55}56"""575859class Author(BaseModel):60 login: str61 avatarUrl: str62 url: str636465class LabelNode(BaseModel):66 name: str676869class Labels(BaseModel):70 nodes: list[LabelNode]717273class ReviewNode(BaseModel):74 author: Author | None = None75 state: str767778class Reviews(BaseModel):79 nodes: list[ReviewNode]808182class PullRequestNode(BaseModel):83 number: int84 labels: Labels85 author: Author | None = None86 title: str87 createdAt: datetime88 lastEditedAt: datetime | None = None89 updatedAt: datetime | None = None90 state: str91 reviews: Reviews929394class PullRequestEdge(BaseModel):95 cursor: str96 node: PullRequestNode979899class PullRequests(BaseModel):100 edges: list[PullRequestEdge]101102103class PRsRepository(BaseModel):104 pullRequests: PullRequests105106107class PRsResponseData(BaseModel):108 repository: PRsRepository109110111class PRsResponse(BaseModel):112 data: PRsResponseData113114115class Settings(BaseSettings):116 github_token: SecretStr117 github_repository: str118 httpx_timeout: int = 30119120121def get_graphql_response(122 *,123 settings: Settings,124 query: str,125 after: str | None = None,126) -> dict[str, Any]:127 headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"}128 variables = {"after": after}129 response = httpx.post(130 github_graphql_url,131 headers=headers,132 timeout=settings.httpx_timeout,133 json={"query": query, "variables": variables, "operationName": "Q"},134 )135 if response.status_code != 200:136 logging.error(f"Response was not 200, after: {after}")137 logging.error(response.text)138 raise RuntimeError(response.text)139 data = response.json()140 if "errors" in data:141 logging.error(f"Errors in response, after: {after}")142 logging.error(data["errors"])143 logging.error(response.text)144 raise RuntimeError(response.text)145 return data146147148def get_graphql_pr_edges(149 *, settings: Settings, after: str | None = None150) -> list[PullRequestEdge]:151 data = get_graphql_response(settings=settings, query=prs_query, after=after)152 graphql_response = PRsResponse.model_validate(data)153 return graphql_response.data.repository.pullRequests.edges154155156def get_pr_nodes(settings: Settings) -> list[PullRequestNode]:157 pr_nodes: list[PullRequestNode] = []158 pr_edges = get_graphql_pr_edges(settings=settings)159160 while pr_edges:161 for edge in pr_edges:162 pr_nodes.append(edge.node)163 last_edge = pr_edges[-1]164 pr_edges = get_graphql_pr_edges(settings=settings, after=last_edge.cursor)165 return pr_nodes166167168class ContributorsResults(BaseModel):169 contributors: Counter[str]170 translation_reviewers: Counter[str]171 translators: Counter[str]172 authors: dict[str, Author]173174175def get_contributors(pr_nodes: list[PullRequestNode]) -> ContributorsResults:176 contributors = Counter[str]()177 translation_reviewers = Counter[str]()178 translators = Counter[str]()179 authors: dict[str, Author] = {}180181 for pr in pr_nodes:182 if pr.author:183 authors[pr.author.login] = pr.author184 is_lang = False185 for label in pr.labels.nodes:186 if label.name == "lang-all":187 is_lang = True188 break189 for review in pr.reviews.nodes:190 if review.author:191 authors[review.author.login] = review.author192 if is_lang:193 translation_reviewers[review.author.login] += 1194 if pr.state == "MERGED" and pr.author:195 if is_lang:196 translators[pr.author.login] += 1197 else:198 contributors[pr.author.login] += 1199 return ContributorsResults(200 contributors=contributors,201 translation_reviewers=translation_reviewers,202 translators=translators,203 authors=authors,204 )205206207def get_users_to_write(208 *,209 counter: Counter[str],210 authors: dict[str, Author],211 min_count: int = 2,212) -> dict[str, Any]:213 users: dict[str, Any] = {}214 for user, count in counter.most_common():215 if count >= min_count:216 author = authors[user]217 users[user] = {218 "login": user,219 "count": count,220 "avatarUrl": author.avatarUrl,221 "url": author.url,222 }223 return users224225226def update_content(*, content_path: Path, new_content: Any) -> bool:227 old_content = content_path.read_text(encoding="utf-8")228229 new_content = yaml.dump(new_content, sort_keys=False, width=200, allow_unicode=True)230 if old_content == new_content:231 logging.info(f"The content hasn't changed for {content_path}")232 return False233 content_path.write_text(new_content, encoding="utf-8")234 logging.info(f"Updated {content_path}")235 return True236237238def main() -> None:239 logging.basicConfig(level=logging.INFO)240 settings = Settings() # ty: ignore[missing-argument]241 logging.info(f"Using config: {settings.model_dump_json()}")242 g = Github(settings.github_token.get_secret_value())243 repo = g.get_repo(settings.github_repository)244245 pr_nodes = get_pr_nodes(settings=settings)246 contributors_results = get_contributors(pr_nodes=pr_nodes)247 authors = contributors_results.authors248249 top_contributors = get_users_to_write(250 counter=contributors_results.contributors,251 authors=authors,252 )253254 top_translators = get_users_to_write(255 counter=contributors_results.translators,256 authors=authors,257 )258 top_translations_reviewers = get_users_to_write(259 counter=contributors_results.translation_reviewers,260 authors=authors,261 )262263 # For local development264 # contributors_path = Path("../docs/en/data/contributors.yml")265 contributors_path = Path("./docs/en/data/contributors.yml")266 # translators_path = Path("../docs/en/data/translators.yml")267 translators_path = Path("./docs/en/data/translators.yml")268 # translation_reviewers_path = Path("../docs/en/data/translation_reviewers.yml")269 translation_reviewers_path = Path("./docs/en/data/translation_reviewers.yml")270271 updated = [272 update_content(content_path=contributors_path, new_content=top_contributors),273 update_content(content_path=translators_path, new_content=top_translators),274 update_content(275 content_path=translation_reviewers_path,276 new_content=top_translations_reviewers,277 ),278 ]279280 if not any(updated):281 logging.info("The data hasn't changed, finishing.")282 return283284 logging.info("Setting up GitHub Actions git user")285 subprocess.run(["git", "config", "user.name", "github-actions[bot]"], check=True)286 subprocess.run(287 ["git", "config", "user.email", "github-actions[bot]@users.noreply.github.com"],288 check=True,289 )290 branch_name = f"fastapi-people-contributors-{secrets.token_hex(4)}"291 logging.info(f"Creating a new branch {branch_name}")292 subprocess.run(["git", "checkout", "-b", branch_name], check=True)293 logging.info("Adding updated file")294 subprocess.run(295 [296 "git",297 "add",298 str(contributors_path),299 str(translators_path),300 str(translation_reviewers_path),301 ],302 check=True,303 )304 logging.info("Committing updated file")305 message = "👥 Update FastAPI People - Contributors and Translators"306 subprocess.run(["git", "commit", "-m", message], check=True)307 logging.info("Pushing branch")308 subprocess.run(["git", "push", "origin", branch_name], check=True)309 logging.info("Creating PR")310 pr = repo.create_pull(title=message, body=message, base="master", head=branch_name)311 logging.info(f"Created PR: {pr.number}")312 logging.info("Finished")313314315if __name__ == "__main__":316 main()
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.