Source code for swh.lister.grokmirror.lister

# Copyright (C) 2026  The Software Heritage developers
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information

from dataclasses import dataclass
from datetime import datetime, timezone
import gzip
import json
import logging
from typing import Any, Dict, Iterator, Optional
from urllib.parse import urljoin
import zlib

import iso8601

from swh.lister.pattern import CredentialsType, Lister
from swh.scheduler.interface import SchedulerInterface
from swh.scheduler.model import ListedOrigin

logger = logging.getLogger(__name__)

Repositories = Dict[str, Any]


[docs] @dataclass class GrokmirrorListerState: """State of Grokmirror lister""" last_listing_date: Optional[datetime] = None """Last date when Grokmirror lister was executed"""
[docs] class GrokmirrorLister(Lister[GrokmirrorListerState, Repositories]): """Lister class for Grokmirror instances. This lister will retrieve the list of published git repositories by retrieving the JSON manifest files published on the server, checking the modification dates, decompressing and parsing one. The lister checks the URLs /manifest.js.gz /manifest.js /manifest https://git.kernel.org/pub/scm/utils/grokmirror/grokmirror.git/about/ """ LISTER_NAME = "grokmirror" def __init__( self, scheduler: SchedulerInterface, url: Optional[str] = None, instance: Optional[str] = None, credentials: Optional[CredentialsType] = None, max_origins_per_page: Optional[int] = None, max_pages: Optional[int] = None, enable_origins: bool = True, **kwargs, ): """Lister class for Grokmirror repositories.""" super().__init__( scheduler=scheduler, url=url, instance=instance, credentials=credentials, max_origins_per_page=max_origins_per_page, max_pages=max_pages, enable_origins=enable_origins, **kwargs, ) self.listing_date = datetime.now(tz=timezone.utc) self.api_url = urljoin(self.url, "manifest.js.gz")
[docs] def state_from_dict(self, d: Dict[str, Any]) -> GrokmirrorListerState: last_listing_date = d.get("last_listing_date") if last_listing_date is not None: d["last_listing_date"] = iso8601.parse_date(last_listing_date) return GrokmirrorListerState(**d)
[docs] def state_to_dict(self, state: GrokmirrorListerState) -> Dict[str, Any]: d: Dict[str, Optional[str]] = {"last_listing_date": None} last_listing_date = state.last_listing_date if last_listing_date is not None: d["last_listing_date"] = last_listing_date.isoformat() return d
[docs] def get_pages(self) -> Iterator[Repositories]: headers = {} if self.state.last_listing_date is not None: if_modified_since = self.state.last_listing_date.strftime( "%a, %d %b %Y %H:%M:%S GMT" ) headers["If-Modified-Since"] = if_modified_since response = self.http_request(self.api_url, headers=headers) if response.status_code == 404: self.api_url = self.api_url.removesuffix(".gz") response = self.http_request(self.api_url, headers=headers) if response.status_code == 404: self.api_url = self.api_url.removesuffix(".js") response = self.http_request(self.api_url, headers=headers) if response.status_code == 200: try: json_data = gzip.decompress(response.content).decode("utf-8") except (gzip.BadGzipFile, OSError, EOFError, zlib.error): json_data = response.text repositories = json.loads(json_data) yield repositories elif response.status_code != 304: response.raise_for_status()
[docs] def get_origins_from_page( self, repositories: Repositories ) -> Iterator[ListedOrigin]: """Convert Grokmirror repositories into a list of ListedOrigins.""" assert self.lister_obj.id is not None for repo, meta in repositories.items(): origin_url = urljoin(self.url, repo) kwargs: Dict[str, Any] = {} if modified := meta.get("modified"): kwargs["last_update"] = datetime.fromtimestamp( modified, tz=timezone.utc ) if reference := meta.get("reference"): kwargs["is_fork"] = bool(reference) kwargs["forked_from_url"] = urljoin(self.url, reference) yield ListedOrigin( lister_id=self.lister_obj.id, url=origin_url, visit_type="git", **kwargs, ) for symlink in meta.get("symlinks", []): origin_url = urljoin(self.url, symlink) yield ListedOrigin( lister_id=self.lister_obj.id, url=origin_url, visit_type="git", **kwargs, )
[docs] def finalize(self) -> None: self.state.last_listing_date = self.listing_date