npm: use /latest, the abbreviated doc strips repository and metadata
Browse files- src/devnet/sources/npm.py +36 -42
src/devnet/sources/npm.py
CHANGED
|
@@ -5,17 +5,18 @@ from urllib.parse import quote
|
|
| 5 |
|
| 6 |
from ..core import Resource, UpstreamError, get
|
| 7 |
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
_ABBREVIATED = {"Accept": "application/vnd.npm.install-v1+json"}
|
| 14 |
-
|
| 15 |
-
# ponytail: repo_slug is parsed from the `repository` field, which npm lets
|
| 16 |
-
# authors write in at least six shapes. Ceiling: a monorepo `directory` field is
|
| 17 |
-
# ignored, so the slug points at the repo root rather than the sub-package.
|
| 18 |
-
# Upgrade: cross-check package.json `exports` when a wrong root shows up.
|
| 19 |
|
| 20 |
|
| 21 |
def _text(value) -> str | None:
|
|
@@ -23,9 +24,9 @@ def _text(value) -> str | None:
|
|
| 23 |
if isinstance(value, str):
|
| 24 |
return value.strip() or None
|
| 25 |
if isinstance(value, dict):
|
| 26 |
-
for
|
| 27 |
-
if isinstance(value.get(
|
| 28 |
-
return value[
|
| 29 |
return None
|
| 30 |
|
| 31 |
|
|
@@ -47,22 +48,20 @@ def _repo_slug(repository) -> str | None:
|
|
| 47 |
|
| 48 |
|
| 49 |
def package(name: str) -> dict:
|
| 50 |
-
"""Current
|
| 51 |
raw_name = str(name or "").strip().lower()
|
| 52 |
if not raw_name or len(raw_name) > 214:
|
| 53 |
raise UpstreamError("invalid npm package name")
|
| 54 |
|
| 55 |
-
payload = get(
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
versions = payload.get("versions") or {}
|
| 63 |
-
times = payload.get("time") or {}
|
| 64 |
-
manifest = versions.get(latest) or {}
|
| 65 |
maintainers = payload.get("maintainers") or []
|
|
|
|
| 66 |
canonical = payload.get("name") or raw_name
|
| 67 |
|
| 68 |
return Resource(
|
|
@@ -73,34 +72,29 @@ def package(name: str) -> dict:
|
|
| 73 |
url=f"https://www.npmjs.com/package/{canonical}",
|
| 74 |
description=payload.get("description"),
|
| 75 |
author={"name": _text(payload.get("author"))},
|
| 76 |
-
timestamps={
|
| 77 |
-
|
| 78 |
-
"
|
|
|
|
|
|
|
| 79 |
},
|
| 80 |
-
metrics={"versions": len(versions), "maintainers": len(maintainers)},
|
| 81 |
tags=[k for k in (payload.get("keywords") or []) if isinstance(k, str)][:12],
|
| 82 |
metadata={
|
| 83 |
"license": _text(payload.get("license")),
|
| 84 |
-
"repo_slug": _repo_slug(
|
| 85 |
-
"repository":
|
| 86 |
-
payload.get("repository").get("url")
|
| 87 |
-
if isinstance(payload.get("repository"), dict)
|
| 88 |
-
else None
|
| 89 |
-
),
|
| 90 |
-
"dist_tags": tags,
|
| 91 |
"homepage": _text(payload.get("homepage")),
|
|
|
|
| 92 |
},
|
| 93 |
data={
|
| 94 |
-
"latest_version":
|
| 95 |
-
"
|
| 96 |
-
"
|
| 97 |
-
"
|
| 98 |
-
"dependencies": manifest.get("dependencies") or {},
|
| 99 |
-
"peer_dependencies": manifest.get("peerDependencies") or {},
|
| 100 |
-
"engines": manifest.get("engines") or {},
|
| 101 |
"maintainers": [
|
| 102 |
m.get("name") for m in maintainers if isinstance(m, dict) and m.get("name")
|
| 103 |
][:10],
|
|
|
|
| 104 |
},
|
| 105 |
provenance={"provider": "npm_registry", "url": API.format(name=raw_name)},
|
| 106 |
).dict()
|
|
|
|
| 5 |
|
| 6 |
from ..core import Resource, UpstreamError, get
|
| 7 |
|
| 8 |
+
# The /latest document carries full metadata for one version. The full package
|
| 9 |
+
# document adds version history and publish dates, but trips the response size
|
| 10 |
+
# cap for popular packages - @types/node has thousands of versions.
|
| 11 |
+
#
|
| 12 |
+
# ponytail: no version list and no publish dates. Ceiling: npm alone cannot answer
|
| 13 |
+
# "is this package abandoned?" - use the repo's last push or its RSS feed instead.
|
| 14 |
+
# Upgrade: a second call to the abbreviated document when a caller needs history.
|
| 15 |
+
API = "https://registry.npmjs.org/{name}/latest"
|
| 16 |
|
| 17 |
+
# ponytail: repo_slug is parsed from `repository`, which npm lets authors write in
|
| 18 |
+
# at least six shapes. Ceiling: a monorepo `directory` field is ignored, so the
|
| 19 |
+
# slug points at the repo root rather than the sub-package.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _text(value) -> str | None:
|
|
|
|
| 24 |
if isinstance(value, str):
|
| 25 |
return value.strip() or None
|
| 26 |
if isinstance(value, dict):
|
| 27 |
+
for field in ("type", "name", "url"):
|
| 28 |
+
if isinstance(value.get(field), str) and value[field].strip():
|
| 29 |
+
return value[field].strip()
|
| 30 |
return None
|
| 31 |
|
| 32 |
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
def package(name: str) -> dict:
|
| 51 |
+
"""Current published version, dependencies, license and source repository."""
|
| 52 |
raw_name = str(name or "").strip().lower()
|
| 53 |
if not raw_name or len(raw_name) > 214:
|
| 54 |
raise UpstreamError("invalid npm package name")
|
| 55 |
|
| 56 |
+
payload = get(API.format(name=quote(raw_name, safe="")), ttl=900)
|
| 57 |
+
if not isinstance(payload, dict):
|
| 58 |
+
raise UpstreamError("unexpected npm response")
|
| 59 |
+
|
| 60 |
+
version = payload.get("version") or ""
|
| 61 |
+
dependencies = payload.get("dependencies") or {}
|
| 62 |
+
peer = payload.get("peerDependencies") or {}
|
|
|
|
|
|
|
|
|
|
| 63 |
maintainers = payload.get("maintainers") or []
|
| 64 |
+
repository = payload.get("repository")
|
| 65 |
canonical = payload.get("name") or raw_name
|
| 66 |
|
| 67 |
return Resource(
|
|
|
|
| 72 |
url=f"https://www.npmjs.com/package/{canonical}",
|
| 73 |
description=payload.get("description"),
|
| 74 |
author={"name": _text(payload.get("author"))},
|
| 75 |
+
timestamps={},
|
| 76 |
+
metrics={
|
| 77 |
+
"dependencies": len(dependencies),
|
| 78 |
+
"peer_dependencies": len(peer),
|
| 79 |
+
"maintainers": len(maintainers),
|
| 80 |
},
|
|
|
|
| 81 |
tags=[k for k in (payload.get("keywords") or []) if isinstance(k, str)][:12],
|
| 82 |
metadata={
|
| 83 |
"license": _text(payload.get("license")),
|
| 84 |
+
"repo_slug": _repo_slug(repository),
|
| 85 |
+
"repository": repository if isinstance(repository, str) else (repository or {}).get("url"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
"homepage": _text(payload.get("homepage")),
|
| 87 |
+
"deprecated": _text(payload.get("deprecated")),
|
| 88 |
},
|
| 89 |
data={
|
| 90 |
+
"latest_version": version,
|
| 91 |
+
"dependencies": dependencies,
|
| 92 |
+
"peer_dependencies": peer,
|
| 93 |
+
"engines": payload.get("engines") or {},
|
|
|
|
|
|
|
|
|
|
| 94 |
"maintainers": [
|
| 95 |
m.get("name") for m in maintainers if isinstance(m, dict) and m.get("name")
|
| 96 |
][:10],
|
| 97 |
+
"tarball": (payload.get("dist") or {}).get("tarball"),
|
| 98 |
},
|
| 99 |
provenance={"provider": "npm_registry", "url": API.format(name=raw_name)},
|
| 100 |
).dict()
|