diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml
new file mode 100644
index 000000000..3c5c881f1
--- /dev/null
+++ b/.github/workflows/topic-repos.yml
@@ -0,0 +1,40 @@
+name: Update Topic Repos
+
+on:
+ schedule:
+ - cron: "0 12 1 * *"
+ workflow_dispatch:
+
+env:
+ UV_SYSTEM_PYTHON: 1
+
+jobs:
+ topic-repos:
+ 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 GitHub Actions dependencies
+ run: uv pip install -r requirements-github-actions.txt
+ - name: Update Topic Repos
+ run: python ./scripts/topic_repos.py
+ env:
+ GITHUB_TOKEN: ${{ secrets.FASTAPI_PR_TOKEN }}
diff --git a/docs/en/data/topic_repos.yml b/docs/en/data/topic_repos.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md
index 5a3b8ee33..3ed04e5c5 100644
--- a/docs/en/docs/external-links.md
+++ b/docs/en/docs/external-links.md
@@ -28,9 +28,12 @@ If you have an article, project, tool, or anything related to **FastAPI** that i
{% endfor %}
{% endfor %}
-## Projects
+## GitHub Repositories
-Latest GitHub projects with the topic `fastapi`:
+Most starred GitHub repositories with the topic `fastapi`:
-
-
+{% for repo in topic_repos %}
+
+★ {{repo.stars}} - {{repo.name}} by @{{repo.owner_login}}.
+
+{% endfor %}
diff --git a/docs/en/docs/js/custom.js b/docs/en/docs/js/custom.js
index ff17710e2..4c0ada312 100644
--- a/docs/en/docs/js/custom.js
+++ b/docs/en/docs/js/custom.js
@@ -1,25 +1,3 @@
-const div = document.querySelector('.github-topic-projects')
-
-async function getDataBatch(page) {
- const response = await fetch(`https://api.github.com/search/repositories?q=topic:fastapi&per_page=100&page=${page}`, { headers: { Accept: 'application/vnd.github.mercy-preview+json' } })
- const data = await response.json()
- return data
-}
-
-async function getData() {
- let page = 1
- let data = []
- let dataBatch = await getDataBatch(page)
- data = data.concat(dataBatch.items)
- const totalCount = dataBatch.total_count
- while (data.length < totalCount) {
- page += 1
- dataBatch = await getDataBatch(page)
- data = data.concat(dataBatch.items)
- }
- return data
-}
-
function setupTermynal() {
document.querySelectorAll(".use-termynal").forEach(node => {
node.style.display = "block";
@@ -158,20 +136,6 @@ async function showRandomAnnouncement(groupId, timeInterval) {
}
async function main() {
- if (div) {
- data = await getData()
- div.innerHTML = ''
- const ul = document.querySelector('.github-topic-projects ul')
- data.forEach(v => {
- if (v.full_name === 'fastapi/fastapi') {
- return
- }
- const li = document.createElement('li')
- li.innerHTML = `★ ${v.stargazers_count} - ${v.full_name} by @${v.owner.login}`
- ul.append(li)
- })
- }
-
setupTermynal();
showRandomAnnouncement('announce-left', 5000)
showRandomAnnouncement('announce-right', 10000)
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index f2abf7f6b..e9a639d0b 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -72,6 +72,7 @@ plugins:
- members: ../en/data/members.yml
- sponsors_badge: ../en/data/sponsors_badge.yml
- sponsors: ../en/data/sponsors.yml
+ - topic_repos: ../en/data/topic_repos.yml
redirects:
redirect_maps:
deployment/deta.md: deployment/cloud.md
diff --git a/scripts/topic_repos.py b/scripts/topic_repos.py
new file mode 100644
index 000000000..bc1497751
--- /dev/null
+++ b/scripts/topic_repos.py
@@ -0,0 +1,80 @@
+import logging
+import secrets
+import subprocess
+from pathlib import Path
+
+import yaml
+from github import Github
+from pydantic import BaseModel, SecretStr
+from pydantic_settings import BaseSettings
+
+
+class Settings(BaseSettings):
+ github_repository: str
+ github_token: SecretStr
+
+
+class Repo(BaseModel):
+ name: str
+ html_url: str
+ stars: int
+ owner_login: str
+ owner_html_url: str
+
+
+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(), per_page=100)
+ r = g.get_repo(settings.github_repository)
+ repos = g.search_repositories(query="topic:fastapi")
+ repos_list = list(repos)
+ final_repos: list[Repo] = []
+ for repo in repos_list[:100]:
+ if repo.full_name == settings.github_repository:
+ continue
+ final_repos.append(
+ Repo(
+ name=repo.name,
+ html_url=repo.html_url,
+ stars=repo.stargazers_count,
+ owner_login=repo.owner.login,
+ owner_html_url=repo.owner.html_url,
+ )
+ )
+ data = [repo.model_dump() for repo in final_repos]
+
+ # Local development
+ # repos_path = Path("../docs/en/data/topic_repos.yml")
+ repos_path = Path("./docs/en/data/topic_repos.yml")
+ repos_old_content = repos_path.read_text(encoding="utf-8")
+ new_repos_content = yaml.dump(data, sort_keys=False, width=200, allow_unicode=True)
+ if repos_old_content == new_repos_content:
+ logging.info("The data hasn't changed. Finishing.")
+ return
+ repos_path.write_text(new_repos_content, 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 = f"fastapi-topic-repos-{secrets.token_hex(4)}"
+ 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(repos_path)], check=True)
+ logging.info("Committing updated file")
+ message = "👥 Update FastAPI GitHub topic repositories"
+ 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 = r.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()