mirror of
https://github.com/vinta/awesome-python.git
synced 2026-11-07 02:38:50 +08:00
Merge pull request #2971 from vinta/feature/markdown-it-py-parser
feat: replace regex README parser with markdown-it-py AST parser
This commit is contained in:
15 files changed
+2529
-1443
No files matched your search
@@ -1,17 +1,24 @@
|
||||
-include .env
|
||||
export
|
||||
|
||||
site_install:
|
||||
uv sync --no-dev
|
||||
install:
|
||||
uv sync
|
||||
|
||||
site_fetch_stats:
|
||||
fetch_stats:
|
||||
uv run python website/fetch_github_stars.py
|
||||
|
||||
site_build:
|
||||
test:
|
||||
uv run pytest website/tests/ -v
|
||||
|
||||
build:
|
||||
uv run python website/build.py
|
||||
|
||||
site_preview: site_build
|
||||
python -m http.server -d website/output/ 8000
|
||||
|
||||
site_deploy: site_build
|
||||
@echo "Deploy via GitHub Actions (push to master)"
|
||||
preview: build
|
||||
@echo "Check the website on http://localhost:8000"
|
||||
uv run watchmedo shell-command \
|
||||
--patterns='*.md;*.html;*.css;*.js;*.py' \
|
||||
--recursive \
|
||||
--wait --drop \
|
||||
--command='uv run python website/build.py' \
|
||||
README.md website/templates website/static website/data & \
|
||||
python -m http.server -b 127.0.0.1 -d website/output/ 8000
|
||||
+17
-9
@@ -2,22 +2,30 @@
|
||||
name = "awesome-python"
|
||||
version = "0.1.0"
|
||||
description = "An opinionated list of awesome Python frameworks, libraries, software and resources."
|
||||
authors = [{ name = "Vinta Chen", email = "vinta.chen@gmail.com" }]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"httpx==0.28.1",
|
||||
"jinja2==3.1.6",
|
||||
"markdown==3.10.2",
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://awesome-python.com/"
|
||||
Repository = "https://github.com/vinta/awesome-python"
|
||||
|
||||
[dependency-groups]
|
||||
build = ["httpx==0.28.1", "jinja2==3.1.6", "markdown-it-py==4.0.0"]
|
||||
lint = ["ruff==0.15.6"]
|
||||
test = ["pytest==9.0.2"]
|
||||
dev = [
|
||||
"pytest==9.0.2",
|
||||
"ruff==0.15.6",
|
||||
{ include-group = "build" },
|
||||
{ include-group = "lint" },
|
||||
{ include-group = "test" },
|
||||
"watchdog==6.0.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["website/tests"]
|
||||
pythonpath = ["website"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
line-length = 100
|
||||
line-length = 200
|
||||
@@ -18,30 +18,46 @@ wheels = [
|
||||
name = "awesome-python"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
build = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "httpx" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
{ name = "watchdog" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[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]
|
||||
build = [
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "jinja2", specifier = "==3.1.6" },
|
||||
{ name = "markdown-it-py", specifier = "==4.0.0" },
|
||||
]
|
||||
dev = [
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "jinja2", specifier = "==3.1.6" },
|
||||
{ name = "markdown-it-py", specifier = "==4.0.0" },
|
||||
{ name = "pytest", specifier = "==9.0.2" },
|
||||
{ name = "ruff", specifier = "==0.15.6" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = "==0.15.6" }]
|
||||
test = [{ name = "pytest", specifier = "==9.0.2" }]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
@@ -129,12 +145,15 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.10.2"
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
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" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -189,6 +208,15 @@ wheels = [
|
||||
{ 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 = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
@@ -256,3 +284,24 @@ wheels = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
|
||||
]
|
||||
+25
-277
File diff suppressed because it is too large.
Load diff
+1274
-750
File diff suppressed because it is too large.
Load diff
@@ -10,14 +10,14 @@ from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from build import extract_github_repo
|
||||
from build import extract_github_repo, load_stars
|
||||
|
||||
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
|
||||
BATCH_SIZE = 50
|
||||
|
||||
|
||||
def extract_github_repos(text: str) -> set[str]:
|
||||
@@ -30,17 +30,6 @@ def extract_github_repos(text: str) -> set[str]:
|
||||
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)
|
||||
@@ -61,7 +50,7 @@ def build_graphql_query(repos: list[str]) -> str:
|
||||
continue
|
||||
parts.append(
|
||||
f'repo_{i}: repository(owner: "{owner}", name: "{name}") '
|
||||
f"{{ stargazerCount pushedAt owner {{ login }} }}"
|
||||
f"{{ stargazerCount owner {{ login }} defaultBranchRef {{ target {{ ... on Commit {{ committedDate }} }} }} }}"
|
||||
)
|
||||
if not parts:
|
||||
return ""
|
||||
@@ -78,10 +67,12 @@ def parse_graphql_response(
|
||||
node = data.get(f"repo_{i}")
|
||||
if node is None:
|
||||
continue
|
||||
default_branch = node.get("defaultBranchRef") or {}
|
||||
target = default_branch.get("target") or {}
|
||||
result[repo] = {
|
||||
"stars": node.get("stargazerCount", 0),
|
||||
"owner": node.get("owner", {}).get("login", ""),
|
||||
"pushed_at": node.get("pushedAt", ""),
|
||||
"last_commit_at": target.get("committedDate", ""),
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -114,7 +105,7 @@ def main() -> None:
|
||||
current_repos = extract_github_repos(readme_text)
|
||||
print(f"Found {len(current_repos)} GitHub repos in README.md")
|
||||
|
||||
cache = load_cache()
|
||||
cache = load_stars(CACHE_FILE)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Prune entries not in current README
|
||||
@@ -173,7 +164,7 @@ def main() -> None:
|
||||
cache[repo] = {
|
||||
"stars": results[repo]["stars"],
|
||||
"owner": results[repo]["owner"],
|
||||
"pushed_at": results[repo]["pushed_at"],
|
||||
"last_commit_at": results[repo]["last_commit_at"],
|
||||
"fetched_at": now_iso,
|
||||
}
|
||||
fetched_count += 1
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path d="M8 2h16a6 6 0 0 1 6 6v8H2V8a6 6 0 0 1 6-6z" fill="#1d5fa6"/>
|
||||
<path d="M2 16h28v8a6 6 0 0 1-6 6H8a6 6 0 0 1-6-6z" fill="#f0c73e"/>
|
||||
<circle cx="11.5" cy="9.5" r="2.2" fill="#f0c73e"/>
|
||||
<circle cx="20.5" cy="22.5" r="2.2" fill="#1d5fa6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 320 B |
+142
-4
@@ -1,15 +1,44 @@
|
||||
// State
|
||||
var activeFilter = null; // { type: "cat"|"group", value: "..." }
|
||||
var activeSort = { col: 'stars', order: 'desc' };
|
||||
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');
|
||||
|
||||
// Relative time formatting
|
||||
function relativeTime(isoStr) {
|
||||
var date = new Date(isoStr);
|
||||
var now = new Date();
|
||||
var diffMs = now - date;
|
||||
var diffHours = Math.floor(diffMs / 3600000);
|
||||
var diffDays = Math.floor(diffMs / 86400000);
|
||||
if (diffHours < 1) return 'just now';
|
||||
if (diffHours < 24) return diffHours === 1 ? '1 hour ago' : diffHours + ' hours ago';
|
||||
if (diffDays === 1) return 'yesterday';
|
||||
if (diffDays < 30) return diffDays + ' days ago';
|
||||
var diffMonths = Math.floor(diffDays / 30);
|
||||
if (diffMonths < 12) return diffMonths === 1 ? '1 month ago' : diffMonths + ' months ago';
|
||||
var diffYears = Math.floor(diffDays / 365);
|
||||
return diffYears === 1 ? '1 year ago' : diffYears + ' years ago';
|
||||
}
|
||||
|
||||
// Format all commit date cells
|
||||
document.querySelectorAll('.col-commit[data-commit]').forEach(function (td) {
|
||||
var time = td.querySelector('time');
|
||||
if (time) time.textContent = relativeTime(td.dataset.commit);
|
||||
});
|
||||
|
||||
// Store original row order for sort reset
|
||||
rows.forEach(function (row, i) {
|
||||
row._origIndex = i;
|
||||
row._expandRow = row.nextElementSibling;
|
||||
});
|
||||
|
||||
function collapseAll() {
|
||||
var openRows = document.querySelectorAll('.table tbody tr.row.open');
|
||||
openRows.forEach(function (row) {
|
||||
@@ -46,16 +75,18 @@ function applyFilters() {
|
||||
show = row._searchText.includes(query);
|
||||
}
|
||||
|
||||
row.hidden = !show;
|
||||
if (row.hidden !== !show) row.hidden = !show;
|
||||
|
||||
if (show) {
|
||||
visibleCount++;
|
||||
row.querySelector('.col-num').textContent = String(visibleCount);
|
||||
var numCell = row.cells[0];
|
||||
if (numCell.textContent !== String(visibleCount)) {
|
||||
numCell.textContent = String(visibleCount);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (noResults) noResults.hidden = visibleCount > 0;
|
||||
if (countEl) countEl.textContent = visibleCount;
|
||||
|
||||
// Update tag highlights
|
||||
tags.forEach(function (tag) {
|
||||
@@ -74,6 +105,76 @@ function applyFilters() {
|
||||
filterBar.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateURL();
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
var params = new URLSearchParams();
|
||||
var query = searchInput ? searchInput.value.trim() : '';
|
||||
if (query) params.set('q', query);
|
||||
if (activeFilter) {
|
||||
params.set(activeFilter.type === 'cat' ? 'category' : 'group', activeFilter.value);
|
||||
}
|
||||
if (activeSort.col !== 'stars' || activeSort.order !== 'desc') {
|
||||
params.set('sort', activeSort.col);
|
||||
params.set('order', activeSort.order);
|
||||
}
|
||||
var qs = params.toString();
|
||||
history.replaceState(null, '', qs ? '?' + qs : location.pathname);
|
||||
}
|
||||
|
||||
function getSortValue(row, col) {
|
||||
if (col === 'name') {
|
||||
return row.querySelector('.col-name a').textContent.trim().toLowerCase();
|
||||
}
|
||||
if (col === 'stars') {
|
||||
var text = row.querySelector('.col-stars').textContent.trim().replace(/,/g, '');
|
||||
var num = parseInt(text, 10);
|
||||
return isNaN(num) ? -1 : num;
|
||||
}
|
||||
if (col === 'commit-time') {
|
||||
var attr = row.querySelector('.col-commit').getAttribute('data-commit');
|
||||
return attr ? new Date(attr).getTime() : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortRows() {
|
||||
var arr = Array.prototype.slice.call(rows);
|
||||
if (activeSort) {
|
||||
arr.sort(function (a, b) {
|
||||
var aVal = getSortValue(a, activeSort.col);
|
||||
var bVal = getSortValue(b, activeSort.col);
|
||||
if (activeSort.col === 'name') {
|
||||
var cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
if (cmp === 0) return a._origIndex - b._origIndex;
|
||||
return activeSort.order === 'desc' ? -cmp : cmp;
|
||||
}
|
||||
if (aVal <= 0 && bVal <= 0) return a._origIndex - b._origIndex;
|
||||
if (aVal <= 0) return 1;
|
||||
if (bVal <= 0) return -1;
|
||||
var cmp = aVal - bVal;
|
||||
if (cmp === 0) return a._origIndex - b._origIndex;
|
||||
return activeSort.order === 'desc' ? -cmp : cmp;
|
||||
});
|
||||
} else {
|
||||
arr.sort(function (a, b) { return a._origIndex - b._origIndex; });
|
||||
}
|
||||
arr.forEach(function (row) {
|
||||
tbody.appendChild(row);
|
||||
tbody.appendChild(row._expandRow);
|
||||
});
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
document.querySelectorAll('th[data-sort]').forEach(function (th) {
|
||||
th.classList.remove('sort-asc', 'sort-desc');
|
||||
if (activeSort && th.dataset.sort === activeSort.col) {
|
||||
th.classList.add('sort-' + activeSort.order);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expand/collapse: event delegation on tbody
|
||||
@@ -130,6 +231,23 @@ if (filterClear) {
|
||||
});
|
||||
}
|
||||
|
||||
// Column sorting
|
||||
document.querySelectorAll('th[data-sort]').forEach(function (th) {
|
||||
th.addEventListener('click', function () {
|
||||
var col = th.dataset.sort;
|
||||
var defaultOrder = col === 'name' ? 'asc' : 'desc';
|
||||
var altOrder = defaultOrder === 'asc' ? 'desc' : 'asc';
|
||||
if (activeSort && activeSort.col === col) {
|
||||
if (activeSort.order === defaultOrder) activeSort = { col: col, order: altOrder };
|
||||
else activeSort = { col: 'stars', order: 'desc' };
|
||||
} else {
|
||||
activeSort = { col: col, order: defaultOrder };
|
||||
}
|
||||
sortRows();
|
||||
updateSortIndicators();
|
||||
});
|
||||
});
|
||||
|
||||
// Search input
|
||||
if (searchInput) {
|
||||
var searchTimer;
|
||||
@@ -152,3 +270,23 @@ if (searchInput) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore state from URL
|
||||
(function () {
|
||||
var params = new URLSearchParams(location.search);
|
||||
var q = params.get('q');
|
||||
var cat = params.get('category');
|
||||
var group = params.get('group');
|
||||
var sort = params.get('sort');
|
||||
var order = params.get('order');
|
||||
if (q && searchInput) searchInput.value = q;
|
||||
if (cat) activeFilter = { type: 'cat', value: cat };
|
||||
else if (group) activeFilter = { type: 'group', value: group };
|
||||
if ((sort === 'name' || sort === 'stars' || sort === 'commit-time') && (order === 'desc' || order === 'asc')) {
|
||||
activeSort = { col: sort, order: order };
|
||||
}
|
||||
if (q || cat || group || sort) {
|
||||
sortRows();
|
||||
}
|
||||
updateSortIndicators();
|
||||
})();
|
||||
+132
-23
File diff suppressed because it is too large.
Load diff
+12
-15
@@ -17,10 +17,7 @@
|
||||
/>
|
||||
<meta property="og:url" content="https://awesome-python.com/" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐍</text></svg>"
|
||||
/>
|
||||
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<script
|
||||
async
|
||||
@@ -41,24 +38,24 @@
|
||||
<main id="content">{% block content %}{% endblock %}</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/vinta" target="_blank" rel="noopener"
|
||||
>GitHub</a
|
||||
>
|
||||
<a href="https://twitter.com/vinta" target="_blank" rel="noopener"
|
||||
>Twitter</a
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
>Curated by
|
||||
<a href="https://github.com/vinta" target="_blank" rel="noopener"
|
||||
>Made by
|
||||
<a href="https://vinta.ws/" target="_blank" rel="noopener"
|
||||
>Vinta</a
|
||||
></span
|
||||
>
|
||||
<span class="footer-sep">/</span>
|
||||
<a href="https://github.com/vinta" target="_blank" rel="noopener"
|
||||
>GitHub</a
|
||||
>
|
||||
<span class="footer-sep">/</span>
|
||||
<a href="https://twitter.com/vinta" target="_blank" rel="noopener"
|
||||
>Twitter</a
|
||||
>
|
||||
</footer>
|
||||
|
||||
<noscript
|
||||
><p style="text-align: center; padding: 1rem; color: #666">
|
||||
><p class="noscript-msg">
|
||||
JavaScript is needed for search and filtering.
|
||||
</p></noscript
|
||||
>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2 class="sr-only">Search and filter</h2>
|
||||
<div class="controls">
|
||||
<div class="search-wrap">
|
||||
<svg
|
||||
@@ -60,22 +61,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<h2 class="sr-only">Results</h2>
|
||||
<div class="table-wrap" tabindex="0" role="region" aria-label="Libraries table">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-num"><span class="sr-only">#</span></th>
|
||||
<th class="col-name">Project Name</th>
|
||||
<th class="col-stars">GitHub Stars</th>
|
||||
<th class="col-name" data-sort="name">Project Name</th>
|
||||
<th class="col-stars" data-sort="stars">GitHub Stars</th>
|
||||
<th class="col-commit" data-sort="commit-time">Last Commit</th>
|
||||
<th class="col-cat">Category</th>
|
||||
<th class="col-group">Group</th>
|
||||
<th class="col-arrow"></th>
|
||||
<th class="col-arrow"><span class="sr-only">Details</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr
|
||||
class="row"
|
||||
role="button"
|
||||
data-cat="{{ entry.category }}"
|
||||
data-group="{{ entry.group }}"
|
||||
tabindex="0"
|
||||
@@ -92,25 +95,24 @@
|
||||
{% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{%
|
||||
else %}—{% endif %}
|
||||
</td>
|
||||
<td class="col-commit"
|
||||
{% if entry.last_commit_at %}data-commit="{{ entry.last_commit_at }}"{% endif %}
|
||||
>{% if entry.last_commit_at %}<time datetime="{{ entry.last_commit_at }}">{{ entry.last_commit_at[:10] }}</time>{% else %}—{% endif %}</td>
|
||||
<td class="col-cat">
|
||||
<button class="tag" data-type="cat" data-value="{{ entry.category }}">
|
||||
{{ entry.category }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="col-group">
|
||||
<button class="tag" data-type="group" data-value="{{ entry.group }}">
|
||||
{{ entry.group }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="col-arrow"><span class="arrow">→</span></td>
|
||||
</tr>
|
||||
<tr class="expand-row" id="expand-{{ loop.index }}">
|
||||
<td></td>
|
||||
<td colspan="5">
|
||||
<td colspan="3">
|
||||
<div class="expand-content">
|
||||
{% if entry.description %}
|
||||
<div class="expand-desc">{{ entry.description | safe }}</div>
|
||||
{% endif %} {% if entry.also_see %}
|
||||
{% endif %}
|
||||
{% if entry.also_see %}
|
||||
<div class="expand-also-see">
|
||||
Also see: {% for see in entry.also_see %}<a
|
||||
href="{{ see.url }}"
|
||||
@@ -131,11 +133,16 @@
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ entry.url | replace("https://", "") }}</a
|
||||
>{% if entry.pushed_at %}<span class="expand-sep">·</span
|
||||
>Last pushed {{ entry.pushed_at[:10] }}{% endif %}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col-cat">
|
||||
<button class="tag" data-type="group" data-value="{{ entry.group }}">
|
||||
{{ entry.group }}
|
||||
</button>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
+1
-289
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,6 @@ 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,
|
||||
)
|
||||
@@ -65,27 +64,6 @@ class TestExtractGithubRepos:
|
||||
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"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user