]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
👥 Add new section FastAPI People (#2195)
authorSebastián Ramírez <tiangolo@gmail.com>
Sun, 18 Oct 2020 19:35:42 +0000 (21:35 +0200)
committerGitHub <noreply@github.com>
Sun, 18 Oct 2020 19:35:42 +0000 (21:35 +0200)
* 👥 Update FastAPI People

* ✨ Add first version of FastAPI People GitHub action code

* 🐳 Add Docker and configs for the FastAPI People GitHub Action

* 👷 Add GitHub Action workflow for FastAPI People

* 📝 Add FastAPI People to docs

* 💄 Add custom CSSs for FastAPI People

.github/actions/people/Dockerfile [new file with mode: 0644]
.github/actions/people/action.yml [new file with mode: 0644]
.github/actions/people/app/main.py [new file with mode: 0644]
.github/workflows/people.yml [new file with mode: 0644]
docs/en/data/people.yml [new file with mode: 0644]
docs/en/docs/css/custom.css
docs/en/docs/fastapi-people.md [new file with mode: 0644]
docs/en/mkdocs.yml

diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile
new file mode 100644 (file)
index 0000000..e0feb69
--- /dev/null
@@ -0,0 +1,7 @@
+FROM python:3.7
+
+RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,,6.0.0"
+
+COPY ./app /app
+
+CMD ["python", "/app/main.py"]
diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml
new file mode 100644 (file)
index 0000000..f435c39
--- /dev/null
@@ -0,0 +1,10 @@
+name: "Generate FastAPI People"
+description: "Generate the data for the FastAPI People page"
+author: "Sebastián Ramírez <tiangolo@gmail.com>"
+inputs:
+  token:
+    description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
+    required: true
+runs:
+  using: 'docker'
+  image: 'Dockerfile'
diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py
new file mode 100644 (file)
index 0000000..c670c38
--- /dev/null
@@ -0,0 +1,510 @@
+import logging
+import subprocess
+from collections import Counter
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from typing import Container, Dict, List, Optional, Set
+
+import httpx
+from github import Github
+import yaml
+from pydantic import BaseModel, BaseSettings, SecretStr
+
+github_graphql_url = "https://api.github.com/graphql"
+
+issues_query = """
+query Q($after: String) { 
+  repository(name: "fastapi", owner: "tiangolo") {
+    issues(first: 100, after: $after) {
+      edges {
+        cursor
+        node {
+          number
+          author {
+            login
+            avatarUrl
+            url
+          }
+          title
+          createdAt
+          state
+          comments(first: 100) {
+            nodes {
+              createdAt
+              author {
+                login
+                avatarUrl
+                url
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+"""
+
+prs_query = """
+query Q($after: String) { 
+  repository(name: "fastapi", owner: "tiangolo") {
+    pullRequests(first: 100, after: $after) {
+      edges {
+        cursor
+        node {
+          number
+          labels(first: 100) {
+            nodes {
+              name
+            }
+          }
+          author {
+            login
+            avatarUrl
+            url
+          }
+          title
+          createdAt
+          state
+          comments(first: 100) {
+            nodes {
+              createdAt
+              author {
+                login
+                avatarUrl
+                url
+              }
+            }
+          }
+          reviews(first:100) {
+            nodes {
+              author {
+                login
+                avatarUrl
+                url
+              }
+              state
+            }
+          }
+        }
+      }
+    }
+  }
+}
+"""
+
+sponsors_query = """
+query Q($after: String) {
+  user(login: "tiangolo") {
+    sponsorshipsAsMaintainer(first: 100, after: $after) {
+      edges {
+        cursor
+        node {
+          sponsorEntity {
+            ... on Organization {
+              login
+              avatarUrl
+              url
+            }
+            ... on User {
+              login
+              avatarUrl
+              url
+            }
+          }
+          tier {
+            name
+            monthlyPriceInDollars
+          }
+        }
+      }
+    }
+  }
+}
+"""
+
+
+class Author(BaseModel):
+    login: str
+    avatarUrl: str
+    url: str
+
+
+class CommentsNode(BaseModel):
+    createdAt: datetime
+    author: Optional[Author] = None
+
+
+class Comments(BaseModel):
+    nodes: List[CommentsNode]
+
+
+class IssuesNode(BaseModel):
+    number: int
+    author: Optional[Author] = None
+    title: str
+    createdAt: datetime
+    state: str
+    comments: Comments
+
+
+class IssuesEdge(BaseModel):
+    cursor: str
+    node: IssuesNode
+
+
+class Issues(BaseModel):
+    edges: List[IssuesEdge]
+
+
+class IssuesRepository(BaseModel):
+    issues: Issues
+
+
+class IssuesResponseData(BaseModel):
+    repository: IssuesRepository
+
+
+class IssuesResponse(BaseModel):
+    data: IssuesResponseData
+
+
+class LabelNode(BaseModel):
+    name: str
+
+
+class Labels(BaseModel):
+    nodes: List[LabelNode]
+
+
+class ReviewNode(BaseModel):
+    author: Optional[Author] = None
+    state: str
+
+
+class Reviews(BaseModel):
+    nodes: List[ReviewNode]
+
+
+class PullRequestNode(BaseModel):
+    number: int
+    labels: Labels
+    author: Optional[Author] = None
+    title: str
+    createdAt: datetime
+    state: str
+    comments: Comments
+    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 SponsorEntity(BaseModel):
+    login: str
+    avatarUrl: str
+    url: str
+
+
+class Tier(BaseModel):
+    name: str
+    monthlyPriceInDollars: float
+
+
+class SponsorshipAsMaintainerNode(BaseModel):
+    sponsorEntity: SponsorEntity
+    tier: Tier
+
+
+class SponsorshipAsMaintainerEdge(BaseModel):
+    cursor: str
+    node: SponsorshipAsMaintainerNode
+
+
+class SponsorshipAsMaintainer(BaseModel):
+    edges: List[SponsorshipAsMaintainerEdge]
+
+
+class SponsorsUser(BaseModel):
+    sponsorshipsAsMaintainer: SponsorshipAsMaintainer
+
+
+class SponsorsResponseData(BaseModel):
+    user: SponsorsUser
+
+
+class SponsorsResponse(BaseModel):
+    data: SponsorsResponseData
+
+
+class Settings(BaseSettings):
+    input_token: SecretStr
+    github_repository: str
+
+
+def get_graphql_response(
+    *, settings: Settings, query: str, after: Optional[str] = None
+):
+    headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
+    variables = {"after": after}
+    response = httpx.post(
+        github_graphql_url,
+        headers=headers,
+        json={"query": query, "variables": variables, "operationName": "Q"},
+    )
+    if not response.status_code == 200:
+        logging.error(f"Response was not 200, after: {after}")
+        logging.error(response.text)
+        raise RuntimeError(response.text)
+    data = response.json()
+    return data
+
+
+def get_graphql_issue_edges(*, settings: Settings, after: Optional[str] = None):
+    data = get_graphql_response(settings=settings, query=issues_query, after=after)
+    graphql_response = IssuesResponse.parse_obj(data)
+    return graphql_response.data.repository.issues.edges
+
+
+def get_graphql_pr_edges(*, settings: Settings, after: Optional[str] = None):
+    data = get_graphql_response(settings=settings, query=prs_query, after=after)
+    graphql_response = PRsResponse.parse_obj(data)
+    return graphql_response.data.repository.pullRequests.edges
+
+
+def get_graphql_sponsor_edges(*, settings: Settings, after: Optional[str] = None):
+    data = get_graphql_response(settings=settings, query=sponsors_query, after=after)
+    graphql_response = SponsorsResponse.parse_obj(data)
+    return graphql_response.data.user.sponsorshipsAsMaintainer.edges
+
+
+def get_experts(settings: Settings):
+    issue_nodes: List[IssuesNode] = []
+    issue_edges = get_graphql_issue_edges(settings=settings)
+
+    while issue_edges:
+        for edge in issue_edges:
+            issue_nodes.append(edge.node)
+        last_edge = issue_edges[-1]
+        issue_edges = get_graphql_issue_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 issue in issue_nodes:
+        issue_author_name = None
+        if issue.author:
+            authors[issue.author.login] = issue.author
+            issue_author_name = issue.author.login
+        issue_commentors = set()
+        for comment in issue.comments.nodes:
+            if comment.author:
+                authors[comment.author.login] = comment.author
+                if comment.author.login == issue_author_name:
+                    continue
+                issue_commentors.add(comment.author.login)
+        for author_name in issue_commentors:
+            commentors[author_name] += 1
+            if issue.createdAt > one_month_ago:
+                last_month_commentors[author_name] += 1
+    return commentors, last_month_commentors, authors
+
+
+def get_contributors(settings: Settings):
+    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)
+
+    contributors = Counter()
+    commentors = Counter()
+    reviewers = Counter()
+    authors: Dict[str, Author] = {}
+
+    for pr in pr_nodes:
+        author_name = None
+        if pr.author:
+            authors[pr.author.login] = pr.author
+            author_name = pr.author.login
+        pr_commentors: Set[str] = set()
+        pr_reviewers: Set[str] = set()
+        for comment in pr.comments.nodes:
+            if comment.author:
+                authors[comment.author.login] = comment.author
+                if comment.author.login == author_name:
+                    continue
+                pr_commentors.add(comment.author.login)
+        for author_name in pr_commentors:
+            commentors[author_name] += 1
+        for review in pr.reviews.nodes:
+            if review.author:
+                authors[review.author.login] = review.author
+                pr_reviewers.add(review.author.login)
+        for reviewer in pr_reviewers:
+            reviewers[reviewer] += 1
+        if pr.state == "MERGED" and pr.author:
+            contributors[pr.author.login] += 1
+    return contributors, commentors, reviewers, authors
+
+
+def get_sponsors(settings: Settings):
+    nodes: List[SponsorshipAsMaintainerNode] = []
+    edges = get_graphql_sponsor_edges(settings=settings)
+
+    while edges:
+        for edge in edges:
+            nodes.append(edge.node)
+        last_edge = edges[-1]
+        edges = get_graphql_sponsor_edges(settings=settings, after=last_edge.cursor)
+
+    entities: Dict[str, SponsorEntity] = {}
+    for node in nodes:
+        entities[node.sponsorEntity.login] = node.sponsorEntity
+    return entities
+
+
+def get_top_users(
+    *,
+    counter: Counter,
+    min_count: int,
+    authors: Dict[str, Author],
+    skip_users: Container[str],
+):
+    users = []
+    for commentor, count in counter.most_common(50):
+        if commentor in skip_users:
+            continue
+        if count >= min_count:
+            author = authors[commentor]
+            users.append(
+                {
+                    "login": commentor,
+                    "count": count,
+                    "avatarUrl": author.avatarUrl,
+                    "url": author.url,
+                }
+            )
+    return users
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.INFO)
+    settings = Settings()
+    logging.info(f"Using config: {settings.json()}")
+    g = Github(settings.input_token.get_secret_value())
+    repo = g.get_repo(settings.github_repository)
+    issue_commentors, issue_last_month_commentors, issue_authors = get_experts(
+        settings=settings
+    )
+    contributors, pr_commentors, reviewers, pr_authors = get_contributors(
+        settings=settings
+    )
+    authors = {**issue_authors, **pr_authors}
+    maintainers_logins = {"tiangolo"}
+    bot_names = {"codecov", "github-actions"}
+    maintainers = []
+    for login in maintainers_logins:
+        user = authors[login]
+        maintainers.append(
+            {
+                "login": login,
+                "answers": issue_commentors[login],
+                "prs": contributors[login],
+                "avatarUrl": user.avatarUrl,
+                "url": user.url,
+            }
+        )
+
+    min_count_expert = 10
+    min_count_last_month = 3
+    min_count_contributor = 4
+    min_count_reviewer = 4
+    skip_users = maintainers_logins | bot_names
+    experts = get_top_users(
+        counter=issue_commentors,
+        min_count=min_count_expert,
+        authors=authors,
+        skip_users=skip_users,
+    )
+    last_month_active = get_top_users(
+        counter=issue_last_month_commentors,
+        min_count=min_count_last_month,
+        authors=authors,
+        skip_users=skip_users,
+    )
+    top_contributors = get_top_users(
+        counter=contributors,
+        min_count=min_count_contributor,
+        authors=authors,
+        skip_users=skip_users,
+    )
+    top_reviewers = get_top_users(
+        counter=reviewers,
+        min_count=min_count_reviewer,
+        authors=authors,
+        skip_users=skip_users,
+    )
+
+    sponsors_by_login = get_sponsors(settings=settings)
+    sponsors = []
+    for login, sponsor in sponsors_by_login.items():
+        sponsors.append(
+            {"login": login, "avatarUrl": sponsor.avatarUrl, "url": sponsor.url}
+        )
+
+    people = {
+        "maintainers": maintainers,
+        "experts": experts,
+        "last_month_active": last_month_active,
+        "top_contributors": top_contributors,
+        "top_reviewers": top_reviewers,
+        "sponsors": sponsors,
+    }
+    people_path = Path("./docs/en/data/people.yml")
+    people_path.write_text(
+        yaml.dump(people, sort_keys=False, width=200, allow_unicode=True),
+        encoding="utf-8",
+    )
+    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"
+    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(people_path)], check=True)
+    logging.info("Committing updated file")
+    message = "👥 Update FastAPI People"
+    result = 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")
diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml
new file mode 100644 (file)
index 0000000..d09d820
--- /dev/null
@@ -0,0 +1,15 @@
+name: FastAPI People
+
+on:
+  schedule:
+    - cron: "30 * * * *"
+  workflow_dispatch:
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: ./.github/actions/people
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml
new file mode 100644 (file)
index 0000000..030ec1e
--- /dev/null
@@ -0,0 +1,340 @@
+maintainers:
+- login: tiangolo
+  answers: 979
+  prs: 177
+  avatarUrl: https://avatars1.githubusercontent.com/u/1326112?u=05f95ca7fdead36edd9c86be46b4ef6c3c71f876&v=4
+  url: https://github.com/tiangolo
+experts:
+- login: dmontagu
+  count: 262
+  avatarUrl: https://avatars2.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
+  url: https://github.com/dmontagu
+- login: euri10
+  count: 166
+  avatarUrl: https://avatars3.githubusercontent.com/u/1104190?u=ffd411da5d3b7ad3aa18261317f7ddc76f763c33&v=4
+  url: https://github.com/euri10
+- login: phy25
+  count: 129
+  avatarUrl: https://avatars0.githubusercontent.com/u/331403?v=4
+  url: https://github.com/phy25
+- login: Kludex
+  count: 93
+  avatarUrl: https://avatars1.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
+  url: https://github.com/Kludex
+- login: sm-Fifteen
+  count: 39
+  avatarUrl: https://avatars0.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4
+  url: https://github.com/sm-Fifteen
+- login: prostomarkeloff
+  count: 33
+  avatarUrl: https://avatars3.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4
+  url: https://github.com/prostomarkeloff
+- login: ycd
+  count: 33
+  avatarUrl: https://avatars2.githubusercontent.com/u/62724709?u=496a800351ea1009678e40b26288a2a6c0dfa8bd&v=4
+  url: https://github.com/ycd
+- login: ArcLightSlavik
+  count: 30
+  avatarUrl: https://avatars3.githubusercontent.com/u/31127044?u=b81d0c33b056152513fb14749a9fe00f39887a8e&v=4
+  url: https://github.com/ArcLightSlavik
+- login: wshayes
+  count: 29
+  avatarUrl: https://avatars2.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
+  url: https://github.com/wshayes
+- login: Mause
+  count: 28
+  avatarUrl: https://avatars2.githubusercontent.com/u/1405026?v=4
+  url: https://github.com/Mause
+- login: dbanty
+  count: 25
+  avatarUrl: https://avatars2.githubusercontent.com/u/43723790?u=0cf33e4f40efc2ea206a1189fd63a11344eb88ed&v=4
+  url: https://github.com/dbanty
+- login: nsidnev
+  count: 22
+  avatarUrl: https://avatars0.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4
+  url: https://github.com/nsidnev
+- login: chris-allnutt
+  count: 21
+  avatarUrl: https://avatars0.githubusercontent.com/u/565544?v=4
+  url: https://github.com/chris-allnutt
+- login: Dustyposa
+  count: 21
+  avatarUrl: https://avatars0.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4
+  url: https://github.com/Dustyposa
+- login: acnebs
+  count: 19
+  avatarUrl: https://avatars2.githubusercontent.com/u/9054108?u=bfd127b3e6200f4d00afd714f0fc95c2512df19b&v=4
+  url: https://github.com/acnebs
+- login: retnikt
+  count: 19
+  avatarUrl: https://avatars1.githubusercontent.com/u/24581770?v=4
+  url: https://github.com/retnikt
+- login: SirTelemak
+  count: 19
+  avatarUrl: https://avatars1.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4
+  url: https://github.com/SirTelemak
+- login: jorgerpo
+  count: 17
+  avatarUrl: https://avatars1.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4
+  url: https://github.com/jorgerpo
+- login: raphaelauv
+  count: 17
+  avatarUrl: https://avatars3.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4
+  url: https://github.com/raphaelauv
+- login: Slyfoxy
+  count: 17
+  avatarUrl: https://avatars1.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4
+  url: https://github.com/Slyfoxy
+- login: haizaar
+  count: 13
+  avatarUrl: https://avatars3.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4
+  url: https://github.com/haizaar
+- login: zamiramir
+  count: 11
+  avatarUrl: https://avatars1.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4
+  url: https://github.com/zamiramir
+- login: stefanondisponibile
+  count: 10
+  avatarUrl: https://avatars1.githubusercontent.com/u/20441825?u=ee1e59446b98f8ec2363caeda4c17164d0d9cc7d&v=4
+  url: https://github.com/stefanondisponibile
+last_month_active:
+- login: Mause
+  count: 20
+  avatarUrl: https://avatars2.githubusercontent.com/u/1405026?v=4
+  url: https://github.com/Mause
+- login: ycd
+  count: 8
+  avatarUrl: https://avatars2.githubusercontent.com/u/62724709?u=496a800351ea1009678e40b26288a2a6c0dfa8bd&v=4
+  url: https://github.com/ycd
+- login: Kludex
+  count: 7
+  avatarUrl: https://avatars1.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
+  url: https://github.com/Kludex
+- login: ArcLightSlavik
+  count: 6
+  avatarUrl: https://avatars3.githubusercontent.com/u/31127044?u=b81d0c33b056152513fb14749a9fe00f39887a8e&v=4
+  url: https://github.com/ArcLightSlavik
+- login: SebastianLuebke
+  count: 4
+  avatarUrl: https://avatars3.githubusercontent.com/u/21161532?u=ba033c1bf6851b874cfa05a8a824b9f1ff434c37&v=4
+  url: https://github.com/SebastianLuebke
+- login: includeamin
+  count: 3
+  avatarUrl: https://avatars1.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4
+  url: https://github.com/includeamin
+top_contributors:
+- login: dmontagu
+  count: 16
+  avatarUrl: https://avatars2.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
+  url: https://github.com/dmontagu
+- login: waynerv
+  count: 16
+  avatarUrl: https://avatars3.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
+  url: https://github.com/waynerv
+- login: euri10
+  count: 13
+  avatarUrl: https://avatars3.githubusercontent.com/u/1104190?u=ffd411da5d3b7ad3aa18261317f7ddc76f763c33&v=4
+  url: https://github.com/euri10
+- login: tokusumi
+  count: 10
+  avatarUrl: https://avatars0.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4
+  url: https://github.com/tokusumi
+- login: mariacamilagl
+  count: 7
+  avatarUrl: https://avatars2.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
+  url: https://github.com/mariacamilagl
+- login: Serrones
+  count: 6
+  avatarUrl: https://avatars3.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4
+  url: https://github.com/Serrones
+- login: wshayes
+  count: 5
+  avatarUrl: https://avatars2.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
+  url: https://github.com/wshayes
+- login: jekirl
+  count: 4
+  avatarUrl: https://avatars3.githubusercontent.com/u/2546697?v=4
+  url: https://github.com/jekirl
+top_reviewers:
+- login: Kludex
+  count: 47
+  avatarUrl: https://avatars1.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4
+  url: https://github.com/Kludex
+- login: tokusumi
+  count: 37
+  avatarUrl: https://avatars0.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4
+  url: https://github.com/tokusumi
+- login: dmontagu
+  count: 23
+  avatarUrl: https://avatars2.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4
+  url: https://github.com/dmontagu
+- login: cassiobotaro
+  count: 14
+  avatarUrl: https://avatars2.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4
+  url: https://github.com/cassiobotaro
+- login: AdrianDeAnda
+  count: 13
+  avatarUrl: https://avatars0.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4
+  url: https://github.com/AdrianDeAnda
+- login: Laineyzhang55
+  count: 12
+  avatarUrl: https://avatars0.githubusercontent.com/u/59285379?v=4
+  url: https://github.com/Laineyzhang55
+- login: yanever
+  count: 11
+  avatarUrl: https://avatars2.githubusercontent.com/u/21978760?v=4
+  url: https://github.com/yanever
+- login: SwftAlpc
+  count: 11
+  avatarUrl: https://avatars1.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4
+  url: https://github.com/SwftAlpc
+- login: ycd
+  count: 11
+  avatarUrl: https://avatars2.githubusercontent.com/u/62724709?u=496a800351ea1009678e40b26288a2a6c0dfa8bd&v=4
+  url: https://github.com/ycd
+- login: waynerv
+  count: 10
+  avatarUrl: https://avatars3.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
+  url: https://github.com/waynerv
+- login: mariacamilagl
+  count: 10
+  avatarUrl: https://avatars2.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4
+  url: https://github.com/mariacamilagl
+- login: Attsun1031
+  count: 10
+  avatarUrl: https://avatars2.githubusercontent.com/u/1175560?v=4
+  url: https://github.com/Attsun1031
+- login: RunningIkkyu
+  count: 9
+  avatarUrl: https://avatars0.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4
+  url: https://github.com/RunningIkkyu
+- login: komtaki
+  count: 9
+  avatarUrl: https://avatars1.githubusercontent.com/u/39375566?v=4
+  url: https://github.com/komtaki
+- login: Serrones
+  count: 7
+  avatarUrl: https://avatars3.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4
+  url: https://github.com/Serrones
+- login: ryuckel
+  count: 7
+  avatarUrl: https://avatars1.githubusercontent.com/u/36391432?u=094eec0cfddd5013f76f31e55e56147d78b19553&v=4
+  url: https://github.com/ryuckel
+- login: MashhadiNima
+  count: 5
+  avatarUrl: https://avatars0.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4
+  url: https://github.com/MashhadiNima
+- login: euri10
+  count: 4
+  avatarUrl: https://avatars3.githubusercontent.com/u/1104190?u=ffd411da5d3b7ad3aa18261317f7ddc76f763c33&v=4
+  url: https://github.com/euri10
+- login: rkbeatss
+  count: 4
+  avatarUrl: https://avatars0.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4
+  url: https://github.com/rkbeatss
+sponsors:
+- login: samuelcolvin
+  avatarUrl: https://avatars3.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4
+  url: https://github.com/samuelcolvin
+- login: mkeen
+  avatarUrl: https://avatars3.githubusercontent.com/u/38221?u=03e076e08a10a4de0d48a348f1aab0223c5cf24a&v=4
+  url: https://github.com/mkeen
+- login: wshayes
+  avatarUrl: https://avatars2.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4
+  url: https://github.com/wshayes
+- login: ltieman
+  avatarUrl: https://avatars1.githubusercontent.com/u/1084689?u=c9bf77f5e57f98b49694870219b9bd9d1cc862e7&v=4
+  url: https://github.com/ltieman
+- login: mrmattwright
+  avatarUrl: https://avatars3.githubusercontent.com/u/1277725?v=4
+  url: https://github.com/mrmattwright
+- login: timdrijvers
+  avatarUrl: https://avatars1.githubusercontent.com/u/1694939?v=4
+  url: https://github.com/timdrijvers
+- login: abdelhai
+  avatarUrl: https://avatars3.githubusercontent.com/u/1752577?u=8f8f2bce75f3ab68188cea2b5da37c784197acd8&v=4
+  url: https://github.com/abdelhai
+- login: ddahan
+  avatarUrl: https://avatars0.githubusercontent.com/u/1933516?u=4068dc3c5db5d3605116c4f5df6deb9fee324c33&v=4
+  url: https://github.com/ddahan
+- login: cbonoz
+  avatarUrl: https://avatars0.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4
+  url: https://github.com/cbonoz
+- login: mrgnw
+  avatarUrl: https://avatars3.githubusercontent.com/u/2504532?u=7ec43837a6d0afa80f96f0788744ea6341b89f97&v=4
+  url: https://github.com/mrgnw
+- login: paul121
+  avatarUrl: https://avatars2.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4
+  url: https://github.com/paul121
+- login: andre1sk
+  avatarUrl: https://avatars1.githubusercontent.com/u/3148093?v=4
+  url: https://github.com/andre1sk
+- login: igorcorrea
+  avatarUrl: https://avatars0.githubusercontent.com/u/3438238?u=c57605077c31a8f7b2341fc4912507f91b4a5621&v=4
+  url: https://github.com/igorcorrea
+- login: pawamoy
+  avatarUrl: https://avatars2.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
+  url: https://github.com/pawamoy
+- login: p141592
+  avatarUrl: https://avatars3.githubusercontent.com/u/5256328?u=7f9fdf3329bf90017cff00c8a78781bd7a2b48aa&v=4
+  url: https://github.com/p141592
+- login: fabboe
+  avatarUrl: https://avatars3.githubusercontent.com/u/7251331?v=4
+  url: https://github.com/fabboe
+- login: macleodmac
+  avatarUrl: https://avatars2.githubusercontent.com/u/8996312?u=e39c68c3e0b1d264dcba4850134a291680f46355&v=4
+  url: https://github.com/macleodmac
+- login: cristeaadrian
+  avatarUrl: https://avatars0.githubusercontent.com/u/9112724?u=76099d546d6ee44b3ad7269773ecb916590c6a36&v=4
+  url: https://github.com/cristeaadrian
+- login: iambobmae
+  avatarUrl: https://avatars2.githubusercontent.com/u/12390270?u=c9a35c2ee5092a9b4135ebb1f91b7f521c467031&v=4
+  url: https://github.com/iambobmae
+- login: Cozmo25
+  avatarUrl: https://avatars1.githubusercontent.com/u/12619962?u=679dcd6785121e14f6254e9dd0961baf3b1fef5d&v=4
+  url: https://github.com/Cozmo25
+- login: augustogoulart
+  avatarUrl: https://avatars3.githubusercontent.com/u/13952931?u=9326220a94c303c21dc0da56f1f2ff3c10ed591f&v=4
+  url: https://github.com/augustogoulart
+- login: la-mar
+  avatarUrl: https://avatars1.githubusercontent.com/u/16618300?u=7755c0521d2bb0d704f35a51464b15c1e2e6c4da&v=4
+  url: https://github.com/la-mar
+- login: robintully
+  avatarUrl: https://avatars2.githubusercontent.com/u/17059673?u=862b9bb01513f5acd30df97433cb97a24dbfb772&v=4
+  url: https://github.com/robintully
+- login: wedwardbeck
+  avatarUrl: https://avatars3.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4
+  url: https://github.com/wedwardbeck
+- login: linusg
+  avatarUrl: https://avatars3.githubusercontent.com/u/19366641?u=125e390abef8fff3b3b0d370c369cba5d7fd4c67&v=4
+  url: https://github.com/linusg
+- login: SebastianLuebke
+  avatarUrl: https://avatars3.githubusercontent.com/u/21161532?u=ba033c1bf6851b874cfa05a8a824b9f1ff434c37&v=4
+  url: https://github.com/SebastianLuebke
+- login: raminsj13
+  avatarUrl: https://avatars2.githubusercontent.com/u/24259406?u=d51f2a526312ebba150a06936ed187ca0727d329&v=4
+  url: https://github.com/raminsj13
+- login: mertguvencli
+  avatarUrl: https://avatars3.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4
+  url: https://github.com/mertguvencli
+- login: orihomie
+  avatarUrl: https://avatars3.githubusercontent.com/u/29889683?u=6bc2135a52fcb3a49e69e7d50190796618185fda&v=4
+  url: https://github.com/orihomie
+- login: dcooper01
+  avatarUrl: https://avatars2.githubusercontent.com/u/32238294?u=2a83c78b7f2a5f97beeede0b604bbe44cd21b46b&v=4
+  url: https://github.com/dcooper01
+- login: d3vzer0
+  avatarUrl: https://avatars3.githubusercontent.com/u/34250156?u=c50c9df0e34f411f7e5f050a72e8d89696284eba&v=4
+  url: https://github.com/d3vzer0
+- login: dbanty
+  avatarUrl: https://avatars2.githubusercontent.com/u/43723790?u=0cf33e4f40efc2ea206a1189fd63a11344eb88ed&v=4
+  url: https://github.com/dbanty
+- login: Brontomerus
+  avatarUrl: https://avatars0.githubusercontent.com/u/61284158?u=c00d807195815014d0b6597b3801ee9c494802dd&v=4
+  url: https://github.com/Brontomerus
+- login: primer-api
+  avatarUrl: https://avatars2.githubusercontent.com/u/62152773?u=4549d79b0ad1d30ecfbef6c6933593e90e819c75&v=4
+  url: https://github.com/primer-api
+- login: daverin
+  avatarUrl: https://avatars1.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4
+  url: https://github.com/daverin
index b7de5e34ec533ff2e6b2008083684ae75dc5d00d..16dddc6ddb5827c4e75c0612b5b837b5a38ef872 100644 (file)
@@ -1,18 +1,57 @@
 a.external-link::after {
-    /* \00A0 is a non-breaking space
+  /* \00A0 is a non-breaking space
         to make the mark be on the same line as the link
     */
-    content: "\00A0[↪]";
+  content: "\00A0[↪]";
 }
 
 a.internal-link::after {
-    /* \00A0 is a non-breaking space
+  /* \00A0 is a non-breaking space
         to make the mark be on the same line as the link
     */
-    content: "\00A0↪";
+  content: "\00A0↪";
 }
 
 /* Give space to lower icons so Gitter chat doesn't get on top of them */
 .md-footer-meta {
-    padding-bottom: 2em;
+  padding-bottom: 2em;
+}
+
+.user-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.user-list-center {
+  justify-content: space-evenly;
+}
+
+.user {
+  margin: 1em;
+  min-width: 7em;
+}
+
+.user .avatar-wrapper {
+  width: 80px;
+  height: 80px;
+  margin: 10px auto;
+  overflow: hidden;
+  border-radius: 50%;
+  position: relative;
+}
+
+.user .avatar-wrapper img {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.user .title {
+  text-align: center;
+}
+
+.user .count {
+  font-size: 80%;
+  text-align: center;
 }
diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md
new file mode 100644 (file)
index 0000000..80b225e
--- /dev/null
@@ -0,0 +1,135 @@
+# FastAPI People
+
+FastAPI has an amazing community that welcomes people from all backgrounds.
+
+## Creator - Maintainer
+
+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>
+{% endfor %}
+
+</div>
+{% endif %}
+
+I'm the creator and maintainer 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}.
+
+...But here I want to show you the community.
+
+---
+
+**FastAPI** receives a lot of support from the community. And I want to highlight their contributions.
+
+These are the people that:
+
+* [Help others with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}.
+* [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}.
+* Review Pull Requests, [especially important for translations](contributing.md#translations){.internal-link target=_blank}.
+
+A round of applause to them. 👏 🙇
+
+## Most active users last month
+
+These are the users that have been [helping others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-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_active %}
+
+<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">Issues replied: {{ user.count }}</div></div>
+{% endfor %}
+
+</div>
+{% endif %}
+
+## Experts
+
+Here are the **FastAPI Experts**. 🤓
+
+These are the users that have [helped others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} through *all time*.
+
+They have proven to be experts by helping many others. ✨
+
+{% if people %}
+<div class="user-list user-list-center">
+{% for user in people.experts %}
+
+<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">Issues replied: {{ user.count }}</div></div>
+{% endfor %}
+
+</div>
+{% endif %}
+
+## Top Contributors
+
+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. 📦
+
+{% if people %}
+<div class="user-list user-list-center">
+{% for user in people.top_contributors %}
+
+<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>
+{% endfor %}
+
+</div>
+{% endif %}
+
+There are many other contributors (more than a hundred), you can see them all in the <a href="https://github.com/tiangolo/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Contributors page</a>. 👷
+
+## Top Reviewers
+
+These users are the **Top Reviewers**. 🕵️
+
+### Reviews for Translations
+
+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.
+
+---
+
+The **Top Reviewers** 🕵️ have reviewed the most Pull Requests from others, ensuring the quality of the code, documentation, and especially, the **translations**.
+
+{% if people %}
+<div class="user-list user-list-center">
+{% for user in people.top_reviewers %}
+
+<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>
+{% endfor %}
+
+</div>
+{% endif %}
+
+## Sponsors
+
+These are the **Sponsors**. 😎
+
+They are supporting my work with **FastAPI** (and others) through <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub Sponsors</a>.
+
+{% if people %}
+<div class="user-list user-list-center">
+{% for user in people.sponsors %}
+
+<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>
+{% endfor %}
+
+</div>
+{% endif %}
+
+## About the data - technical details
+
+The intention of this page is to highlight the effort of the community to help others.
+
+Especially including efforts that are normally less visible, and in many cases more arduous, like helping others with issues and reviewing Pull Requests with translations.
+
+The data is calculated each month, you can read the <a href="https://github.com/tiangolo/fastapi/blob/master/.github/actions/people/app/main.py" class="external-link" target="_blank">source code here</a>.
+
+I also reserve the right to update the algorithm, sections, thresholds, etc (just in case 🤷).
index 784cfee10609a32c7e53ce469d9ecfa3dd9a901f..6fa7dc6640a5cb0e69dc0a92e9b06eb8fe9bbe07 100644 (file)
@@ -37,6 +37,7 @@ nav:
   - uk: /uk/
   - zh: /zh/
 - features.md
+- fastapi-people.md
 - python-types.md
 - Tutorial - User Guide:
   - tutorial/index.md