diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | app.py | 19 | ||||
-rw-r--r-- | repos/__init__.py | 28 | ||||
-rw-r--r-- | repos/alpine_linux.py | 68 | ||||
-rw-r--r-- | repos/base.py | 66 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | templates/badge.svg.jinja | 12 |
7 files changed, 197 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e18a86f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/data/ +/venv/ @@ -0,0 +1,19 @@ +from flask import Flask, render_template + +import repos + +app = Flask(__name__) + + +@app.route('/') +def hello_world(): + return '<a href="/badge/rust.svg">sample badge for Rust</a>' + + +@app.route('/badge/<package>.svg') +def badge(package: str): + return render_template('badge.svg.jinja', versions=repos.get_versions(package)) + + +if __name__ == '__main__': + app.run() diff --git a/repos/__init__.py b/repos/__init__.py new file mode 100644 index 0000000..0f71dd1 --- /dev/null +++ b/repos/__init__.py @@ -0,0 +1,28 @@ +from typing import Mapping, List, Any + +from . import alpine_linux +from .base import Repository + +__all__ = [ + 'get_versions', +] + +def repos_from(module): + for exported in module.__all__: + attr = getattr(module, exported) + if isinstance(attr, Repository): + yield attr + +all_repos: List[Repository] = [ + *repos_from(alpine_linux), +] + +def get_versions(package: str) -> Mapping[str, Mapping[str, str]]: + result = dict() + for repo in all_repos: + repo_versions = repo.get_versions() + if package in repo_versions: + if repo.family not in result: + result[repo.family] = dict() + result[repo.family][repo.repo] = repo_versions[package] + return result diff --git a/repos/alpine_linux.py b/repos/alpine_linux.py new file mode 100644 index 0000000..64f159a --- /dev/null +++ b/repos/alpine_linux.py @@ -0,0 +1,68 @@ +from io import TextIOWrapper +from pathlib import Path +import tarfile +from typing import Mapping, TextIO + +from .base import Repository + +__all__ = [ + 'stable_main_x86_64', + 'stable_community_x86_64', + 'edge_main_x86_64', + 'edge_community_x86_64', + 'edge_testing_x86_64', +] + +def parse_apkindex(apkindex: TextIO) -> Mapping[str, str]: + result = dict() + current_package = None + current_version = None + ignore_lines = ['C', 'A', 'S', 'I', 'T', 'U', 'L', 'o', 'm', 't', 'c', 'D', 'p', 'i', 'k'] + for line in apkindex: + line = line.strip() + if len(line) == 0: + if current_package is not None and current_version is not None: + result[current_package] = current_version + current_package = None + current_version = None + continue + try: + line_type, line_data = line.split(':', 1) + except ValueError: + print('what uhhhh the fuck', line, line.split(':', 1)) + continue + if line_type == 'C': + # TODO figure out what this means + pass + elif line_type == 'P': + current_package = line_data + elif line_type == 'V': + current_version = line_data + elif line_type in ignore_lines: + pass + else: + raise ValueError('unknown line type: ' + line_type + ' in line ' + repr(line)) + return result + +def parse_cached(cached: Path) -> Mapping[str, str]: + apkindex = tarfile.open(cached) + for archive_member in apkindex.getmembers(): + if archive_member.name == 'APKINDEX': + apkindex_file = apkindex.extractfile(archive_member) + apkindex_file = TextIOWrapper(apkindex_file) + return parse_apkindex(apkindex_file) + +def build_repo(name: str, url_path: str): + url = f'http://dl-cdn.alpinelinux.org/alpine/{url_path}/APKINDEX.tar.gz' + return Repository( + family='Alpine Linux', + repo=name, + index_url=url, + parse=parse_cached, + ) + +stable_main_x86_64 = build_repo('Stable (main/x86_64)', 'latest-stable/main/x86_64') +stable_community_x86_64 = build_repo('Stable (community/x86_64)', 'latest-stable/community/x86_64') +edge_main_x86_64 = build_repo('Edge (main/x86_64)', 'edge/main/x86_64') +edge_community_x86_64 = build_repo('Edge (community/x86_64)', 'edge/community/x86_64') +edge_testing_x86_64 = build_repo('Edge (testing/x86_64)', 'edge/testing/x86_64') diff --git a/repos/base.py b/repos/base.py new file mode 100644 index 0000000..66ecf2d --- /dev/null +++ b/repos/base.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +import datetime +import gzip +import json +import os +from pathlib import Path +import re +from typing import Callable, Mapping + +import requests + +HTTP_DATE = '%a, %d %b %Y %H:%M:%S GMT' +SLUGIFY = re.compile('\W+') + +def slug(text: str) -> str: + return SLUGIFY.sub('-', text.lower()).strip('-') + +@dataclass() +class Repository: + family: str + repo: str + index_url: str + parse: Callable[[Path], Mapping[str, str]] + + def _full_name(self): + return f'{self.family} {self.repo}' + + def _cache_dir(self) -> Path: + return Path('data') / slug(self.family) / slug(self.repo) + + def _cache_file(self, name: str) -> Path: + return self._cache_dir() / name + + def get_versions(self) -> Mapping[str, str]: + self._cache_dir().mkdir(parents=True, exist_ok=True) + downloaded_file = self._cache_file('downloaded') + if downloaded_file.exists(): + mtime = downloaded_file.stat().st_mtime + else: + mtime = 0 + mtime = datetime.datetime.fromtimestamp(mtime, datetime.timezone.utc) + mtime = mtime.strftime(HTTP_DATE) + + parsed_file = self._cache_file('parsed.json.gz') + + response = requests.get(self.index_url, headers={ + 'If-Modified-Since': mtime, + }, stream=True) + if response.status_code != requests.codes.not_modified: + response.raise_for_status() + print('Re-downloading', self._full_name()) + set_mtime = response.headers.get('Last-Modified', '') + with downloaded_file.open('wb') as f: + for chunk in response.iter_content(chunk_size=256): + f.write(chunk) + if len(set_mtime) > 0: + set_mtime = datetime.datetime.strptime(set_mtime, HTTP_DATE) + os.utime(downloaded_file, (datetime.datetime.now().timestamp(), set_mtime.timestamp())) + + parsed_data = self.parse(downloaded_file) + with gzip.open(parsed_file, 'wt') as f: + json.dump(parsed_data, f) + return parsed_data + else: + with gzip.open(parsed_file, 'rt') as f: + return json.load(f) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8598ba2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==1.1.2 +requests==2.25.1 diff --git a/templates/badge.svg.jinja b/templates/badge.svg.jinja new file mode 100644 index 0000000..64117b8 --- /dev/null +++ b/templates/badge.svg.jinja @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<svg width="690" height="420" xmlns="http://www.w3.org/2000/svg"> + {% set ns = namespace(y = 20) %} + <text x="0" y="{{ ns.y }}{% set ns.y = ns.y + 20 %}">Packaging Status</text> + {% for family, family_contents in versions.items() %} + <text x="10" y="{{ ns.y }}{% set ns.y = ns.y + 20 %}">{{ family }}</text> + {% for repo, version in family_contents.items() %} + <text x="20" y="{{ ns.y }}">{{ repo }}</text> + <text x="300" y="{{ ns.y }}{% set ns.y = ns.y + 20 %}">{{ version }}</text> + {% endfor %} + {% endfor %} +</svg> |