HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1"""PyPI — `https://pypi.org/pypi/<pkg>/json` (public JSON document) for the AI Python ecosystem. Tier 2.23Packages = the `pypi` names of registry/repositories.yaml (identifiers `pypi` + `github_repo` → merges with the GitHub entity) plus a4curated list of standalone libraries (identifiers `pypi` only, entity type `library`).56Claims: `pypi_version`, `pypi_release_at` (earliest upload time of the latest release's files), `license`, `description` (summary),7`homepage`, `requires_python`, `metric.releases`, `repository_url` (when the project links GitHub). For packages that are NOT mapped to a8GitHub repository the connector also sets `latest_version` / `latest_release_at`; for mapped packages those two properties are owned by9the GitHub release feed so two same-tier sources never flip-flop the same property when a tag lands hours before the wheel.10"""11from __future__ import annotations1213import re14from datetime import UTC15from typing import Any1617from aiatlas.registry import org_by_github, org_ref18from aiatlas.sdk.connector import BaseConnector, Parsed, RunContext19from aiatlas.sdk.extract.dates import parse_datetime20from aiatlas.sdk.facts import EntityRef, Facts, Target21from aiatlas.sdk.fetch import FetchResult2223from .github import claim_license, repo_entity, repo_entries2425EXTRA_PACKAGES = [26 "torch", "transformers", "vllm", "mlx", "mlx-lm", "llama-cpp-python", "langchain", "langgraph", "llama-index", "sglang", "jax", "onnxruntime",27 "diffusers", "peft", "trl", "unsloth", "openai", "anthropic", "google-genai", "mistralai", "cohere", "litellm", "faiss-cpu", "chromadb",28 "qdrant-client", "pymilvus", "weaviate-client", "lancedb", "crewai", "pydantic-ai", "browser-use", "ragas", "langfuse", "ray", "bentoml",29 "triton", "flash-attn", "lm-eval", "sentence-transformers", "accelerate", "bitsandbytes", "autoawq", "gguf", "safetensors", "tokenizers",30 "datasets", "evaluate", "gradio", "streamlit", "fastapi", "huggingface-hub", "optimum", "xformers", "einops", "tiktoken", "instructor",31 "dspy", "outlines", "guidance", "semantic-kernel", "haystack-ai", "autogen-agentchat", "smolagents", "mcp", "openai-agents", "letta",32 "vector-quantize-pytorch", "timm", "torchvision", "torchaudio", "scikit-learn", "xgboost", "lightgbm", "keras", "tensorflow", "deepspeed",33 "megatron-core", "flax", "optax", "numpyro", "pyro-ppl", "spacy", "nltk", "whisperx", "faster-whisper", "ctranslate2", "onnx", "tensorrt",34 "nvidia-modelopt", "auto-gptq", "exllamav2", "llmcompressor", "sparseml", "lmdeploy", "text-generation", "openllm", "modal", "wandb", "mlflow",35 "comet-ml", "arize-phoenix", "opentelemetry-instrumentation-openai", "promptfoo", "deepeval", "inspect-ai", "trulens", "guardrails-ai",36]37GITHUB_URL = re.compile(r"https?://github\.com/([\w.-]+/[\w.-]+)")383940class PyPIConnector(BaseConnector):41 name = "pypi"42 label = "PyPI — versions and metadata of the AI Python ecosystem"43 description = "Public JSON documents of the packages in registry/repositories.yaml plus a curated list of standalone AI libraries."44 source_key = "pypi.org"45 version = "2"46 parser_version = "2"47 interval_seconds = 12 * 360048 min_interval_seconds = 6 * 360049 max_interval_seconds = 3 * 8640050 rate_per_min = 3051 tier = 252 priority = 253 expected_min_records = 6054 concurrency = 35556 def packages(self) -> dict[str, dict[str, Any] | None]:57 out: dict[str, dict[str, Any] | None] = {}58 for entry in repo_entries():59 if entry.get("pypi"):60 out[_norm(entry["pypi"])] = entry61 for pkg in self.config.get("packages") or EXTRA_PACKAGES:62 out.setdefault(_norm(pkg), None)63 return out6465 async def discover(self, ctx: RunContext) -> list[Target]:66 return [Target(url=f"https://pypi.org/pypi/{pkg}/json", doc_type="package", key=f"pkg:{pkg}", min_bytes=200,67 meta={"pypi": pkg, "repo": (entry or {}).get("repo"), "content_type": "application/json"}, priority=2)68 for pkg, entry in self.packages().items()]6970 async def extract(self, ctx: RunContext, target: Target, res: FetchResult, parsed: Parsed) -> Facts:71 facts = Facts()72 data = parsed.json if parsed.kind == "json" else None73 if not isinstance(data, dict) or not isinstance(data.get("info"), dict):74 return facts75 info = data["info"]76 pkg = _norm(info.get("name") or target.meta.get("pypi") or "")77 if not pkg:78 return facts79 entry = next((e for e in repo_entries() if e.get("pypi") and _norm(e["pypi"]) == pkg), None)80 urls = {k.lower(): v for k, v in (info.get("project_urls") or {}).items() if isinstance(v, str)}81 gh = _github(urls, info.get("home_page"))82 if entry:83 ref = repo_entity(facts, entry)84 else:85 org = None86 if gh:87 known = org_by_github(gh.split("/")[0])88 org = org_ref(known["key"]) if known else None89 ids = {"pypi": pkg}90 if gh and _norm(gh.split("/")[1]) == pkg: # `gguf` links to ggml-org/llama.cpp: a dependency, not the same project91 ids["github_repo"] = gh92 ref = facts.entity("library", info.get("name") or pkg, identifiers=ids, organization=org, aliases=[pkg])93 if org:94 facts.entities.append(org)95 facts.relate(org, "develops", ref)96 if gh:97 facts.claim(ref, "repository_url", f"https://github.com/{gh}")98 facts.claim(ref, "pypi", pkg)99 facts.claim(ref, "pypi_url", info.get("package_url") or f"https://pypi.org/project/{pkg}/")100 version = info.get("version")101 released = _release_time(data, version)102 facts.claim(ref, "pypi_version", version)103 facts.claim(ref, "pypi_release_at", released)104 if not entry:105 facts.claim(ref, "latest_version", version)106 facts.claim(ref, "latest_release_at", released)107 lic = _license(info)108 claim_license(facts, ref, lic, prop="pypi_license")109 if not entry:110 claim_license(facts, ref, lic) # mapped repos: GitHub's SPDX id owns `license` (avoids same-tier flip-flops)111 facts.claim(ref, "kind", "library")112 summary = (info.get("summary") or "").strip()113 facts.claim(ref, "description", summary[:1000] or None)114 homepage = urls.get("homepage") or urls.get("home") or info.get("home_page")115 if not entry and isinstance(homepage, str) and homepage.startswith("http"):116 facts.claim(ref, "homepage", homepage)117 docs = urls.get("documentation") or urls.get("docs")118 facts.claim(ref, "docs_url", docs if isinstance(docs, str) and docs.startswith("http") else None)119 facts.claim(ref, "requires_python", info.get("requires_python") or None)120 facts.claim(ref, "author", (info.get("author") or "").strip() or None)121 facts.claim(ref, "metric.releases", len(data.get("releases") or {}))122 facts.claim(ref, "python_classifiers", [c for c in info.get("classifiers") or [] if c.startswith("Programming Language :: Python :: 3.")][:12])123 facts.document_entity = ref124 facts.document_title = f"{info.get('name') or pkg} {version or ''} on PyPI".strip()125 return facts126127128def _norm(name: str) -> str:129 return re.sub(r"[-_.]+", "-", name.strip()).lower()130131132def _github(urls: dict[str, str], home: Any) -> str | None:133 for key in ("source", "source code", "repository", "code", "github", "homepage", "home"):134 v = urls.get(key)135 if v:136 m = GITHUB_URL.search(v)137 if m:138 return m.group(1).removesuffix(".git")139 if isinstance(home, str):140 m = GITHUB_URL.search(home)141 if m:142 return m.group(1).removesuffix(".git")143 return None144145146def _license(info: dict[str, Any]) -> str | None:147 expr = info.get("license_expression")148 if isinstance(expr, str) and expr.strip():149 return expr.strip()150 lic = info.get("license")151 if isinstance(lic, str) and 0 < len(lic.strip()) <= 80:152 return lic.strip()153 for c in info.get("classifiers") or []:154 if c.startswith("License :: OSI Approved :: "):155 return c.rsplit(" :: ", 1)[1]156 if c.startswith("License :: ") and c != "License :: OSI Approved":157 return c.rsplit(" :: ", 1)[1]158 return None159160161def _release_time(data: dict[str, Any], version: str | None) -> str | None:162 files = (data.get("releases") or {}).get(version or "") or data.get("urls") or []163 times = [parse_datetime(f.get("upload_time_iso_8601") or f.get("upload_time")) for f in files if isinstance(f, dict)]164 times = [t for t in times if t]165 return min(times).astimezone(UTC).isoformat(timespec="seconds") if times else None166167168__all__ = ["EXTRA_PACKAGES", "PyPIConnector"]169_ = EntityRef # re-exported type for tests170CONNECTORS = [PyPIConnector]171