mirror of
https://github.com/wilsonfreitas/awesome-quant.git
synced 2026-07-27 20:07:50 +00:00
cf807c050d
h2 headings are now categories (not languages). Language is extracted from inline tags like `Python` `Rust` at the start of descriptions. Changes: - Add extract_languages() function to parse backtick-delimited language tags - Update README parsing loop to extract languages from descriptions - Update Project class to handle multiple languages per entry - Add 'languages' CSV column with comma-separated language list - Primary 'language' column contains first language for backward compatibility The parser now supports category-first README structure with inline language tags.
296 lines
9.4 KiB
Python
296 lines
9.4 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from html.parser import HTMLParser
|
|
from threading import Thread
|
|
from urllib.request import urlopen
|
|
|
|
import pandas as pd
|
|
from github import Auth, Github
|
|
|
|
# using an access token
|
|
auth = Auth.Token(os.environ["GITHUB_ACCESS_TOKEN"])
|
|
g = Github(auth=auth)
|
|
|
|
|
|
def extract_repo(url):
|
|
reu = re.compile(r"^https://github.com/([\w-]+/[-\w\.]+)$")
|
|
m = reu.match(url)
|
|
if m:
|
|
return m.group(1)
|
|
else:
|
|
return ""
|
|
|
|
|
|
def extract_github_url(description):
|
|
"""Extract GitHub URL from description if present."""
|
|
github_pattern = re.compile(r"\[GitHub\]\((https://github\.com/[\w-]+/[-\w\.]+)\)")
|
|
m = github_pattern.search(description)
|
|
if m:
|
|
return m.group(1)
|
|
return ""
|
|
|
|
|
|
def get_cran_info(url):
|
|
"""Fetch Published date and GitHub URL from a CRAN package page.
|
|
|
|
Returns (published_date, github_url) — either may be empty string.
|
|
"""
|
|
try:
|
|
m = re.search(r"package=(\w+)", url) or re.search(
|
|
r"/packages/(\w+)", url
|
|
)
|
|
if not m:
|
|
return "", ""
|
|
pkg = m.group(1)
|
|
page_url = f"https://cran.r-project.org/web/packages/{pkg}/index.html"
|
|
with urlopen(page_url, timeout=10) as resp:
|
|
page_html = resp.read().decode("utf-8", errors="replace")
|
|
|
|
class CranParser(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._in_td = False
|
|
self._found_published = False
|
|
self._in_github_span = False
|
|
self.date = ""
|
|
self.github_url = ""
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag == "td":
|
|
self._in_td = True
|
|
# GitHub links are wrapped in <span class="GitHub">
|
|
if tag == "span":
|
|
classes = dict(attrs).get("class", "")
|
|
if "GitHub" in classes:
|
|
self._in_github_span = True
|
|
# Also check <a href> for github.com links
|
|
if tag == "a" and not self.github_url:
|
|
href = dict(attrs).get("href", "")
|
|
if "github.com" in href and "/issues" not in href:
|
|
self.github_url = href
|
|
|
|
def handle_endtag(self, tag):
|
|
if tag == "td":
|
|
self._in_td = False
|
|
if tag == "span":
|
|
self._in_github_span = False
|
|
|
|
def handle_data(self, data):
|
|
if self._in_td:
|
|
if data.strip() == "Published:":
|
|
self._found_published = True
|
|
elif self._found_published and not self.date:
|
|
d = data.strip()
|
|
if re.match(r"\d{4}-\d{2}-\d{2}", d):
|
|
self.date = d
|
|
|
|
parser = CranParser()
|
|
parser.feed(page_html)
|
|
# Clean trailing slashes or .git from GitHub URL
|
|
gh = parser.github_url.rstrip("/")
|
|
if gh.endswith(".git"):
|
|
gh = gh[:-4]
|
|
return parser.date, gh
|
|
except Exception as e:
|
|
print(f"CRAN ERROR {url}: {e}")
|
|
return "", ""
|
|
|
|
|
|
def get_pypi_last_updated(url):
|
|
"""Fetch the last release date from PyPI JSON API."""
|
|
try:
|
|
# Extract package name from URL like https://pypi.org/project/tushare/
|
|
m = re.search(r"pypi\.org/project/([\w.-]+)", url) or re.search(
|
|
r"pypi\.python\.org/pypi/([\w.-]+)", url
|
|
)
|
|
if not m:
|
|
return ""
|
|
pkg = m.group(1).rstrip("/")
|
|
api_url = f"https://pypi.org/pypi/{pkg}/json"
|
|
with urlopen(api_url, timeout=10) as resp:
|
|
data = json.loads(resp.read().decode("utf-8"))
|
|
|
|
releases = data.get("releases", {})
|
|
if not releases:
|
|
return ""
|
|
|
|
# Find the latest release with an upload time
|
|
for version in sorted(releases.keys(), reverse=True):
|
|
release_data = releases[version]
|
|
if release_data:
|
|
upload_time = release_data[0].get("upload_time_iso_8601")
|
|
if upload_time:
|
|
return upload_time.split("T")[0]
|
|
return ""
|
|
except Exception as e:
|
|
print(f"PYPI ERROR {url}: {e}")
|
|
return ""
|
|
|
|
|
|
def slugify(text):
|
|
"""Convert text to lowercase hyphen-separated slug."""
|
|
text = text.lower().strip()
|
|
text = re.sub(r"[&/]+", "-", text)
|
|
text = re.sub(r"[^\w\s-]", "", text)
|
|
text = re.sub(r"[\s_]+", "-", text)
|
|
text = re.sub(r"-+", "-", text)
|
|
return text.strip("-")
|
|
|
|
|
|
re_langs = re.compile(r'^((?:`[^`]+`\s*)+)-\s*(.*)$')
|
|
|
|
|
|
def extract_languages(description: str) -> tuple[list[str], str]:
|
|
"""Extract inline language tags from description.
|
|
|
|
Returns (languages, clean_description).
|
|
E.g. "`Python` `Rust` - High-performance..." -> (["Python", "Rust"], "High-performance...")
|
|
"""
|
|
m = re_langs.match(description)
|
|
if m:
|
|
lang_str = m.group(1)
|
|
clean_desc = m.group(2)
|
|
langs = re.findall(r'`([^`]+)`', lang_str)
|
|
return langs, clean_desc
|
|
return [], description
|
|
|
|
|
|
def get_repo_info(repo):
|
|
"""Fetch last commit date and star count from GitHub."""
|
|
try:
|
|
if repo:
|
|
r = g.get_repo(repo)
|
|
cs = r.get_commits()
|
|
last_commit = cs[0].commit.author.date.strftime("%Y-%m-%d")
|
|
stars = r.stargazers_count
|
|
return last_commit, stars
|
|
else:
|
|
return "", 0
|
|
except Exception:
|
|
print("ERROR " + repo)
|
|
return "error", 0
|
|
|
|
|
|
class Project(Thread):
|
|
def __init__(self, match, language, category, section_path):
|
|
super().__init__()
|
|
self._match = match
|
|
self.regs = None
|
|
self._language = language
|
|
self._category = category
|
|
self._section_path = section_path
|
|
self.languages = []
|
|
self.clean_description = ""
|
|
|
|
def run(self):
|
|
m = self._match
|
|
primary_url = m.group(2)
|
|
# Use clean_description if it was set by the parser, otherwise extract from match
|
|
if self.clean_description:
|
|
description = self.clean_description
|
|
else:
|
|
description = m.group(3)
|
|
|
|
# Check if primary URL is GitHub
|
|
is_github = "github.com" in primary_url
|
|
|
|
# If not GitHub, check if there's a GitHub link in the description
|
|
github_url = ""
|
|
if not is_github:
|
|
github_url = extract_github_url(description)
|
|
else:
|
|
github_url = primary_url
|
|
|
|
is_cran = "cran.r-project.org" in primary_url
|
|
is_pypi = "pypi.org" in primary_url or "pypi.python.org" in primary_url
|
|
is_commercial = self._language == "Commercial & Proprietary Services"
|
|
|
|
# For CRAN projects, scrape the CRAN page for GitHub URL and published date
|
|
cran_date = ""
|
|
if is_cran:
|
|
cran_date, cran_github = get_cran_info(primary_url)
|
|
if cran_github and not github_url:
|
|
github_url = cran_github
|
|
|
|
repo = extract_repo(github_url)
|
|
print(repo or primary_url)
|
|
last_commit, stars = get_repo_info(repo)
|
|
|
|
# Fallback: use CRAN/PyPI dates when no GitHub data
|
|
if not last_commit or last_commit == "error":
|
|
if is_cran and cran_date:
|
|
last_commit = cran_date
|
|
elif is_pypi:
|
|
pypi_date = get_pypi_last_updated(primary_url)
|
|
if pypi_date:
|
|
last_commit = pypi_date
|
|
|
|
# Build section slug from category or language
|
|
section_slug = slugify(self._category or self._language)
|
|
|
|
self.regs = dict(
|
|
project=m.group(1),
|
|
language=self._language,
|
|
languages=",".join(self.languages),
|
|
category=self._category,
|
|
section=self._section_path,
|
|
section_slug=section_slug,
|
|
last_commit=last_commit,
|
|
stars=stars,
|
|
url=primary_url,
|
|
description=description,
|
|
github=is_github or bool(github_url),
|
|
cran=is_cran,
|
|
pypi=is_pypi,
|
|
commercial=is_commercial,
|
|
repo=repo,
|
|
)
|
|
|
|
|
|
projects = []
|
|
|
|
with open("README.md", "r", encoding="utf8") as f:
|
|
ret = re.compile(r"^(#+) (.*)$")
|
|
rex = re.compile(r"^\s*- \[(.*)\]\((.*)\) - (.*)$")
|
|
re_badge = re.compile(r"\s*!\[[^\]]*\]\([^)]*\)\s*")
|
|
m_titles = []
|
|
last_head_level = 0
|
|
current_category = ""
|
|
for line in f:
|
|
line = re_badge.sub(" ", line)
|
|
m = rex.match(line)
|
|
if m:
|
|
raw_desc = m.group(3).strip()
|
|
|
|
# Extract language tags from description
|
|
languages, clean_description = extract_languages(raw_desc)
|
|
primary_language = languages[0] if languages else ""
|
|
|
|
p = Project(
|
|
m,
|
|
primary_language,
|
|
current_category,
|
|
current_category,
|
|
)
|
|
p.languages = languages
|
|
p.clean_description = clean_description
|
|
p.start()
|
|
projects.append(p)
|
|
else:
|
|
m = ret.match(line)
|
|
if m:
|
|
hrs = m.group(1)
|
|
title = m.group(2).strip()
|
|
if len(hrs) == 2 and title != "Contents":
|
|
current_category = title
|
|
|
|
while True:
|
|
checks = [not p.is_alive() for p in projects]
|
|
if all(checks):
|
|
break
|
|
|
|
projects = [p.regs for p in projects]
|
|
df = pd.DataFrame(projects)
|
|
df.to_csv("site/projects.csv", index=False)
|