From 177183d9bde4b4f0a51f0d97e9f95588204a619d Mon Sep 17 00:00:00 2001 From: Vinta Chen Date: Wed, 18 Mar 2026 13:48:49 +0800 Subject: [PATCH] add custom website build system Replaces MkDocs with a bespoke Python site generator using Jinja2 templates and Markdown. Adds uv for dependency management, GitHub Actions workflow for deployment, and Makefile targets for local development (fetch_stars, build, preview, deploy). Co-Authored-By: Claude --- .github/workflows/deploy-website.yml | 48 + .gitignore | 14 +- Makefile | 18 +- pyproject.toml | 23 + uv.lock | 258 +++ website/build.py | 502 +++++ website/data/github_stars.json | 2627 ++++++++++++++++++++++ website/fetch_github_stars.py | 192 ++ website/static/main.js | 154 ++ website/static/style.css | 459 ++++ website/templates/base.html | 67 + website/templates/index.html | 146 ++ website/tests/test_build.py | 642 ++++++ website/tests/test_fetch_github_stars.py | 161 ++ 14 files changed, 5298 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/deploy-website.yml create mode 100644 pyproject.toml create mode 100644 uv.lock create mode 100644 website/build.py create mode 100644 website/data/github_stars.json create mode 100644 website/fetch_github_stars.py create mode 100644 website/static/main.js create mode 100644 website/static/style.css create mode 100644 website/templates/base.html create mode 100644 website/templates/index.html create mode 100644 website/tests/test_build.py create mode 100644 website/tests/test_fetch_github_stars.py diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml new file mode 100644 index 00000000..28e254ea --- /dev/null +++ b/.github/workflows/deploy-website.yml @@ -0,0 +1,48 @@ +name: Deploy Website + +on: + push: + branches: + - master + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --no-dev + + - name: Build site + run: uv run python website/build.py + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: website/output/ + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 096c327d..083917b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,15 @@ +# macOS .DS_Store +# python +.venv/ *.py[co] -docs/index.md -site/ +# website +website/output/ -# PyCharm IDE -.idea +# claude code +.claude/skills/ +.superpowers/ +.gstack/ +skills-lock.json diff --git a/Makefile b/Makefile index eda7a8ff..5d6d7581 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,14 @@ site_install: - pip install -r requirements.txt + uv sync --no-dev -site_link: - ln -sf $(CURDIR)/README.md $(CURDIR)/docs/index.md +fetch_stars: + uv run python website/fetch_github_stars.py -site_preview: site_link - mkdocs serve +site_build: + uv run python website/build.py -site_build: site_link - mkdocs build +site_preview: site_build + python -m http.server -d website/output/ 8000 -site_deploy: site_link - mkdocs gh-deploy --clean +site_deploy: site_build + @echo "Deploy via GitHub Actions (push to master)" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d564cde9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "awesome-python" +version = "0.1.0" +description = "An opinionated list of awesome Python frameworks, libraries, software and resources." +requires-python = ">=3.13" +dependencies = [ + "httpx==0.28.1", + "jinja2==3.1.6", + "markdown==3.10.2", +] + +[dependency-groups] +dev = [ + "pytest==9.0.2", + "ruff==0.15.6", +] + +[tool.pytest.ini_options] +testpaths = ["website/tests"] + +[tool.ruff] +target-version = "py313" +line-length = 100 diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..1f7b17c7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,258 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "awesome-python" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx" }, + { name = "jinja2" }, + { name = "markdown" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = "==0.28.1" }, + { name = "jinja2", specifier = "==3.1.6" }, + { name = "markdown", specifier = "==3.10.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = "==9.0.2" }, + { name = "ruff", specifier = "==0.15.6" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] diff --git a/website/build.py b/website/build.py new file mode 100644 index 00000000..b8340eb5 --- /dev/null +++ b/website/build.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python3 +"""Build a single-page HTML site from README.md for the awesome-python website.""" + +import json +import re +import shutil +from pathlib import Path +from typing import TypedDict + +import markdown +from jinja2 import Environment, FileSystemLoader + +# Thematic grouping of categories. Each category name must match exactly +# as it appears in README.md (the ## heading text). +SECTION_GROUPS: list[tuple[str, list[str]]] = [ + ("Web & API", [ + "Web Frameworks", "RESTful API", "GraphQL", "WebSocket", + "ASGI Servers", "WSGI Servers", "HTTP Clients", "Template Engine", + "Web Asset Management", "Web Content Extracting", "Web Crawling", + ]), + ("Data & ML", [ + "Data Analysis", "Data Validation", "Data Visualization", + "Machine Learning", "Deep Learning", "Computer Vision", + "Natural Language Processing", "Recommender Systems", "Science", + "Quantum Computing", + ]), + ("DevOps & Infrastructure", [ + "DevOps Tools", "Distributed Computing", "Task Queues", + "Job Scheduler", "Serverless Frameworks", "Logging", "Processes", + "Shell", "Network Virtualization", "RPC Servers", + ]), + ("Database & Storage", [ + "Database", "Database Drivers", "ORM", "Caching", "Search", + "Serialization", + ]), + ("Development Tools", [ + "Testing", "Debugging Tools", "Code Analysis", "Build Tools", + "Refactoring", "Documentation", "Editor Plugins and IDEs", + "Interactive Interpreter", + ]), + ("CLI & GUI", [ + "Command-line Interface Development", "Command-line Tools", + "GUI Development", + ]), + ("Content & Media", [ + "Audio", "Video", "Image Processing", "HTML Manipulation", + "Text Processing", "Specific Formats Processing", + "File Manipulation", "Downloader", + ]), + ("System & Runtime", [ + "Asynchronous Programming", "Environment Management", + "Package Management", "Package Repositories", "Distribution", + "Implementations", "Built-in Classes Enhancement", + "Functional Programming", "Configuration Files", + ]), + ("Security & Auth", [ + "Authentication", "Cryptography", "Penetration Testing", + "Permissions", + ]), + ("Specialized", [ + "CMS", "Admin Panels", "Email", "Game Development", "Geolocation", + "Hardware", "Internationalization", "Date and Time", + "URL Manipulation", "Robotics", "Microsoft Windows", "Miscellaneous", + "Algorithms and Design Patterns", "Static Site Generator", + ]), + ("Resources", []), # Filled dynamically from parsed resources +] + + +def slugify(name: str) -> str: + """Convert a category name to a URL-friendly slug.""" + slug = name.lower() + slug = re.sub(r"[^a-z0-9\s-]", "", slug) + slug = re.sub(r"[\s]+", "-", slug.strip()) + slug = re.sub(r"-+", "-", slug) + return slug + + +def count_entries(content: str) -> int: + """Count library entries (lines starting with * [ or - [) in a content block.""" + return sum(1 for line in content.split("\n") if re.match(r"\s*[-*]\s+\[", line)) + + +def extract_preview(content: str, *, max_names: int = 4) -> str: + """Extract first N main library names from markdown content for preview text. + + Only includes top-level or single-indent entries (indent <= 3 spaces), + skipping subcategory labels (items without links) and deep sub-entries. + """ + names = [] + for m in re.finditer(r"^(\s*)[-*]\s+\[([^\]]+)\]", content, re.MULTILINE): + indent_len = len(m.group(1)) + if indent_len > 3: + continue + names.append(m.group(2)) + if len(names) >= max_names: + break + return ", ".join(names) + + +def render_content_html(content: str) -> str: + """Render category markdown content to HTML with subcategory detection. + + Lines that are list items without links (e.g., "- Synchronous") are + treated as subcategory headers and rendered as bold dividers. + + Indent levels in the README: + - 0 spaces: top-level entry or subcategory label + - 2 spaces: entry under a subcategory (still a main entry) + - 4+ spaces: sub-entry (e.g., awesome-django under django) + """ + lines = content.split("\n") + out: list[str] = [] + + for line in lines: + stripped = line.strip() + indent_len = len(line) - len(line.lstrip()) + + # Detect subcategory labels: list items without links + m = re.match(r"^[-*]\s+(.+)$", stripped) + if m and "[" not in stripped: + label = m.group(1) + out.append(f'
{label}
') + continue + + # Entry with link and description: * [name](url) - Description. + m = re.match( + r"^\s*[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[-\u2013\u2014]\s*(.+)$", + line, + ) + if m: + name, url, desc = m.groups() + if indent_len > 3: + out.append( + f'
' + f'{name}' + f"
" + ) + else: + out.append( + f'
' + f'{name}' + f'{desc}' + f"
" + ) + continue + + # Link-only entry (no description): * [name](url) + m = re.match(r"^\s*[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*$", line) + if m: + name, url = m.groups() + if indent_len > 3: + out.append( + f'
' + f'{name}' + f"
" + ) + else: + out.append( + f'
' + f'{name}' + f"
" + ) + continue + + return "\n".join(out) + + +def parse_readme(text: str) -> tuple[list[dict], list[dict]]: + """Parse README.md text into categories and resources. + + Returns: + (categories, resources) where each is a list of dicts with keys: + name, slug, description, content + """ + lines = text.split("\n") + + separator_idx = None + for i, line in enumerate(lines): + if line.strip() == "---" and i > 0: + separator_idx = i + break + + if separator_idx is None: + return [], [] + + resources_idx = None + contributing_idx = None + for i, line in enumerate(lines): + if line.strip() == "# Resources": + resources_idx = i + elif line.strip() == "# Contributing": + contributing_idx = i + + cat_end = resources_idx if resources_idx is not None else len(lines) + category_lines = lines[separator_idx + 1 : cat_end] + + resource_lines = [] + if resources_idx is not None: + res_end = contributing_idx if contributing_idx is not None else len(lines) + resource_lines = lines[resources_idx:res_end] + + categories = _extract_sections(category_lines, level=2) + resources = _extract_sections(resource_lines, level=2) + + return categories, resources + + +def _extract_sections(lines: list[str], *, level: int) -> list[dict]: + """Extract ## sections from a block of lines.""" + prefix = "#" * level + " " + sections = [] + current_name = None + current_lines: list[str] = [] + + for line in lines: + if line.startswith(prefix) and not line.startswith(prefix + "#"): + if current_name is not None: + sections.append(_build_section(current_name, current_lines)) + current_name = line[len(prefix) :].strip() + current_lines = [] + elif current_name is not None: + current_lines.append(line) + + if current_name is not None: + sections.append(_build_section(current_name, current_lines)) + + return sections + + +def _build_section(name: str, lines: list[str]) -> dict: + """Build a section dict from a name and its content lines.""" + while lines and not lines[0].strip(): + lines = lines[1:] + while lines and not lines[-1].strip(): + lines = lines[:-1] + + description = "" + content_lines = lines + if lines: + m = re.match(r"^_(.+)_$", lines[0].strip()) + if m: + description = m.group(1) + content_lines = lines[1:] + while content_lines and not content_lines[0].strip(): + content_lines = content_lines[1:] + + content = "\n".join(content_lines).strip() + + return { + "name": name, + "slug": slugify(name), + "description": description, + "content": content, + } + + +def render_markdown(text: str) -> str: + """Render markdown text to HTML.""" + md = markdown.Markdown(extensions=["extra"]) + return md.convert(text) + + +def strip_markdown_links(text: str) -> str: + """Replace [text](url) with just text for plain-text contexts.""" + return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) + + +def render_inline_markdown(text: str) -> str: + """Render inline markdown (links, bold, italic) to HTML.""" + from markupsafe import Markup + + html = markdown.markdown(text) + # Strip wrapping

...

since this is inline content + html = re.sub(r"^

(.*)

$", r"\1", html.strip()) + # Add target/rel to links for external navigation + html = html.replace(" list[dict]: + """Organize categories and resources into thematic section groups.""" + cat_by_name = {c["name"]: c for c in categories} + groups = [] + + for group_name, cat_names in SECTION_GROUPS: + if group_name == "Resources": + # Resources group uses parsed resources directly + group_cats = list(resources) + else: + group_cats = [cat_by_name[n] for n in cat_names if n in cat_by_name] + + if group_cats: + groups.append({ + "name": group_name, + "slug": slugify(group_name), + "categories": group_cats, + }) + + # Any categories not in a group go into "Other" + grouped_names = set() + for _, cat_names in SECTION_GROUPS: + grouped_names.update(cat_names) + ungrouped = [c for c in categories if c["name"] not in grouped_names] + if ungrouped: + groups.append({ + "name": "Other", + "slug": "other", + "categories": ungrouped, + }) + + return groups + + +class Entry(TypedDict): + name: str + url: str + description: str + category: str + group: str + stars: int | None + owner: str | None + pushed_at: str | None + + +class StarData(TypedDict): + stars: int + owner: str + pushed_at: str + fetched_at: str + + +GITHUB_REPO_URL_RE = re.compile( + r"^https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$" +) + + +def extract_github_repo(url: str) -> str | None: + """Extract owner/repo from a GitHub repo URL. Returns None for non-GitHub URLs.""" + m = GITHUB_REPO_URL_RE.match(url) + return m.group(1) if m else None + + +def load_stars(path: Path) -> dict[str, StarData]: + """Load star data from JSON. Returns empty dict if file doesn't exist or is corrupt.""" + if path.exists(): + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + return {} + + +def sort_entries(entries: list[dict]) -> list[dict]: + """Sort entries by stars descending, then name ascending. No-star entries go last.""" + def sort_key(entry: dict) -> tuple[int, int, str]: + stars = entry["stars"] + name = entry["name"].lower() + if stars is None: + return (1, 0, name) + return (0, -stars, name) + return sorted(entries, key=sort_key) + + +def extract_entries( + categories: list[dict], + resources: list[dict], + groups: list[dict], +) -> list[dict]: + """Flatten categories into individual library entries for table display.""" + cat_to_group: dict[str, str] = {} + for group in groups: + for cat in group["categories"]: + cat_to_group[cat["name"]] = group["name"] + + entries: list[dict] = [] + for cat in categories: + group_name = cat_to_group.get(cat["name"], "Other") + last_entry_indent = -1 + for line in cat["content"].split("\n"): + indent_len = len(line) - len(line.lstrip()) + + # Link-only sub-item deeper than parent → "also see" + m_sub = re.match(r"\s*[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*$", line) + if m_sub and indent_len > last_entry_indent >= 0 and entries: + entries[-1]["also_see"].append({ + "name": m_sub.group(1), + "url": m_sub.group(2), + }) + continue + + if indent_len > 3: + continue + m = re.match( + r"\s*[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*(?:[-\u2013\u2014]\s*(.+))?$", + line, + ) + if m: + last_entry_indent = indent_len + entries.append({ + "name": m.group(1), + "url": m.group(2), + "description": render_inline_markdown(m.group(3)) if m.group(3) else "", + "category": cat["name"], + "group": group_name, + "stars": None, + "owner": None, + "pushed_at": None, + "also_see": [], + }) + return entries + + +def build(repo_root: str) -> None: + """Main build: parse README, render single-page HTML via Jinja2 templates.""" + repo = Path(repo_root) + website = repo / "website" + readme_text = (repo / "README.md").read_text(encoding="utf-8") + + # Extract subtitle from the first non-empty, non-heading line + subtitle = "" + for line in readme_text.split("\n"): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + subtitle = stripped + break + + categories, resources = parse_readme(readme_text) + + # Enrich with entry counts, rendered HTML, previews, and clean descriptions + for cat in categories + resources: + cat["entry_count"] = count_entries(cat["content"]) + cat["content_html"] = render_content_html(cat["content"]) + cat["preview"] = extract_preview(cat["content"]) + cat["description"] = strip_markdown_links(cat["description"]) + + total_entries = sum(c["entry_count"] for c in categories) + + # Organize into groups + groups = group_categories(categories, resources) + + # Flatten entries for table view + entries = extract_entries(categories, resources, groups) + + # Load and merge GitHub star data + stars_data = load_stars(website / "data" / "github_stars.json") + for entry in entries: + repo_key = extract_github_repo(entry["url"]) + if repo_key and repo_key in stars_data: + entry["stars"] = stars_data[repo_key]["stars"] + entry["owner"] = stars_data[repo_key]["owner"] + entry["pushed_at"] = stars_data[repo_key].get("pushed_at", "") + + # Sort by stars descending + entries = sort_entries(entries) + + # Set up Jinja2 + env = Environment( + loader=FileSystemLoader(website / "templates"), + autoescape=True, + ) + + # Output directory + site_dir = website / "output" + if site_dir.exists(): + shutil.rmtree(site_dir) + site_dir.mkdir(parents=True) + + # Generate single index.html + tpl_index = env.get_template("index.html") + (site_dir / "index.html").write_text( + tpl_index.render( + categories=categories, + resources=resources, + groups=groups, + subtitle=subtitle, + entries=entries, + total_entries=total_entries, + total_categories=len(categories), + ), + encoding="utf-8", + ) + + # Copy static assets + static_src = website / "static" + static_dst = site_dir / "static" + if static_src.exists(): + shutil.copytree(static_src, static_dst) + + # Write CNAME + (site_dir / "CNAME").write_text("awesome-python.com\n", encoding="utf-8") + + print(f"Built single page with {len(categories)} categories + {len(resources)} resources") + print(f"Total entries: {total_entries}") + print(f"Output: {site_dir}") + + +if __name__ == "__main__": + build(str(Path(__file__).parent.parent)) diff --git a/website/data/github_stars.json b/website/data/github_stars.json new file mode 100644 index 00000000..1476651f --- /dev/null +++ b/website/data/github_stars.json @@ -0,0 +1,2627 @@ +{ + "0rpc/zerorpc-python": { + "stars": 3237, + "owner": "0rpc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "567-labs/instructor": { + "stars": 12554, + "owner": "567-labs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Alir3z4/html2text": { + "stars": 2135, + "owner": "Alir3z4", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "AnswerDotAI/fasthtml": { + "stars": 6883, + "owner": "AnswerDotAI", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "AtsushiSakai/PythonRobotics": { + "stars": 28887, + "owner": "AtsushiSakai", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "BeanieODM/beanie": { + "stars": 2661, + "owner": "BeanieODM", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Bogdanp/dramatiq": { + "stars": 5172, + "owner": "Bogdanp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ChristosChristofidis/awesome-deep-learning": { + "stars": 27712, + "owner": "ChristosChristofidis", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "CleanCut/green": { + "stars": 806, + "owner": "CleanCut", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Cornices/cornice": { + "stars": 390, + "owner": "Cornices", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "DLR-RM/stable-baselines3": { + "stars": 12908, + "owner": "DLR-RM", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Delgan/loguru": { + "stars": 23690, + "owner": "Delgan", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "DiffSK/configobj": { + "stars": 337, + "owner": "DiffSK", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "DmytroLitvinov/awesome-flake8-extensions": { + "stars": 1276, + "owner": "DmytroLitvinov", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "EmilStenstrom/justhtml": { + "stars": 1116, + "owner": "EmilStenstrom", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "FactoryBoy/factory_boy": { + "stars": 3781, + "owner": "FactoryBoy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "HBNetwork/python-decouple": { + "stars": 3017, + "owner": "HBNetwork", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "HypothesisWorks/hypothesis": { + "stars": 8498, + "owner": "HypothesisWorks", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Instagram/MonkeyType": { + "stars": 4996, + "owner": "Instagram", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "IronLanguages/ironpython3": { + "stars": 2735, + "owner": "IronLanguages", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "JaidedAI/EasyOCR": { + "stars": 29096, + "owner": "JaidedAI", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Kozea/pygal": { + "stars": 2752, + "owner": "Kozea", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Lightning-AI/pytorch-lightning": { + "stars": 30934, + "owner": "Lightning-AI", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "LuminosoInsight/python-ftfy": { + "stars": 4015, + "owner": "rspeer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "MagicStack/uvloop": { + "stars": 11687, + "owner": "MagicStack", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ManimCommunity/manim": { + "stars": 37253, + "owner": "ManimCommunity", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Manisso/fsociety": { + "stars": 11925, + "owner": "Manisso", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Maratyszcza/PeachPy": { + "stars": 2048, + "owner": "Maratyszcza", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "MasoniteFramework/masonite": { + "stars": 2365, + "owner": "MasoniteFramework", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "MechanicalSoup/MechanicalSoup": { + "stars": 4850, + "owner": "MechanicalSoup", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "MervinPraison/PraisonAI": { + "stars": 5677, + "owner": "MervinPraison", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Microsoft/PTVS": { + "stars": 2567, + "owner": "microsoft", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "MongoEngine/mongoengine": { + "stars": 4349, + "owner": "MongoEngine", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "NicolasHug/Surprise": { + "stars": 6772, + "owner": "NicolasHug", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Nuitka/Nuitka": { + "stars": 14642, + "owner": "Nuitka", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "OpenBB-finance/OpenBB": { + "stars": 63238, + "owner": "OpenBB-finance", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Parisson/TimeSide": { + "stars": 394, + "owner": "Parisson", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Parsely/streamparse": { + "stars": 1504, + "owner": "pystorm", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PennyLaneAI/pennylane": { + "stars": 3111, + "owner": "PennyLaneAI", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PrefectHQ/prefect": { + "stars": 21889, + "owner": "PrefectHQ", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PyCQA/flake8": { + "stars": 3770, + "owner": "PyCQA", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PyCQA/prospector": { + "stars": 2074, + "owner": "prospector-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PyMySQL/PyMySQL": { + "stars": 7838, + "owner": "PyMySQL", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "PyMySQL/mysqlclient": { + "stars": 2525, + "owner": "PyMySQL", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Pylons/colander": { + "stars": 464, + "owner": "Pylons", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Pylons/waitress": { + "stars": 1572, + "owner": "Pylons", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Qiskit/qiskit": { + "stars": 7137, + "owner": "Qiskit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "RaRe-Technologies/gensim": { + "stars": 16375, + "owner": "piskvorky", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "RasaHQ/rasa": { + "stars": 21086, + "owner": "RasaHQ", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "RaylockLLC/DearPyGui": { + "stars": 15279, + "owner": "hoffstadt", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "SCons/scons": { + "stars": 2357, + "owner": "SCons", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "SciTools/cartopy": { + "stars": 1589, + "owner": "SciTools", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ScrapeGraphAI/toonify": { + "stars": 323, + "owner": "ScrapeGraphAI", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "SmileyChris/django-countries": { + "stars": 1521, + "owner": "SmileyChris", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Suor/django-cacheops": { + "stars": 2263, + "owner": "Suor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Suor/funcy": { + "stars": 3501, + "owner": "Suor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Supervisor/supervisor": { + "stars": 9007, + "owner": "Supervisor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Tencent/rapidjson": { + "stars": 15007, + "owner": "Tencent", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Textualize/rich": { + "stars": 55801, + "owner": "Textualize", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Textualize/textual": { + "stars": 34878, + "owner": "Textualize", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "TheAlgorithms/Python": { + "stars": 218785, + "owner": "TheAlgorithms", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "TkTech/pysimdjson": { + "stars": 761, + "owner": "TkTech", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "TomNicholas/Python-for-Scientists": { + "stars": 357, + "owner": "TomNicholas", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "Valloric/YouCompleteMe": { + "stars": 26276, + "owner": "ycm-core", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "WhyNotHugo/python-barcode": { + "stars": 649, + "owner": "WhyNotHugo", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ZoomerAnalytics/xlwings": { + "stars": 6, + "owner": "ZoomerAnalytics", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "aaugustin/websockets": { + "stars": 5643, + "owner": "python-websockets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "abhiTronix/vidgear": { + "stars": 3684, + "owner": "abhiTronix", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "aboSamoor/polyglot": { + "stars": 2368, + "owner": "aboSamoor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "agno-agi/agno": { + "stars": 38754, + "owner": "agno-agi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ahupp/python-magic": { + "stars": 2896, + "owner": "ahupp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "aizvorski/scikit-video": { + "stars": 152, + "owner": "aizvorski", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ajenti/ajenti": { + "stars": 7908, + "owner": "ajenti", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "alecthomas/voluptuous": { + "stars": 1847, + "owner": "alecthomas", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "altair-viz/altair": { + "stars": 10301, + "owner": "vega", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "amitt001/delegator.py": { + "stars": 1746, + "owner": "amitt001", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "amoffat/sh": { + "stars": 7241, + "owner": "amoffat", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "amosgyamfi/awesome-fasthtml": { + "stars": 79, + "owner": "amosgyamfi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "andialbrecht/sqlparse": { + "stars": 3999, + "owner": "andialbrecht", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ansible/ansible": { + "stars": 68310, + "owner": "ansible", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "apache/spark": { + "stars": 42992, + "owner": "apache", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "arrow-py/arrow": { + "stars": 9035, + "owner": "arrow-py", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "art049/odmantic": { + "stars": 1168, + "owner": "art049", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "astral-sh/ruff": { + "stars": 46329, + "owner": "astral-sh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "astral-sh/ty": { + "stars": 17739, + "owner": "astral-sh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "astral-sh/uv": { + "stars": 81192, + "owner": "astral-sh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "asweigart/pyautogui": { + "stars": 12363, + "owner": "asweigart", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "aws/aws-sdk-pandas": { + "stars": 4106, + "owner": "aws", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bbangert/beaker": { + "stars": 545, + "owner": "bbangert", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "beetbox/audioread": { + "stars": 536, + "owner": "beetbox", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "beetbox/beets": { + "stars": 14856, + "owner": "beetbox", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "benedekrozemberczki/karateclub": { + "stars": 2276, + "owner": "benedekrozemberczki", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "benfred/implicit": { + "stars": 3773, + "owner": "benfred", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "benfred/py-spy": { + "stars": 15031, + "owner": "benfred", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "benhamner/Metrics": { + "stars": 1654, + "owner": "benhamner", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "benoitc/gunicorn": { + "stars": 10481, + "owner": "benoitc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bfly123/claude_code_bridge": { + "stars": 1642, + "owner": "bfly123", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bloomberg/bqplot": { + "stars": 3684, + "owner": "bqplot", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bokeh/bokeh": { + "stars": 20365, + "owner": "bokeh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "boppreh/mouse": { + "stars": 961, + "owner": "boppreh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "borgbackup/borg": { + "stars": 13081, + "owner": "borgbackup", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "boto/boto3": { + "stars": 9736, + "owner": "boto", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bpython/bpython": { + "stars": 2771, + "owner": "bpython", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "browser-use/browser-use": { + "stars": 81099, + "owner": "browser-use", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "bterwijn/memory_graph": { + "stars": 771, + "owner": "bterwijn", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "buildout/buildout": { + "stars": 613, + "owner": "buildout", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "buriy/python-readability": { + "stars": 2894, + "owner": "buriy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "canonical/cloud-init": { + "stars": 3627, + "owner": "canonical", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "carlosescri/DottedDict": { + "stars": 222, + "owner": "carlosescri", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "cdgriffith/Box": { + "stars": 2822, + "owner": "cdgriffith", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "chaostoolkit/chaostoolkit": { + "stars": 2001, + "owner": "chaostoolkit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "chapmanb/bcbb": { + "stars": 645, + "owner": "chapmanb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "chapmanb/bcbio-nextgen": { + "stars": 1027, + "owner": "bcbio", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "chardet/chardet": { + "stars": 2486, + "owner": "chardet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "chriskiehl/Gooey": { + "stars": 22025, + "owner": "chriskiehl", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "clips/pattern": { + "stars": 8856, + "owner": "clips", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "cobrateam/splinter": { + "stars": 2767, + "owner": "cobrateam", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "codeinthehole/purl": { + "stars": 303, + "owner": "codeinthehole", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "codelucas/newspaper": { + "stars": 15009, + "owner": "codelucas", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "coleifer/huey": { + "stars": 5940, + "owner": "coleifer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "coleifer/micawber": { + "stars": 674, + "owner": "coleifer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "coleifer/peewee": { + "stars": 11952, + "owner": "coleifer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "conda/conda": { + "stars": 7342, + "owner": "conda", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "cookiecutter/cookiecutter": { + "stars": 24744, + "owner": "cookiecutter", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "copier-org/copier": { + "stars": 3214, + "owner": "copier-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "crossbario/autobahn-python": { + "stars": 2534, + "owner": "crossbario", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "cython/cython": { + "stars": 10654, + "owner": "cython", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dahlia/awesome-sqlalchemy": { + "stars": 3031, + "owner": "dahlia", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dashingsoft/pyarmor": { + "stars": 4989, + "owner": "dashingsoft", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dask/dask": { + "stars": 13768, + "owner": "dask", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "datafolklabs/cement": { + "stars": 1341, + "owner": "datafolklabs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "datastax/python-driver": { + "stars": 1426, + "owner": "apache", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dateutil/dateutil": { + "stars": 2604, + "owner": "dateutil", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "davidaurelio/hashids-python": { + "stars": 1423, + "owner": "davidaurelio", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "daviddrysdale/python-phonenumbers": { + "stars": 3720, + "owner": "daviddrysdale", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "davidhalter/jedi": { + "stars": 6123, + "owner": "davidhalter", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "davidhalter/jedi-vim": { + "stars": 5319, + "owner": "davidhalter", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dbader/schedule": { + "stars": 12246, + "owner": "dbader", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dbcli/litecli": { + "stars": 3214, + "owner": "dbcli", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dbcli/mycli": { + "stars": 11886, + "owner": "dbcli", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dbcli/pgcli": { + "stars": 13073, + "owner": "dbcli", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "deanmalmgren/textract": { + "stars": 4482, + "owner": "deanmalmgren", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "derek73/python-nameparser": { + "stars": 702, + "owner": "derek73", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "desbordante/desbordante-core": { + "stars": 469, + "owner": "Desbordante", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "devpi/devpi": { + "stars": 1146, + "owner": "devpi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "devsnd/tinytag": { + "stars": 805, + "owner": "tinytag", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dfunckt/django-rules": { + "stars": 1970, + "owner": "dfunckt", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dgunning/edgartools": { + "stars": 1859, + "owner": "dgunning", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dhamaniasad/awesome-postgres": { + "stars": 11767, + "owner": "dhamaniasad", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dimka665/awesome-slugify": { + "stars": 491, + "owner": "voronind", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-cache-machine/django-cache-machine": { + "stars": 885, + "owner": "django-cache-machine", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-compressor/django-compressor": { + "stars": 2871, + "owner": "django-compressor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-guardian/django-guardian": { + "stars": 3893, + "owner": "django-guardian", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-haystack/django-haystack": { + "stars": 3800, + "owner": "django-haystack", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-haystack/pysolr": { + "stars": 697, + "owner": "django-haystack", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django-tastypie/django-tastypie": { + "stars": 3955, + "owner": "django-tastypie", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django/channels": { + "stars": 6336, + "owner": "django", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django/daphne": { + "stars": 2651, + "owner": "django", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "django/django": { + "stars": 87081, + "owner": "django", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dmlc/xgboost": { + "stars": 28138, + "owner": "dmlc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "docling-project/docling": { + "stars": 55968, + "owner": "docling-project", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dpkp/kafka-python": { + "stars": 5887, + "owner": "dpkp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dry-python/returns": { + "stars": 4238, + "owner": "dry-python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "dynaconf/dynaconf": { + "stars": 4272, + "owner": "dynaconf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "elapouya/python-docx-template": { + "stars": 2588, + "owner": "elapouya", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "elastic/elasticsearch-dsl-py": { + "stars": 3883, + "owner": "elastic", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "eliben/pyelftools": { + "stars": 2217, + "owner": "eliben", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "emcconville/wand": { + "stars": 1479, + "owner": "emcconville", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "emmett-framework/granian": { + "stars": 5173, + "owner": "emmett-framework", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "encode/django-rest-framework": { + "stars": 29928, + "owner": "encode", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "encode/httpx": { + "stars": 15163, + "owner": "encode", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "encode/uvicorn": { + "stars": 10496, + "owner": "Kludex", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "erikrose/more-itertools": { + "stars": 4043, + "owner": "more-itertools", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "esnme/ultrajson": { + "stars": 4474, + "owner": "ultrajson", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "evhub/coconut": { + "stars": 4313, + "owner": "evhub", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "fabric/fabric": { + "stars": 15406, + "owner": "fabric", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "facebook/PathPicker": { + "stars": 5232, + "owner": "facebook", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "facebook/pyre-check": { + "stars": 7153, + "owner": "facebook", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "facebookresearch/hydra": { + "stars": 10258, + "owner": "facebookresearch", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "faif/python-patterns": { + "stars": 42795, + "owner": "faif", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "falconry/falcon": { + "stars": 9805, + "owner": "falconry", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "feature-engine/feature_engine": { + "stars": 2214, + "owner": "feature-engine", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "feincms/feincms": { + "stars": 1077, + "owner": "feincms", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "fengsp/plan": { + "stars": 1182, + "owner": "fengsp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "fighting41love/funNLP": { + "stars": 79457, + "owner": "fighting41love", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "flask-admin/flask-admin": { + "stars": 6057, + "owner": "pallets-eco", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "flask-api/flask-api": { + "stars": 1468, + "owner": "flask-api", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "flask-restful/flask-restful": { + "stars": 6924, + "owner": "flask-restful", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "fogleman/Quads": { + "stars": 1223, + "owner": "fogleman", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "fxsjy/jieba": { + "stars": 34802, + "owner": "fxsjy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gabrielfalcao/HTTPretty": { + "stars": 2209, + "owner": "gabrielfalcao", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gaojiuli/toapi": { + "stars": 3555, + "owner": "elliotgao2", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gawel/pyquery": { + "stars": 2379, + "owner": "gawel", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "geopandas/geopandas": { + "stars": 5067, + "owner": "geopandas", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "geopy/geopy": { + "stars": 4783, + "owner": "geopy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "getnikola/nikola": { + "stars": 2722, + "owner": "getnikola", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "getpelican/pelican": { + "stars": 13249, + "owner": "getpelican", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "getsentry/responses": { + "stars": 4330, + "owner": "getsentry", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "getsentry/sentry-python": { + "stars": 2156, + "owner": "getsentry", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gevent/gevent": { + "stars": 6443, + "owner": "gevent", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "giampaolo/psutil": { + "stars": 11108, + "owner": "giampaolo", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "glamp/bashplotlib": { + "stars": 1917, + "owner": "glamp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gleitz/howdoi": { + "stars": 10831, + "owner": "gleitz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "google/jax": { + "stars": 35125, + "owner": "jax-ml", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "google/python-fire": { + "stars": 28155, + "owner": "google", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "google/pytype": { + "stars": 5029, + "owner": "google", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "google/yapf": { + "stars": 13991, + "owner": "google", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gorakhargosh/watchdog": { + "stars": 7283, + "owner": "gorakhargosh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gotcha/ipdb": { + "stars": 1968, + "owner": "gotcha", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "grantjenks/python-diskcache": { + "stars": 2846, + "owner": "grantjenks", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "grantjenks/python-sortedcontainers": { + "stars": 3935, + "owner": "grantjenks", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "graphql-python/graphene": { + "stars": 8252, + "owner": "graphql-python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gruns/furl": { + "stars": 2797, + "owner": "gruns", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "gruns/icecream": { + "stars": 10032, + "owner": "gruns", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "h2oai/h2o-3": { + "stars": 7511, + "owner": "h2oai", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "has2k1/plotnine": { + "stars": 4521, + "owner": "has2k1", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "hbldh/bleak": { + "stars": 2351, + "owner": "hbldh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "hi-primus/optimus": { + "stars": 1539, + "owner": "hi-primus", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "html5lib/html5lib-python": { + "stars": 1218, + "owner": "html5lib", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "httpie/cli": { + "stars": 37725, + "owner": "httpie", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "hugapi/hug": { + "stars": 6905, + "owner": "hugapi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "huggingface/diffusers": { + "stars": 33076, + "owner": "huggingface", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "huggingface/transformers": { + "stars": 157984, + "owner": "huggingface", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "humiaozuzu/awesome-flask": { + "stars": 12696, + "owner": "humiaozuzu", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ibayer/fastFM": { + "stars": 1090, + "owner": "ibayer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ijl/orjson": { + "stars": 7961, + "owner": "ijl", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "indico/indico": { + "stars": 2031, + "owner": "indico", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "inducer/pudb": { + "stars": 3218, + "owner": "inducer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "infiniflow/ragflow": { + "stars": 75265, + "owner": "infiniflow", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ionelmc/python-hunter": { + "stars": 866, + "owner": "ionelmc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ionelmc/python-manhole": { + "stars": 400, + "owner": "ionelmc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "isnowfy/snownlp": { + "stars": 6614, + "owner": "isnowfy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jab/bidict": { + "stars": 1578, + "owner": "jab", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jaraco/path.py": { + "stars": 1124, + "owner": "jaraco", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/django-debug-toolbar": { + "stars": 8351, + "owner": "django-commons", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/django-oauth-toolkit": { + "stars": 3310, + "owner": "django-oauth", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/django-pipeline": { + "stars": 1543, + "owner": "jazzband", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/geojson": { + "stars": 984, + "owner": "jazzband", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/pip-tools": { + "stars": 7993, + "owner": "jazzband", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jazzband/tablib": { + "stars": 4751, + "owner": "jazzband", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jeffknupp/sandman2": { + "stars": 2044, + "owner": "jeffknupp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jek/blinker": { + "stars": 2034, + "owner": "pallets-eco", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jendrikseipp/vulture": { + "stars": 4379, + "owner": "jendrikseipp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jet-admin/jet-bridge": { + "stars": 1794, + "owner": "jet-admin", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jfkirk/tensorrec": { + "stars": 1302, + "owner": "jfkirk", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jiaaro/pydub": { + "stars": 9743, + "owner": "jiaaro", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jindaxiang/akshare": { + "stars": 17394, + "owner": "akfamily", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jmcnamara/XlsxWriter": { + "stars": 3920, + "owner": "jmcnamara", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "joke2k/faker": { + "stars": 19220, + "owner": "joke2k", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jonathanslenders/ptpython": { + "stars": 5410, + "owner": "prompt-toolkit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jonathanslenders/python-prompt-toolkit": { + "stars": 10329, + "owner": "prompt-toolkit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jorgenschaefer/elpy": { + "stars": 1940, + "owner": "jorgenschaefer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jpadilla/pyjwt": { + "stars": 5622, + "owner": "jpadilla", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "jschneier/django-storages": { + "stars": 2939, + "owner": "jschneier", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "keleshev/schema": { + "stars": 2944, + "owner": "keleshev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "keon/algorithms": { + "stars": 25390, + "owner": "keon", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "keras-team/keras": { + "stars": 63928, + "owner": "keras-team", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "keunwoochoi/kapre": { + "stars": 946, + "owner": "keunwoochoi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kevin1024/vcrpy": { + "stars": 2951, + "owner": "kevin1024", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kiwicom/schemathesis": { + "stars": 3114, + "owner": "schemathesis", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "klen/mixer": { + "stars": 954, + "owner": "klen", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "knipknap/SpiffWorkflow": { + "stars": 1864, + "owner": "sartography", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kootenpv/yagmail": { + "stars": 2725, + "owner": "kootenpv", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kornia/kornia": { + "stars": 11119, + "owner": "kornia", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kreuzberg-dev/kreuzberg": { + "stars": 6736, + "owner": "kreuzberg-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "kurtmckee/feedparser": { + "stars": 2327, + "owner": "kurtmckee", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "laixintao/iredis": { + "stars": 2728, + "owner": "laixintao", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lancopku/pkuseg-python": { + "stars": 6702, + "owner": "lancopku", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "langchain-ai/langchain": { + "stars": 129943, + "owner": "langchain-ai", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lektor/lektor": { + "stars": 3926, + "owner": "lektor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lemire/simdjson": { + "stars": 23448, + "owner": "simdjson", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lepture/authlib": { + "stars": 5244, + "owner": "authlib", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lepture/mistune": { + "stars": 2998, + "owner": "lepture", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lericson/pylibmc": { + "stars": 493, + "owner": "lericson", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "libAudioFlux/audioFlux": { + "stars": 3281, + "owner": "libAudioFlux", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "librosa/librosa": { + "stars": 8263, + "owner": "librosa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "libvips/pyvips": { + "stars": 789, + "owner": "libvips", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lincolnloop/python-qrcode": { + "stars": 4864, + "owner": "lincolnloop", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "linkedin/shiv": { + "stars": 1918, + "owner": "linkedin", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "litestar-org/litestar": { + "stars": 8099, + "owner": "litestar-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "litestar-org/polyfactory": { + "stars": 1428, + "owner": "litestar-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lk-geimfari/mimesis": { + "stars": 4799, + "owner": "lk-geimfari", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "locustio/locust": { + "stars": 27608, + "owner": "locustio", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lorien/grab": { + "stars": 2458, + "owner": "lorien", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "lyst/lightfm": { + "stars": 5066, + "owner": "lyst", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "maciejkula/spotlight": { + "stars": 3042, + "owner": "maciejkula", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "madmaze/pytesseract": { + "stars": 6321, + "owner": "madmaze", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mahmoud/boltons": { + "stars": 6856, + "owner": "mahmoud", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mailgun/flanker": { + "stars": 1650, + "owner": "mailgun", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marcelotduarte/cx_Freeze": { + "stars": 1532, + "owner": "marcelotduarte", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marimo-team/marimo": { + "stars": 19725, + "owner": "marimo-team", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "markusschanta/awesome-jupyter": { + "stars": 4569, + "owner": "markusschanta", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marph91/jimmy": { + "stars": 400, + "owner": "marph91", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marrow/mailer": { + "stars": 293, + "owner": "marrow", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marshmallow-code/marshmallow": { + "stars": 7228, + "owner": "marshmallow-code", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "marshmallow-code/webargs": { + "stars": 1405, + "owner": "marshmallow-code", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "martinblech/xmltodict": { + "stars": 5726, + "owner": "martinblech", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "martinrusev/imbox": { + "stars": 1211, + "owner": "martinrusev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "matplotlib/matplotlib": { + "stars": 22585, + "owner": "matplotlib", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "metawilm/cl-python": { + "stars": 394, + "owner": "metawilm", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mhammond/pywin32": { + "stars": 5531, + "owner": "mhammond", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mher/flower": { + "stars": 7130, + "owner": "mher", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "michaelhelmick/lassie": { + "stars": 630, + "owner": "michaelhelmick", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "micropython/micropython": { + "stars": 21553, + "owner": "micropython", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "microsoft/markitdown": { + "stars": 90875, + "owner": "microsoft", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "miguelgrinberg/microdot": { + "stars": 2093, + "owner": "miguelgrinberg", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mindflayer/python-mocket": { + "stars": 309, + "owner": "mindflayer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mindsdb/mindsdb": { + "stars": 38775, + "owner": "mindsdb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mingrammer/diagrams": { + "stars": 42078, + "owner": "mingrammer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mininet/mininet": { + "stars": 5788, + "owner": "mininet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "miracle2k/flask-assets": { + "stars": 459, + "owner": "miracle2k", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "miracle2k/webassets": { + "stars": 935, + "owner": "miracle2k", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "miso-belica/sumy": { + "stars": 3664, + "owner": "miso-belica", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mitmproxy/pdoc": { + "stars": 2474, + "owner": "mitmproxy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mitsuhiko/pluginbase": { + "stars": 1141, + "owner": "mitsuhiko", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mitsuhiko/unp": { + "stars": 455, + "owner": "mitsuhiko", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mkdocs/mkdocs": { + "stars": 21860, + "owner": "mkdocs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "modoboa/modoboa": { + "stars": 3468, + "owner": "modoboa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mongodb/django-mongodb-backend": { + "stars": 218, + "owner": "mongodb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mongodb/mongo-python-driver": { + "stars": 4338, + "owner": "mongodb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "moses-palmer/pynput": { + "stars": 2125, + "owner": "moses-palmer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mozilla/unicode-slugify": { + "stars": 328, + "owner": "mozilla", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mozillazg/python-pinyin": { + "stars": 5271, + "owner": "mozillazg", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mpdavis/python-jose": { + "stars": 1743, + "owner": "mpdavis", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mpi4py/mpi4py": { + "stars": 902, + "owner": "mpi4py", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mre/awesome-static-analysis": { + "stars": 14439, + "owner": "analysis-tools-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "msiemens/tinydb": { + "stars": 7487, + "owner": "msiemens", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mstamy2/PyPDF2": { + "stars": 9878, + "owner": "py-pdf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mwaskom/seaborn": { + "stars": 13770, + "owner": "mwaskom", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "mymarilyn/clickhouse-driver": { + "stars": 1293, + "owner": "mymarilyn", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "napalm-automation/napalm": { + "stars": 2438, + "owner": "napalm-automation", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nficano/python-lambda": { + "stars": 1521, + "owner": "nficano", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nicfit/eyeD3": { + "stars": 631, + "owner": "nicfit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nose-devs/nose2": { + "stars": 822, + "owner": "nose-devs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "noxrepo/pox": { + "stars": 652, + "owner": "noxrepo", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nucleic/enaml": { + "stars": 1574, + "owner": "nucleic", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "numba/numba": { + "stars": 10935, + "owner": "numba", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nvbn/thefuck": { + "stars": 95714, + "owner": "nvbn", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "nvdv/vprof": { + "stars": 3982, + "owner": "nvdv", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "oauthlib/oauthlib": { + "stars": 2958, + "owner": "oauthlib", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "offerrall/FuncToWeb": { + "stars": 389, + "owner": "offerrall", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "openai/gym": { + "stars": 37100, + "owner": "openai", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "openembedded/bitbake": { + "stars": 509, + "owner": "openembedded", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "openstack/cliff": { + "stars": 260, + "owner": "openstack", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "orsinium/textdistance": { + "stars": 3524, + "owner": "life4", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets-eco/flask-debugtoolbar": { + "stars": 980, + "owner": "pallets-eco", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/click": { + "stars": 17367, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/flask": { + "stars": 71376, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/itsdangerous": { + "stars": 3102, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/jinja": { + "stars": 11513, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/markupsafe": { + "stars": 685, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pallets/werkzeug": { + "stars": 6849, + "owner": "pallets", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "paramiko/paramiko": { + "stars": 9712, + "owner": "paramiko", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pathsim/pathsim": { + "stars": 334, + "owner": "pathsim", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pathwaycom/pathway": { + "stars": 60281, + "owner": "pathwaycom", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "patrys/httmock": { + "stars": 472, + "owner": "patrys", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "patx/pickledb": { + "stars": 1069, + "owner": "patx", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pdfminer/pdfminer.six": { + "stars": 6933, + "owner": "pdfminer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pennersr/django-allauth": { + "stars": 10307, + "owner": "pennersr", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "peterbrittain/asciimatics": { + "stars": 4271, + "owner": "peterbrittain", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pgjones/hypercorn": { + "stars": 1536, + "owner": "pgjones", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pgmpy/pgmpy": { + "stars": 3213, + "owner": "pgmpy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pikepdf/pikepdf": { + "stars": 2667, + "owner": "pikepdf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "planetopendata/awesome-sqlite": { + "stars": 388, + "owner": "planetopendata", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "platformio/platformio-core": { + "stars": 8931, + "owner": "platformio", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "plotly/plotly.py": { + "stars": 18354, + "owner": "plotly", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pndurette/gTTS": { + "stars": 2595, + "owner": "pndurette", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pola-rs/polars": { + "stars": 37775, + "owner": "pola-rs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ponyorm/pony": { + "stars": 3826, + "owner": "ponyorm", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "prabhupant/python-ds": { + "stars": 3074, + "owner": "prabhupant", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pricingassistant/mrq": { + "stars": 896, + "owner": "pricingassistant", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "prompt-toolkit/python-prompt-toolkit": { + "stars": 10329, + "owner": "prompt-toolkit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "psf/black": { + "stars": 41430, + "owner": "psf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "psf/requests": { + "stars": 53881, + "owner": "psf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "psf/requests-html": { + "stars": 13869, + "owner": "psf", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "psycopg/psycopg": { + "stars": 2322, + "owner": "psycopg", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pudo/dataset": { + "stars": 4853, + "owner": "pudo", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pwaller/pyfiglet": { + "stars": 1545, + "owner": "pwaller", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "py2exe/py2exe": { + "stars": 995, + "owner": "py2exe", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pybee/toga": { + "stars": 5323, + "owner": "beeware", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pybuilder/pybuilder": { + "stars": 1956, + "owner": "pybuilder", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyca/cryptography": { + "stars": 7515, + "owner": "pyca", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyca/pynacl": { + "stars": 1185, + "owner": "pyca", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pydantic/pydantic": { + "stars": 27237, + "owner": "pydantic", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pydantic/pydantic-ai": { + "stars": 15531, + "owner": "pydantic", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyenv-win/pyenv-win": { + "stars": 7064, + "owner": "pyenv-win", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyenv/pyenv": { + "stars": 44440, + "owner": "pyenv", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyeve/cerberus": { + "stars": 3270, + "owner": "pyeve", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyeve/eve": { + "stars": 6746, + "owner": "pyeve", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyexcel/pyexcel": { + "stars": 1284, + "owner": "pyexcel", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyglet/pyglet": { + "stars": 2172, + "owner": "pyglet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pygraphviz/pygraphviz": { + "stars": 834, + "owner": "pygraphviz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyinfra-dev/pyinfra": { + "stars": 4869, + "owner": "pyinfra-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyinstaller/pyinstaller": { + "stars": 12924, + "owner": "pyinstaller", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyinvoke/invoke": { + "stars": 4722, + "owner": "pyinvoke", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pylint-dev/pylint": { + "stars": 5661, + "owner": "pylint-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pymatting/pymatting": { + "stars": 1891, + "owner": "pymatting", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pymc-devs/pymc3": { + "stars": 9527, + "owner": "pymc-devs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pymssql/pymssql": { + "stars": 880, + "owner": "pymssql", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pynamodb/PynamoDB": { + "stars": 2647, + "owner": "pynamodb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pypa/bandersnatch": { + "stars": 528, + "owner": "pypa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pypa/hatch": { + "stars": 7145, + "owner": "pypa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pypa/virtualenv": { + "stars": 5017, + "owner": "pypa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pypa/warehouse": { + "stars": 3978, + "owner": "pypi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyparsing/pyparsing": { + "stars": 2465, + "owner": "pyparsing", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyqtgraph/pyqtgraph": { + "stars": 4311, + "owner": "pyqtgraph", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pyston/pyston": { + "stars": 2507, + "owner": "pyston", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-attrs/attrs": { + "stars": 5746, + "owner": "python-attrs", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-greenlet/greenlet": { + "stars": 1814, + "owner": "python-greenlet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-jsonschema/jsonschema": { + "stars": 4935, + "owner": "python-jsonschema", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-mode/python-mode": { + "stars": 5478, + "owner": "python-mode", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-openxml/python-docx": { + "stars": 5486, + "owner": "python-openxml", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-pillow/Pillow": { + "stars": 13437, + "owner": "python-pillow", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-rapidjson/python-rapidjson": { + "stars": 532, + "owner": "python-rapidjson", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-rope/rope": { + "stars": 2183, + "owner": "python-rope", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python-trio/trio": { + "stars": 7204, + "owner": "python-trio", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python/cpython": { + "stars": 72015, + "owner": "python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python/mypy": { + "stars": 20302, + "owner": "python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "python/typeshed": { + "stars": 5021, + "owner": "python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pythonnet/pythonnet": { + "stars": 5418, + "owner": "pythonnet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pytoolz/cytoolz": { + "stars": 1103, + "owner": "pytoolz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pytoolz/toolz": { + "stars": 5125, + "owner": "pytoolz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pytorch/pytorch": { + "stars": 98355, + "owner": "pytorch", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "pytransitions/transitions": { + "stars": 6459, + "owner": "pytransitions", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "quantopian/zipline": { + "stars": 19514, + "owner": "quantopian", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "quantumlib/Cirq": { + "stars": 4890, + "owner": "quantumlib", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "quodlibet/mutagen": { + "stars": 1867, + "owner": "quodlibet", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "r0x0r/pywebview": { + "stars": 5803, + "owner": "r0x0r", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ranaroussi/yfinance": { + "stars": 22172, + "owner": "ranaroussi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ray-project/ray": { + "stars": 41786, + "owner": "ray-project", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "redis/redis-py": { + "stars": 13505, + "owner": "redis", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "reflex-dev/reflex": { + "stars": 28234, + "owner": "reflex-dev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "robotframework/robotframework": { + "stars": 11478, + "owner": "robotframework", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ronaldoussoren/py2app": { + "stars": 421, + "owner": "ronaldoussoren", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "rq/rq": { + "stars": 10605, + "owner": "rq", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "rsalmei/alive-progress": { + "stars": 6256, + "owner": "rsalmei", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "run-llama/llama_index": { + "stars": 47740, + "owner": "run-llama", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "s3tools/s3cmd": { + "stars": 4869, + "owner": "s3tools", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "saffsd/langid.py": { + "stars": 2455, + "owner": "saffsd", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "saltstack/salt": { + "stars": 15281, + "owner": "saltstack", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "samuelcolvin/watchfiles": { + "stars": 2444, + "owner": "samuelcolvin", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sanic-org/sanic": { + "stars": 18639, + "owner": "sanic-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "scanny/python-pptx": { + "stars": 3233, + "owner": "scanny", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "schematics/schematics": { + "stars": 2591, + "owner": "schematics", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "scottrogowski/code2flow": { + "stars": 4545, + "owner": "scottrogowski", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "scrapy/scrapy": { + "stars": 60855, + "owner": "scrapy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sdispater/pendulum": { + "stars": 6628, + "owner": "python-pendulum", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sdispater/poetry": { + "stars": 34316, + "owner": "python-poetry", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sebastien/cuisine": { + "stars": 1270, + "owner": "sebastien", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "secdev/scapy": { + "stars": 12108, + "owner": "secdev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sehmaschine/django-grappelli": { + "stars": 3927, + "owner": "sehmaschine", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "selwin/python-user-agents": { + "stars": 1520, + "owner": "selwin", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sergree/matchering": { + "stars": 2446, + "owner": "sergree", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "shahraizali/awesome-django": { + "stars": 1901, + "owner": "shahraizali", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "shapely/shapely": { + "stars": 4395, + "owner": "shapely", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sherlock-project/sherlock": { + "stars": 73803, + "owner": "sherlock-project", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "simonw/datasette": { + "stars": 10835, + "owner": "simonw", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "simonw/sqlite-utils": { + "stars": 2019, + "owner": "simonw", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sirfz/tesserocr": { + "stars": 2160, + "owner": "sirfz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "skorokithakis/shortuuid": { + "stars": 2178, + "owner": "skorokithakis", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sloria/doitlive": { + "stars": 3566, + "owner": "sloria", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sphinx-doc/sphinx": { + "stars": 7721, + "owner": "sphinx-doc", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "spotify/annoy": { + "stars": 14181, + "owner": "spotify", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "spotify/luigi": { + "stars": 18694, + "owner": "spotify", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "spulec/freezegun": { + "stars": 4498, + "owner": "spulec", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "spyder-ide/spyder": { + "stars": 9163, + "owner": "spyder-ide", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sqlalchemy/dogpile.cache": { + "stars": 291, + "owner": "sqlalchemy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sqlmapproject/sqlmap": { + "stars": 36853, + "owner": "sqlmapproject", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "stanfordnlp/stanza": { + "stars": 7739, + "owner": "stanfordnlp", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "statsmodels/statsmodels": { + "stars": 11298, + "owner": "statsmodels", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "stchris/untangle": { + "stars": 631, + "owner": "stchris", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "strawberry-graphql/strawberry-django": { + "stars": 488, + "owner": "strawberry-graphql", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "streamlit/streamlit": { + "stars": 43924, + "owner": "streamlit", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sunainapai/makesite": { + "stars": 1872, + "owner": "sunainapai", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "sympy/sympy": { + "stars": 14495, + "owner": "sympy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tartley/colorama": { + "stars": 3770, + "owner": "tartley", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tayllan/awesome-algorithms": { + "stars": 24835, + "owner": "tayllan", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tensorflow/tensorflow": { + "stars": 194201, + "owner": "tensorflow", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "thauber/django-schedule": { + "stars": 850, + "owner": "thauber", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "thumbor/thumbor": { + "stars": 10465, + "owner": "thumbor", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tiangolo/fastapi": { + "stars": 96302, + "owner": "fastapi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tiangolo/typer": { + "stars": 19041, + "owner": "fastapi", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "timofurrer/awesome-asyncio": { + "stars": 5030, + "owner": "timofurrer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "timofurrer/try": { + "stars": 751, + "owner": "timofurrer", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "timothycrosley/isort": { + "stars": 6916, + "owner": "PyCQA", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tmux-python/tmuxp": { + "stars": 4451, + "owner": "tmux-python", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tmux/tmux": { + "stars": 43165, + "owner": "tmux", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tomerfiliba/rpyc": { + "stars": 1693, + "owner": "tomerfiliba-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tomschimansky/customtkinter": { + "stars": 13223, + "owner": "TomSchimansky", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tornadoweb/tornado": { + "stars": 22406, + "owner": "tornadoweb", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tqdm/tqdm": { + "stars": 31040, + "owner": "tqdm", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "trustedsec/social-engineer-toolkit": { + "stars": 14671, + "owner": "trustedsec", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "twisted/treq": { + "stars": 606, + "owner": "twisted", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "twisted/twisted": { + "stars": 5951, + "owner": "twisted", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "tyiannak/pyAudioAnalysis": { + "stars": 6234, + "owner": "tyiannak", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "typeddjango/awesome-python-typing": { + "stars": 1950, + "owner": "typeddjango", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ultraplot/UltraPlot": { + "stars": 279, + "owner": "Ultraplot", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "un33k/python-slugify": { + "stars": 1599, + "owner": "un33k", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "unclecode/crawl4ai": { + "stars": 62122, + "owner": "unclecode", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "unfoldadmin/django-unfold": { + "stars": 3369, + "owner": "unfoldadmin", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "uralbash/awesome-pyramid": { + "stars": 570, + "owner": "uralbash", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "urllib3/urllib3": { + "stars": 4012, + "owner": "urllib3", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "vadikko2/python-cqrs": { + "stars": 44, + "owner": "pypatterns", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "vinta/pangu.py": { + "stars": 276, + "owner": "vinta", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "vispy/vispy": { + "stars": 3558, + "owner": "vispy", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "vitali87/code-graph-rag": { + "stars": 2131, + "owner": "vitali87", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "vllm-project/vllm": { + "stars": 73457, + "owner": "vllm-project", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "wagtail/wagtail": { + "stars": 20240, + "owner": "wagtail", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "waylan/Python-Markdown": { + "stars": 4186, + "owner": "Python-Markdown", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "web2py/pydal": { + "stars": 531, + "owner": "web2py", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "wireservice/csvkit": { + "stars": 6360, + "owner": "wireservice", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "wooey/wooey": { + "stars": 2218, + "owner": "wooey", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "worldveil/dejavu": { + "stars": 6736, + "owner": "worldveil", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "xonsh/xonsh": { + "stars": 9252, + "owner": "xonsh", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "yfedoseev/pdf_oxide": { + "stars": 431, + "owner": "yfedoseev", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "yoloseem/awesome-sphinxdoc": { + "stars": 973, + "owner": "ygzgxyz", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ytdl-org/youtube-dl": { + "stars": 139894, + "owner": "ytdl-org", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "zappa/Zappa": { + "stars": 3676, + "owner": "zappa", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "zauberzeug/nicegui": { + "stars": 15518, + "owner": "zauberzeug", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "zoofIO/flexx": { + "stars": 3343, + "owner": "flexxui", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "zopefoundation/ZODB": { + "stars": 752, + "owner": "zopefoundation", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "ztane/python-Levenshtein": { + "stars": 1278, + "owner": "ztane", + "fetched_at": "2026-03-17T20:08:08.973003+00:00" + }, + "josephmisiti/awesome-machine-learning": { + "stars": 72017, + "owner": "josephmisiti", + "fetched_at": "2026-03-17T20:12:03.066921+00:00" + }, + "sorrycc/awesome-javascript": { + "stars": 34931, + "owner": "sorrycc", + "fetched_at": "2026-03-17T20:12:03.066921+00:00" + }, + "vinta/awesome-python": { + "stars": 287640, + "owner": "vinta", + "fetched_at": "2026-03-17T20:12:03.066921+00:00" + } +} diff --git a/website/fetch_github_stars.py b/website/fetch_github_stars.py new file mode 100644 index 00000000..4f9b50a3 --- /dev/null +++ b/website/fetch_github_stars.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Fetch GitHub star counts and owner info for all GitHub repos in README.md.""" + +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + +import httpx + +from build import extract_github_repo + +CACHE_MAX_AGE_DAYS = 7 +DATA_DIR = Path(__file__).parent / "data" +CACHE_FILE = DATA_DIR / "github_stars.json" +README_PATH = Path(__file__).parent.parent / "README.md" +GRAPHQL_URL = "https://api.github.com/graphql" +BATCH_SIZE = 100 + + +def extract_github_repos(text: str) -> set[str]: + """Extract unique owner/repo pairs from GitHub URLs in markdown text.""" + repos = set() + for url in re.findall(r"https?://github\.com/[^\s)\]]+", text): + repo = extract_github_repo(url.split("#")[0].rstrip("/")) + if repo: + repos.add(repo) + return repos + + +def load_cache() -> dict: + """Load the star cache from disk. Returns empty dict if missing or corrupt.""" + if CACHE_FILE.exists(): + try: + return json.loads(CACHE_FILE.read_text(encoding="utf-8")) + except json.JSONDecodeError: + print(f"Warning: corrupt cache at {CACHE_FILE}, starting fresh.", file=sys.stderr) + return {} + return {} + + +def save_cache(cache: dict) -> None: + """Write the star cache to disk, creating data/ dir if needed.""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + CACHE_FILE.write_text( + json.dumps(cache, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def build_graphql_query(repos: list[str]) -> str: + """Build a GraphQL query with aliases for up to 100 repos.""" + if not repos: + return "" + parts = [] + for i, repo in enumerate(repos): + owner, name = repo.split("/", 1) + if '"' in owner or '"' in name: + continue + parts.append( + f'repo_{i}: repository(owner: "{owner}", name: "{name}") ' + f"{{ stargazerCount pushedAt owner {{ login }} }}" + ) + if not parts: + return "" + return "query { " + " ".join(parts) + " }" + + +def parse_graphql_response( + data: dict, + repos: list[str], +) -> dict[str, dict]: + """Parse GraphQL response into {owner/repo: {stars, owner}} dict.""" + result = {} + for i, repo in enumerate(repos): + node = data.get(f"repo_{i}") + if node is None: + continue + result[repo] = { + "stars": node.get("stargazerCount", 0), + "owner": node.get("owner", {}).get("login", ""), + "pushed_at": node.get("pushedAt", ""), + } + return result + + +def fetch_batch( + repos: list[str], *, client: httpx.Client, +) -> dict[str, dict]: + """Fetch star data for a batch of repos via GitHub GraphQL API.""" + query = build_graphql_query(repos) + if not query: + return {} + resp = client.post(GRAPHQL_URL, json={"query": query}) + resp.raise_for_status() + result = resp.json() + if "errors" in result: + for err in result["errors"]: + print(f" Warning: {err.get('message', err)}", file=sys.stderr) + data = result.get("data", {}) + return parse_graphql_response(data, repos) + + +def main() -> None: + """Fetch GitHub stars for all repos in README.md, updating the JSON cache.""" + token = os.environ.get("GITHUB_TOKEN", "") + if not token: + print("Error: GITHUB_TOKEN environment variable is required.", file=sys.stderr) + sys.exit(1) + + readme_text = README_PATH.read_text(encoding="utf-8") + current_repos = extract_github_repos(readme_text) + print(f"Found {len(current_repos)} GitHub repos in README.md") + + cache = load_cache() + now = datetime.now(timezone.utc) + + # Prune entries not in current README + pruned = {k: v for k, v in cache.items() if k in current_repos} + if len(pruned) < len(cache): + print(f"Pruned {len(cache) - len(pruned)} stale cache entries") + cache = pruned + + # Determine which repos need fetching (missing or stale) + to_fetch = [] + for repo in sorted(current_repos): + entry = cache.get(repo) + if entry and "fetched_at" in entry: + fetched = datetime.fromisoformat(entry["fetched_at"]) + age_days = (now - fetched).days + if age_days < CACHE_MAX_AGE_DAYS: + continue + to_fetch.append(repo) + + print(f"{len(to_fetch)} repos to fetch ({len(current_repos) - len(to_fetch)} cached)") + + if not to_fetch: + save_cache(cache) + print("Cache is up to date.") + return + + # Fetch in batches + fetched_count = 0 + skipped_repos: list[str] = [] + + with httpx.Client( + headers={"Authorization": f"bearer {token}", "Content-Type": "application/json"}, + transport=httpx.HTTPTransport(retries=2), + timeout=30, + ) as client: + for i in range(0, len(to_fetch), BATCH_SIZE): + batch = to_fetch[i : i + BATCH_SIZE] + batch_num = i // BATCH_SIZE + 1 + total_batches = (len(to_fetch) + BATCH_SIZE - 1) // BATCH_SIZE + print(f"Fetching batch {batch_num}/{total_batches} ({len(batch)} repos)...") + + try: + results = fetch_batch(batch, client=client) + except httpx.HTTPStatusError as e: + print(f"HTTP error {e.response.status_code}", file=sys.stderr) + if e.response.status_code == 401: + print("Error: Invalid GITHUB_TOKEN.", file=sys.stderr) + sys.exit(1) + print("Saving partial cache and exiting.", file=sys.stderr) + save_cache(cache) + sys.exit(1) + + now_iso = now.isoformat() + for repo in batch: + if repo in results: + cache[repo] = { + "stars": results[repo]["stars"], + "owner": results[repo]["owner"], + "pushed_at": results[repo]["pushed_at"], + "fetched_at": now_iso, + } + fetched_count += 1 + else: + skipped_repos.append(repo) + + # Save after each batch in case of interruption + save_cache(cache) + + if skipped_repos: + print(f"Skipped {len(skipped_repos)} repos (deleted/private/renamed)") + print(f"Done. Fetched {fetched_count} repos, {len(cache)} total cached.") + + +if __name__ == "__main__": + main() diff --git a/website/static/main.js b/website/static/main.js new file mode 100644 index 00000000..16e18311 --- /dev/null +++ b/website/static/main.js @@ -0,0 +1,154 @@ +// State +var activeFilter = null; // { type: "cat"|"group", value: "..." } +var searchInput = document.querySelector('.search'); +var filterBar = document.querySelector('.filter-bar'); +var filterValue = document.querySelector('.filter-value'); +var filterClear = document.querySelector('.filter-clear'); +var noResults = document.querySelector('.no-results'); +var countEl = document.querySelector('.count'); +var rows = document.querySelectorAll('.table tbody tr.row'); +var tags = document.querySelectorAll('.tag'); +var tbody = document.querySelector('.table tbody'); + +function collapseAll() { + var openRows = document.querySelectorAll('.table tbody tr.row.open'); + openRows.forEach(function (row) { + row.classList.remove('open'); + row.setAttribute('aria-expanded', 'false'); + }); +} + +function applyFilters() { + var query = searchInput ? searchInput.value.toLowerCase().trim() : ''; + var visibleCount = 0; + + // Collapse all expanded rows on filter/search change + collapseAll(); + + rows.forEach(function (row) { + var show = true; + + // Category/group filter + if (activeFilter) { + show = row.dataset[activeFilter.type] === activeFilter.value; + } + + // Text search + if (show && query) { + if (!row._searchText) { + var text = row.textContent.toLowerCase(); + var next = row.nextElementSibling; + if (next && next.classList.contains('expand-row')) { + text += ' ' + next.textContent.toLowerCase(); + } + row._searchText = text; + } + show = row._searchText.includes(query); + } + + row.hidden = !show; + + if (show) { + visibleCount++; + row.querySelector('.col-num').textContent = String(visibleCount); + } + }); + + if (noResults) noResults.hidden = visibleCount > 0; + if (countEl) countEl.textContent = visibleCount; + + // Update tag highlights + tags.forEach(function (tag) { + var isActive = activeFilter + && tag.dataset.type === activeFilter.type + && tag.dataset.value === activeFilter.value; + tag.classList.toggle('active', isActive); + }); + + // Filter bar + if (filterBar) { + if (activeFilter) { + filterBar.hidden = false; + if (filterValue) filterValue.textContent = activeFilter.value; + } else { + filterBar.hidden = true; + } + } +} + +// Expand/collapse: event delegation on tbody +if (tbody) { + tbody.addEventListener('click', function (e) { + // Don't toggle if clicking a link or tag button + if (e.target.closest('a') || e.target.closest('.tag')) return; + + var row = e.target.closest('tr.row'); + if (!row) return; + + var isOpen = row.classList.contains('open'); + if (isOpen) { + row.classList.remove('open'); + row.setAttribute('aria-expanded', 'false'); + } else { + row.classList.add('open'); + row.setAttribute('aria-expanded', 'true'); + } + }); + + // Keyboard: Enter or Space on focused .row toggles expand + tbody.addEventListener('keydown', function (e) { + if (e.key !== 'Enter' && e.key !== ' ') return; + var row = e.target.closest('tr.row'); + if (!row) return; + e.preventDefault(); + row.click(); + }); +} + +// Tag click: filter by category or group +tags.forEach(function (tag) { + tag.addEventListener('click', function (e) { + e.preventDefault(); + var type = tag.dataset.type; + var value = tag.dataset.value; + + // Toggle: click same filter again to clear + if (activeFilter && activeFilter.type === type && activeFilter.value === value) { + activeFilter = null; + } else { + activeFilter = { type: type, value: value }; + } + applyFilters(); + }); +}); + +// Clear filter +if (filterClear) { + filterClear.addEventListener('click', function () { + activeFilter = null; + applyFilters(); + }); +} + +// Search input +if (searchInput) { + var searchTimer; + searchInput.addEventListener('input', function () { + clearTimeout(searchTimer); + searchTimer = setTimeout(applyFilters, 150); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', function (e) { + if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + searchInput.focus(); + } + if (e.key === 'Escape' && document.activeElement === searchInput) { + searchInput.value = ''; + activeFilter = null; + applyFilters(); + searchInput.blur(); + } + }); +} diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 00000000..c31b6875 --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,459 @@ +/* === Reset & Base === */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --font-display: Georgia, "Noto Serif", "Times New Roman", serif; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + + --text-xs: 0.9375rem; + --text-sm: 1rem; + --text-base: 1.125rem; + + --bg: oklch(99.5% 0.003 240); + --bg-hover: oklch(97% 0.008 240); + --text: oklch(15% 0.005 240); + --text-secondary: oklch(35% 0.005 240); + --text-muted: oklch(50% 0.005 240); + --border: oklch(90% 0.005 240); + --border-strong: oklch(75% 0.008 240); + --border-heavy: oklch(25% 0.01 240); + --bg-input: oklch(94.5% 0.035 240); + --accent: oklch(42% 0.14 240); + --accent-hover: oklch(32% 0.16 240); + --accent-light: oklch(97% 0.015 240); + --highlight: oklch(93% 0.10 90); + --highlight-text: oklch(35% 0.10 90); +} + +html { font-size: 16px; } + +body { + font-family: var(--font-body); + background: var(--bg); + color: var(--text); + line-height: 1.55; + min-height: 100vh; + display: flex; + flex-direction: column; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent-hover); text-decoration: underline; } + +/* === Skip Link === */ +.skip-link { + position: absolute; + left: -9999px; + top: 0; + padding: 0.5rem 1rem; + background: var(--text); + color: var(--bg); + font-size: var(--text-xs); + font-weight: 700; + z-index: 200; +} + +.skip-link:focus { left: 0; } + +/* === Hero === */ +.hero { + max-width: 1400px; + margin: 0 auto; + padding: 3.5rem 2rem 1.5rem; +} + +.hero-main { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.hero-submit { + flex-shrink: 0; + padding: 0.4rem 1rem; + border: 1px solid var(--border-strong); + border-radius: 4px; + font-size: var(--text-sm); + color: var(--text); + text-decoration: none; + white-space: nowrap; +} + +.hero-submit:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} + +.hero h1 { + font-family: var(--font-display); + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 400; + letter-spacing: -0.01em; + line-height: 1.1; + color: var(--accent); + margin-bottom: 0.75rem; +} + +.hero-sub { + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 0.5rem; +} + +.hero-sub a { color: var(--text-secondary); font-weight: 600; } +.hero-sub a:hover { color: var(--accent); } + +.hero-gh { + font-size: var(--text-sm); + color: var(--text-muted); + font-weight: 500; +} + +.hero-gh:hover { color: var(--accent); } + +/* === Controls === */ +.controls { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem 1rem; +} + +.search-wrap { + position: relative; + margin-bottom: 0.75rem; +} + +.search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; +} + +.search { + width: 100%; + padding: 0.65rem 1rem 0.65rem 2.75rem; + border: 1px solid transparent; + border-radius: 4px; + background: var(--bg-input); + font-family: var(--font-body); + font-size: var(--text-sm); + color: var(--text); +} + +.search::placeholder { color: var(--text-muted); } + +.search:focus { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-color: var(--accent); + background: var(--bg); +} + +.filter-bar[hidden] { display: none; } + +.filter-bar { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0; + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.filter-bar strong { + color: var(--text); +} + +.filter-clear { + background: none; + border: 1px solid var(--border); + border-radius: 4px; + padding: 0.15rem 0.5rem; + font-family: inherit; + font-size: var(--text-xs); + color: var(--text-muted); + cursor: pointer; +} + +.filter-clear:hover { + border-color: var(--text-muted); + color: var(--text); +} + +.stats { + font-size: var(--text-sm); + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +.stats strong { color: var(--text-secondary); } + +/* === Table === */ +.table-wrap { + width: 100%; + padding: 0; + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--text-sm); +} + +.table thead th { + text-align: left; + font-weight: 700; + font-size: var(--text-base); + color: var(--text); + padding: 0.65rem 0.75rem; + border-bottom: 2px solid var(--border-heavy); + position: sticky; + top: 0; + background: var(--bg); + z-index: 10; + white-space: nowrap; +} + +.table thead th:first-child, +.table tbody td:first-child { + padding-left: max(2rem, calc(50vw - 700px + 2rem)); +} + +.table thead th:last-child, +.table tbody td:last-child { + padding-right: max(2rem, calc(50vw - 700px + 2rem)); +} + +.table tbody td { + padding: 0.7rem 0.75rem; + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.table tbody tr.row:not(.open):hover td { + background: var(--bg-hover); +} + +.table tbody tr[hidden] { display: none; } + +.col-num { + width: 3rem; + color: var(--text-muted); + font-variant-numeric: tabular-nums; + text-align: right; +} + +.col-name { + width: 35%; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.col-name > a { + font-weight: 500; + color: var(--accent); + text-decoration: none; +} + +.col-name > a:hover { text-decoration: underline; color: var(--accent-hover); } + +/* === Stars Column === */ +.col-stars { + width: 5rem; + font-variant-numeric: tabular-nums; + white-space: nowrap; + color: var(--text-secondary); +} + +/* === Arrow Column === */ +.col-arrow { + width: 2.5rem; + text-align: center; +} + +.arrow { + display: inline-block; + font-size: 0.8rem; + color: var(--accent); + transition: transform 0.15s ease; +} + +.row.open .arrow { + transform: rotate(90deg); +} + +/* === Row Click === */ +.row { cursor: pointer; } + +/* === Expand Row === */ +.expand-row { + display: none; +} + +.row.open + .expand-row { + display: table-row; +} + +.row.open td { + background: var(--accent-light); + border-bottom-color: transparent; + padding-bottom: 0.1rem; +} + +.expand-row td { + padding: 0.15rem 0.75rem 0.75rem; + background: var(--accent-light); + border-bottom: 1px solid var(--border); +} + +.expand-content { + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: 1.6; +} + +.expand-also-see { + margin-top: 0.25rem; + font-size: var(--text-xs); + color: var(--text-muted); +} + +.expand-also-see a { + color: var(--accent); + text-decoration: none; +} + +.expand-also-see a:hover { + text-decoration: underline; +} + +.expand-meta { + margin-top: 0.25rem; + font-size: var(--text-xs); + color: var(--text-muted); + font-weight: normal; +} + +.expand-meta a { + color: var(--accent); + text-decoration: none; +} + +.expand-meta a:hover { + text-decoration: underline; +} + +.expand-sep { + margin: 0 0.25rem; + color: var(--border); +} + +.col-cat, .col-group { + width: 13%; + white-space: nowrap; +} + +/* === Tags === */ +.tag { + background: var(--accent-light); + border: none; + font-family: inherit; + font-size: var(--text-xs); + color: oklch(45% 0.06 240); + cursor: pointer; + padding: 0.15rem 0.35rem; + border-radius: 3px; + white-space: nowrap; +} + +.tag:hover { + background: var(--accent-light); + color: var(--accent); +} + +.tag.active { + background: var(--highlight); + color: var(--highlight-text); + font-weight: 600; +} + +/* === No Results === */ +.no-results { + max-width: 1400px; + margin: 0 auto; + padding: 3rem 2rem; + font-size: var(--text-base); + color: var(--text-muted); + text-align: center; +} + +/* === Footer === */ +.footer { + margin-top: auto; + border-top: none; + width: 100%; + padding: 1.25rem 2rem; + font-size: var(--text-xs); + color: var(--text-muted); + background: var(--bg-input); + display: flex; + align-items: center; + justify-content: space-between; +} + +.footer a { color: var(--text-muted); text-decoration: none; } +.footer a:hover { color: var(--accent); } + +.footer-links { + display: flex; + gap: 1rem; +} + +/* === Responsive === */ +@media (max-width: 900px) { + .col-group { display: none; } +} + +@media (max-width: 640px) { + .hero { padding: 2rem 1.25rem 1rem; } + .controls { padding: 0 1.25rem 0.75rem; } + + .table thead th:first-child, + .table tbody td:first-child { padding-left: 1.25rem; } + + .table thead th:last-child, + .table tbody td:last-child { padding-right: 1.25rem; } + + .col-cat { display: none; } + .col-name { white-space: normal; } + .footer { padding: 1.25rem; flex-direction: column; gap: 0.5rem; } +} + +/* === Screen Reader Only === */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* === Reduced Motion === */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + transition-duration: 0.01ms !important; + } +} diff --git a/website/templates/base.html b/website/templates/base.html new file mode 100644 index 00000000..4a3bc249 --- /dev/null +++ b/website/templates/base.html @@ -0,0 +1,67 @@ + + + + + + {% block title %}Awesome Python{% endblock %} + + + + + + + + + + + + + + + +
{% block content %}{% endblock %}
+ + + + + + + diff --git a/website/templates/index.html b/website/templates/index.html new file mode 100644 index 00000000..ea2c4821 --- /dev/null +++ b/website/templates/index.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} +{% block content %} +
+
+
+

Awesome Python

+

+ {{ subtitle }}
Curated by + @vinta + since 2014. +

+ awesome-python on GitHub → +
+ Submit a Project +
+
+ +
+
+ + + + + +
+ +
+ +
+ + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + + + + + {% endfor %} + +
#Project NameGitHub StarsCategoryGroup
+
+ {% if entry.description %} +
{{ entry.description | safe }}
+ {% endif %} {% if entry.also_see %} +
+ Also see: {% for see in entry.also_see %}{{ see.name }}{% if not loop.last %}, {% endif %}{% endfor %} +
+ {% endif %} +
+ {% if entry.owner %}{{ entry.owner }}/{% endif %}{{ entry.url | replace("https://", "") }}{% if entry.pushed_at %}·Last pushed {{ entry.pushed_at[:10] }}{% endif %} +
+
+
+
+ + +{% endblock %} diff --git a/website/tests/test_build.py b/website/tests/test_build.py new file mode 100644 index 00000000..e551f954 --- /dev/null +++ b/website/tests/test_build.py @@ -0,0 +1,642 @@ +"""Tests for the build module.""" + +import json +import os +import shutil +import sys +import textwrap +from pathlib import Path + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from build import ( + build, + count_entries, + extract_github_repo, + extract_preview, + group_categories, + load_stars, + parse_readme, + render_content_html, + slugify, + sort_entries, +) + +# --------------------------------------------------------------------------- +# slugify +# --------------------------------------------------------------------------- + + +class TestSlugify: + def test_simple(self): + assert slugify("Admin Panels") == "admin-panels" + + def test_uppercase_acronym(self): + assert slugify("RESTful API") == "restful-api" + + def test_all_caps(self): + assert slugify("CMS") == "cms" + + def test_hyphenated_input(self): + assert slugify("Command-line Tools") == "command-line-tools" + + def test_special_chars(self): + assert slugify("Editor Plugins and IDEs") == "editor-plugins-and-ides" + + def test_single_word(self): + assert slugify("Audio") == "audio" + + def test_extra_spaces(self): + assert slugify(" Date and Time ") == "date-and-time" + + +# --------------------------------------------------------------------------- +# count_entries +# --------------------------------------------------------------------------- + + +class TestCountEntries: + def test_counts_dash_entries(self): + assert count_entries("- [a](url) - Desc.\n- [b](url) - Desc.") == 2 + + def test_counts_star_entries(self): + assert count_entries("* [a](url) - Desc.") == 1 + + def test_ignores_non_entries(self): + assert count_entries("Some text\n- [a](url) - Desc.\nMore text") == 1 + + def test_counts_indented_entries(self): + assert count_entries(" - [a](url) - Desc.") == 1 + + def test_empty_content(self): + assert count_entries("") == 0 + + +# --------------------------------------------------------------------------- +# extract_preview +# --------------------------------------------------------------------------- + + +class TestExtractPreview: + def test_basic(self): + content = "* [alpha](url) - A.\n* [beta](url) - B.\n* [gamma](url) - C." + assert extract_preview(content) == "alpha, beta, gamma" + + def test_max_four(self): + content = "\n".join(f"* [lib{i}](url) - Desc." for i in range(10)) + assert extract_preview(content) == "lib0, lib1, lib2, lib3" + + def test_empty(self): + assert extract_preview("") == "" + + def test_skips_subcategory_labels(self): + content = "* Synchronous\n* [django](url) - Framework.\n* [flask](url) - Micro." + assert extract_preview(content) == "django, flask" + + +# --------------------------------------------------------------------------- +# render_content_html +# --------------------------------------------------------------------------- + + +class TestRenderContentHtml: + def test_basic_entry(self): + content = "* [django](https://example.com) - A web framework." + html = render_content_html(content) + assert 'href="https://example.com"' in html + assert "django" in html + assert "A web framework." in html + assert 'class="entry"' in html + + def test_subcategory_label(self): + content = "* Synchronous\n* [django](https://x.com) - Framework." + html = render_content_html(content) + assert 'class="subcat"' in html + assert "Synchronous" in html + + def test_sub_entry(self): + content = "* [django](https://x.com) - Framework.\n * [awesome-django](https://y.com)" + html = render_content_html(content) + assert 'class="entry-sub"' in html + assert "awesome-django" in html + + def test_link_only_entry(self): + content = "* [tool](https://x.com)" + html = render_content_html(content) + assert 'href="https://x.com"' in html + assert "tool" in html + + +# --------------------------------------------------------------------------- +# parse_readme +# --------------------------------------------------------------------------- + +MINIMAL_README = textwrap.dedent("""\ + # Awesome Python + + Some intro text. + + --- + + ## Alpha + + _Libraries for alpha stuff._ + + - [lib-a](https://example.com/a) - Does A. + - [lib-b](https://example.com/b) - Does B. + + ## Beta + + _Tools for beta._ + + - [lib-c](https://example.com/c) - Does C. + + # Resources + + Where to discover resources. + + ## Newsletters + + - [News One](https://example.com/n1) + - [News Two](https://example.com/n2) + + ## Podcasts + + - [Pod One](https://example.com/p1) + + # Contributing + + Please contribute! +""") + + +class TestParseReadme: + def test_category_count(self): + cats, resources = parse_readme(MINIMAL_README) + assert len(cats) == 2 + + def test_resource_count(self): + cats, resources = parse_readme(MINIMAL_README) + assert len(resources) == 2 + + def test_category_names(self): + cats, _ = parse_readme(MINIMAL_README) + assert cats[0]["name"] == "Alpha" + assert cats[1]["name"] == "Beta" + + def test_category_slugs(self): + cats, _ = parse_readme(MINIMAL_README) + assert cats[0]["slug"] == "alpha" + assert cats[1]["slug"] == "beta" + + def test_category_description(self): + cats, _ = parse_readme(MINIMAL_README) + assert cats[0]["description"] == "Libraries for alpha stuff." + assert cats[1]["description"] == "Tools for beta." + + def test_category_content_has_entries(self): + cats, _ = parse_readme(MINIMAL_README) + assert "lib-a" in cats[0]["content"] + assert "lib-b" in cats[0]["content"] + + def test_resources_names(self): + _, resources = parse_readme(MINIMAL_README) + assert resources[0]["name"] == "Newsletters" + assert resources[1]["name"] == "Podcasts" + + def test_resources_content(self): + _, resources = parse_readme(MINIMAL_README) + assert "News One" in resources[0]["content"] + assert "Pod One" in resources[1]["content"] + + def test_contributing_skipped(self): + cats, resources = parse_readme(MINIMAL_README) + all_names = [c["name"] for c in cats] + [r["name"] for r in resources] + assert "Contributing" not in all_names + + def test_no_separator(self): + cats, resources = parse_readme("# Just a heading\n\nSome text.\n") + assert cats == [] + assert resources == [] + + def test_no_description(self): + readme = textwrap.dedent("""\ + # Title + + --- + + ## NullDesc + + - [item](https://x.com) - Thing. + + # Resources + + ## Tips + + - [tip](https://x.com) + + # Contributing + + Done. + """) + cats, resources = parse_readme(readme) + assert cats[0]["description"] == "" + assert "item" in cats[0]["content"] + + +# --------------------------------------------------------------------------- +# parse_readme on real README +# --------------------------------------------------------------------------- + + +class TestParseRealReadme: + @pytest.fixture(autouse=True) + def load_readme(self): + readme_path = os.path.join(os.path.dirname(__file__), "..", "..", "README.md") + with open(readme_path, encoding="utf-8") as f: + self.readme_text = f.read() + self.cats, self.resources = parse_readme(self.readme_text) + + def test_at_least_83_categories(self): + assert len(self.cats) >= 83 + + def test_resources_has_newsletters_and_podcasts(self): + names = [r["name"] for r in self.resources] + assert "Newsletters" in names + assert "Podcasts" in names + + def test_contributing_not_in_results(self): + all_names = [c["name"] for c in self.cats] + [ + r["name"] for r in self.resources + ] + assert "Contributing" not in all_names + + def test_first_category_is_admin_panels(self): + assert self.cats[0]["name"] == "Admin Panels" + assert self.cats[0]["slug"] == "admin-panels" + + def test_last_category_is_wsgi_servers(self): + assert self.cats[-1]["name"] == "WSGI Servers" + assert self.cats[-1]["slug"] == "wsgi-servers" + + def test_restful_api_slug(self): + slugs = [c["slug"] for c in self.cats] + assert "restful-api" in slugs + + def test_descriptions_extracted(self): + admin = self.cats[0] + assert admin["description"] == "Libraries for administrative interfaces." + + +# --------------------------------------------------------------------------- +# group_categories +# --------------------------------------------------------------------------- + + +class TestGroupCategories: + def test_groups_known_categories(self): + cats = [ + {"name": "Web Frameworks", "slug": "web-frameworks"}, + {"name": "Testing", "slug": "testing"}, + ] + groups = group_categories(cats, []) + group_names = [g["name"] for g in groups] + assert "Web & API" in group_names + assert "Development Tools" in group_names + + def test_ungrouped_go_to_other(self): + cats = [{"name": "Unknown Category", "slug": "unknown-category"}] + groups = group_categories(cats, []) + group_names = [g["name"] for g in groups] + assert "Other" in group_names + + def test_resources_grouped(self): + resources = [{"name": "Newsletters", "slug": "newsletters"}] + groups = group_categories([], resources) + group_names = [g["name"] for g in groups] + assert "Resources" in group_names + + +# --------------------------------------------------------------------------- +# render_markdown (kept for compatibility) +# --------------------------------------------------------------------------- + + +class TestRenderMarkdown: + def test_renders_link_list(self): + from build import render_markdown + + html = render_markdown("- [lib](https://example.com) - Does stuff.") + assert "
  • " in html + assert 'lib' in html + + def test_renders_plain_text(self): + from build import render_markdown + + html = render_markdown("Hello world") + assert "

    Hello world

    " in html + + +# --------------------------------------------------------------------------- +# build (integration) +# --------------------------------------------------------------------------- + + +class TestBuild: + def _make_repo(self, tmp_path, readme): + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + tpl_dir = tmp_path / "website" / "templates" + tpl_dir.mkdir(parents=True) + (tpl_dir / "base.html").write_text( + "{% block title %}{% endblock %}" + "" + "{% block content %}{% endblock %}", + encoding="utf-8", + ) + (tpl_dir / "index.html").write_text( + '{% extends "base.html" %}{% block content %}' + "{% for group in groups %}" + '
    ' + "

    {{ group.name }}

    " + "{% for cat in group.categories %}" + '
    ' + "{{ cat.name }}" + "{{ cat.preview }}" + "{{ cat.entry_count }}" + '' + "
    " + "{% endfor %}" + "
    " + "{% endfor %}" + "{% endblock %}", + encoding="utf-8", + ) + + def test_build_creates_single_page(self, tmp_path): + readme = textwrap.dedent("""\ + # Awesome Python + + Intro. + + --- + + ## Widgets + + _Widget libraries._ + + - [w1](https://example.com) - A widget. + + ## Gadgets + + _Gadget tools._ + + - [g1](https://example.com) - A gadget. + + # Resources + + Info. + + ## Newsletters + + - [NL](https://example.com) + + # Contributing + + Help! + """) + self._make_repo(tmp_path, readme) + build(str(tmp_path)) + + site = tmp_path / "website" / "output" + assert (site / "index.html").exists() + # No category sub-pages + assert not (site / "categories").exists() + + def test_build_creates_cname(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + --- + + ## Only + + - [x](https://x.com) - X. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + build(str(tmp_path)) + + cname = tmp_path / "website" / "output" / "CNAME" + assert cname.exists() + assert "awesome-python.com" in cname.read_text() + + def test_build_cleans_stale_output(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + --- + + ## Only + + - [x](https://x.com) - X. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + + stale = tmp_path / "website" / "output" / "categories" / "stale" + stale.mkdir(parents=True) + (stale / "index.html").write_text("old", encoding="utf-8") + + build(str(tmp_path)) + + assert not (tmp_path / "website" / "output" / "categories" / "stale").exists() + + def test_index_contains_category_names(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + --- + + ## Alpha + + - [a](https://x.com) - A. + + ## Beta + + - [b](https://x.com) - B. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + build(str(tmp_path)) + + index_html = (tmp_path / "website" / "output" / "index.html").read_text() + assert "Alpha" in index_html + assert "Beta" in index_html + + def test_index_contains_preview_text(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + --- + + ## Stuff + + - [django](https://x.com) - A framework. + - [flask](https://x.com) - A micro. + + # Contributing + + Done. + """) + self._make_repo(tmp_path, readme) + build(str(tmp_path)) + + index_html = (tmp_path / "website" / "output" / "index.html").read_text() + assert "django" in index_html + assert "flask" in index_html + + def test_build_with_stars_sorts_by_stars(self, tmp_path): + readme = textwrap.dedent("""\ + # T + + --- + + ## Stuff + + - [low-stars](https://github.com/org/low) - Low. + - [high-stars](https://github.com/org/high) - High. + - [no-stars](https://example.com/none) - None. + + # Contributing + + Done. + """) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") + + # Copy real templates + real_tpl = Path(__file__).parent / ".." / "templates" + tpl_dir = tmp_path / "website" / "templates" + shutil.copytree(real_tpl, tpl_dir) + + # Create mock star data + data_dir = tmp_path / "website" / "data" + data_dir.mkdir(parents=True) + stars = { + "org/high": {"stars": 5000, "owner": "org", "fetched_at": "2026-01-01T00:00:00+00:00"}, + "org/low": {"stars": 100, "owner": "org", "fetched_at": "2026-01-01T00:00:00+00:00"}, + } + (data_dir / "github_stars.json").write_text(json.dumps(stars), encoding="utf-8") + + build(str(tmp_path)) + + html = (tmp_path / "website" / "output" / "index.html").read_text(encoding="utf-8") + # Star-sorted: high-stars (5000) before low-stars (100) before no-stars (None) + assert html.index("high-stars") < html.index("low-stars") + assert html.index("low-stars") < html.index("no-stars") + # Formatted star counts + assert "5,000" in html + assert "100" in html + # Expand content present + assert "expand-content" in html + + +# --------------------------------------------------------------------------- +# extract_github_repo +# --------------------------------------------------------------------------- + + +class TestExtractGithubRepo: + def test_github_url(self): + assert extract_github_repo("https://github.com/psf/requests") == "psf/requests" + + def test_non_github_url(self): + assert extract_github_repo("https://foss.heptapod.net/pypy/pypy") is None + + def test_github_io_url(self): + assert extract_github_repo("https://user.github.io/proj") is None + + def test_trailing_slash(self): + assert extract_github_repo("https://github.com/org/repo/") == "org/repo" + + def test_deep_path(self): + assert extract_github_repo("https://github.com/org/repo/tree/main") is None + + def test_dot_git_suffix(self): + assert extract_github_repo("https://github.com/org/repo.git") == "org/repo" + + def test_org_only(self): + assert extract_github_repo("https://github.com/org") is None + + +# --------------------------------------------------------------------------- +# load_stars +# --------------------------------------------------------------------------- + + +class TestLoadStars: + def test_returns_empty_when_missing(self, tmp_path): + result = load_stars(tmp_path / "nonexistent.json") + assert result == {} + + def test_loads_valid_json(self, tmp_path): + data = {"psf/requests": {"stars": 52467, "owner": "psf", "fetched_at": "2026-01-01T00:00:00+00:00"}} + f = tmp_path / "stars.json" + f.write_text(json.dumps(data), encoding="utf-8") + result = load_stars(f) + assert result["psf/requests"]["stars"] == 52467 + + def test_returns_empty_on_corrupt_json(self, tmp_path): + f = tmp_path / "stars.json" + f.write_text("not json", encoding="utf-8") + result = load_stars(f) + assert result == {} + + +# --------------------------------------------------------------------------- +# sort_entries +# --------------------------------------------------------------------------- + + +class TestSortEntries: + def test_sorts_by_stars_descending(self): + entries = [ + {"name": "a", "stars": 100, "url": ""}, + {"name": "b", "stars": 500, "url": ""}, + {"name": "c", "stars": 200, "url": ""}, + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["b", "c", "a"] + + def test_equal_stars_sorted_alphabetically(self): + entries = [ + {"name": "beta", "stars": 100, "url": ""}, + {"name": "alpha", "stars": 100, "url": ""}, + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["alpha", "beta"] + + def test_no_stars_go_to_bottom(self): + entries = [ + {"name": "no-stars", "stars": None, "url": ""}, + {"name": "has-stars", "stars": 50, "url": ""}, + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["has-stars", "no-stars"] + + def test_no_stars_sorted_alphabetically(self): + entries = [ + {"name": "zebra", "stars": None, "url": ""}, + {"name": "apple", "stars": None, "url": ""}, + ] + result = sort_entries(entries) + assert [e["name"] for e in result] == ["apple", "zebra"] diff --git a/website/tests/test_fetch_github_stars.py b/website/tests/test_fetch_github_stars.py new file mode 100644 index 00000000..2465899f --- /dev/null +++ b/website/tests/test_fetch_github_stars.py @@ -0,0 +1,161 @@ +"""Tests for fetch_github_stars module.""" + +import json +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from fetch_github_stars import ( + build_graphql_query, + extract_github_repos, + load_cache, + parse_graphql_response, + save_cache, +) + + +class TestExtractGithubRepos: + def test_extracts_owner_repo_from_github_url(self): + readme = "* [requests](https://github.com/psf/requests) - HTTP lib." + result = extract_github_repos(readme) + assert result == {"psf/requests"} + + def test_multiple_repos(self): + readme = ( + "* [requests](https://github.com/psf/requests) - HTTP.\n" + "* [flask](https://github.com/pallets/flask) - Micro." + ) + result = extract_github_repos(readme) + assert result == {"psf/requests", "pallets/flask"} + + def test_ignores_non_github_urls(self): + readme = "* [pypy](https://foss.heptapod.net/pypy/pypy) - Fast Python." + result = extract_github_repos(readme) + assert result == set() + + def test_ignores_github_io_urls(self): + readme = "* [docs](https://user.github.io/project) - Docs site." + result = extract_github_repos(readme) + assert result == set() + + def test_ignores_github_wiki_and_blob_urls(self): + readme = ( + "* [wiki](https://github.com/org/repo/wiki) - Wiki.\n" + "* [file](https://github.com/org/repo/blob/main/f.py) - File." + ) + result = extract_github_repos(readme) + assert result == set() + + def test_handles_trailing_slash(self): + readme = "* [lib](https://github.com/org/repo/) - Lib." + result = extract_github_repos(readme) + assert result == {"org/repo"} + + def test_deduplicates(self): + readme = ( + "* [a](https://github.com/org/repo) - A.\n" + "* [b](https://github.com/org/repo) - B." + ) + result = extract_github_repos(readme) + assert result == {"org/repo"} + + def test_strips_fragment(self): + readme = "* [lib](https://github.com/org/repo#section) - Lib." + result = extract_github_repos(readme) + assert result == {"org/repo"} + + +class TestLoadCache: + def test_returns_empty_when_missing(self, tmp_path, monkeypatch): + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", tmp_path / "nonexistent.json") + result = load_cache() + assert result == {} + + def test_loads_valid_cache(self, tmp_path, monkeypatch): + cache_file = tmp_path / "stars.json" + cache_file.write_text('{"a/b": {"stars": 1}}', encoding="utf-8") + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + result = load_cache() + assert result == {"a/b": {"stars": 1}} + + def test_returns_empty_on_corrupt_json(self, tmp_path, monkeypatch): + cache_file = tmp_path / "stars.json" + cache_file.write_text("not json", encoding="utf-8") + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + result = load_cache() + assert result == {} + + +class TestSaveCache: + def test_creates_directory_and_writes_json(self, tmp_path, monkeypatch): + data_dir = tmp_path / "data" + cache_file = data_dir / "stars.json" + monkeypatch.setattr("fetch_github_stars.DATA_DIR", data_dir) + monkeypatch.setattr("fetch_github_stars.CACHE_FILE", cache_file) + save_cache({"a/b": {"stars": 1}}) + assert cache_file.exists() + assert json.loads(cache_file.read_text(encoding="utf-8")) == {"a/b": {"stars": 1}} + + +class TestBuildGraphqlQuery: + def test_single_repo(self): + query = build_graphql_query(["psf/requests"]) + assert "repository" in query + assert 'owner: "psf"' in query + assert 'name: "requests"' in query + assert "stargazerCount" in query + + def test_multiple_repos_use_aliases(self): + query = build_graphql_query(["psf/requests", "pallets/flask"]) + assert "repo_0:" in query + assert "repo_1:" in query + + def test_empty_list(self): + query = build_graphql_query([]) + assert query == "" + + def test_skips_repos_with_quotes_in_name(self): + query = build_graphql_query(['org/"bad"']) + assert query == "" + + def test_skips_only_bad_repos(self): + query = build_graphql_query(["good/repo", 'bad/"repo"']) + assert "good" in query + assert "bad" not in query + + +class TestParseGraphqlResponse: + def test_parses_star_count_and_owner(self): + data = { + "repo_0": { + "stargazerCount": 52467, + "owner": {"login": "psf"}, + } + } + repos = ["psf/requests"] + result = parse_graphql_response(data, repos) + assert result["psf/requests"]["stars"] == 52467 + assert result["psf/requests"]["owner"] == "psf" + + def test_skips_null_repos(self): + data = {"repo_0": None} + repos = ["deleted/repo"] + result = parse_graphql_response(data, repos) + assert result == {} + + def test_handles_missing_owner(self): + data = {"repo_0": {"stargazerCount": 100}} + repos = ["org/repo"] + result = parse_graphql_response(data, repos) + assert result["org/repo"]["owner"] == "" + + def test_multiple_repos(self): + data = { + "repo_0": {"stargazerCount": 100, "owner": {"login": "a"}}, + "repo_1": {"stargazerCount": 200, "owner": {"login": "b"}}, + } + repos = ["a/x", "b/y"] + result = parse_graphql_response(data, repos) + assert len(result) == 2 + assert result["a/x"]["stars"] == 100 + assert result["b/y"]["stars"] == 200