aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.py8
-rw-r--r--repos/__init__.py2
-rw-r--r--repos/alpine_linux.py15
-rw-r--r--repos/base.py55
-rw-r--r--requirements.txt6
-rw-r--r--templates/badge.svg.jinja39
6 files changed, 103 insertions, 22 deletions
diff --git a/app.py b/app.py
index 8fa15de..d1183e0 100644
--- a/app.py
+++ b/app.py
@@ -12,7 +12,13 @@ def hello_world():
@app.route('/badge/<package>.svg')
def badge(package: str):
- return render_template('badge.svg.jinja', versions=repos.get_versions(package))
+ versions = repos.get_versions(package)
+ newest_version = max(v for family in versions.values() for v in family.values())
+ print(newest_version)
+ rendered = render_template('badge.svg.jinja', versions=versions, newest_version=newest_version)
+ response = make_response(rendered)
+ response.headers['Content-Type'] = 'image/svg+xml'
+ return response
if __name__ == '__main__':
diff --git a/repos/__init__.py b/repos/__init__.py
index 0f71dd1..1dea3a1 100644
--- a/repos/__init__.py
+++ b/repos/__init__.py
@@ -1,4 +1,4 @@
-from typing import Mapping, List, Any
+from typing import Mapping, List
from . import alpine_linux
from .base import Repository
diff --git a/repos/alpine_linux.py b/repos/alpine_linux.py
index 64f159a..7c646cc 100644
--- a/repos/alpine_linux.py
+++ b/repos/alpine_linux.py
@@ -1,9 +1,10 @@
from io import TextIOWrapper
from pathlib import Path
+import re
import tarfile
from typing import Mapping, TextIO
-from .base import Repository
+from .base import Repository, Version
__all__ = [
'stable_main_x86_64',
@@ -13,7 +14,9 @@ __all__ = [
'edge_testing_x86_64',
]
-def parse_apkindex(apkindex: TextIO) -> Mapping[str, str]:
+PACKAGE_REVISION_INFO = re.compile(r'-r\d+$')
+
+def parse_apkindex(apkindex: TextIO) -> Mapping[str, Version]:
result = dict()
current_package = None
current_version = None
@@ -37,14 +40,16 @@ def parse_apkindex(apkindex: TextIO) -> Mapping[str, str]:
elif line_type == 'P':
current_package = line_data
elif line_type == 'V':
- current_version = line_data
+ version = line_data
+ clean_version = PACKAGE_REVISION_INFO.sub('', version)
+ current_version = Version(version, clean_version)
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]:
+def parse_cached(cached: Path) -> Mapping[str, Version]:
apkindex = tarfile.open(cached)
for archive_member in apkindex.getmembers():
if archive_member.name == 'APKINDEX':
@@ -52,7 +57,7 @@ def parse_cached(cached: Path) -> Mapping[str, str]:
apkindex_file = TextIOWrapper(apkindex_file)
return parse_apkindex(apkindex_file)
-def build_repo(name: str, url_path: str):
+def build_repo(name: str, url_path: str) -> Repository:
url = f'http://dl-cdn.alpinelinux.org/alpine/{url_path}/APKINDEX.tar.gz'
return Repository(
family='Alpine Linux',
diff --git a/repos/base.py b/repos/base.py
index 66ecf2d..220a30f 100644
--- a/repos/base.py
+++ b/repos/base.py
@@ -1,26 +1,66 @@
-from dataclasses import dataclass
+from dataclasses import dataclass, asdict as dataclass_asdict
import datetime
+from functools import total_ordering
import gzip
import json
import os
from pathlib import Path
import re
-from typing import Callable, Mapping
+from typing import Any, Callable, Mapping
import requests
+import semver
+
+__all__ = [
+ 'Repository',
+ 'Version',
+]
HTTP_DATE = '%a, %d %b %Y %H:%M:%S GMT'
-SLUGIFY = re.compile('\W+')
+SLUGIFY = re.compile(r'\W+')
def slug(text: str) -> str:
return SLUGIFY.sub('-', text.lower()).strip('-')
+@total_ordering
+@dataclass()
+class Version:
+ original: str
+ clean: str
+
+ def __str__(self) -> str:
+ return self.original
+
+ def __lt__(self, other: Any):
+ if not isinstance(other, Version):
+ return NotImplemented
+ if semver.VersionInfo.isvalid(self.clean) and semver.VersionInfo.isvalid(other.clean):
+ return semver.compare(self.clean, other.clean) < 0
+ return self.original < other.original
+
+class JSONEncoder(json.JSONEncoder):
+ def default(self, o: Any) -> Any:
+ if isinstance(o, Version):
+ return dataclass_asdict(o)
+ return super().default(o)
+
+class JSONDecoder(json.JSONDecoder):
+ @staticmethod
+ def object_hook(o: dict) -> Any:
+ if o.keys() == {'original', 'clean'}:
+ return Version(**o)
+ return o
+
+ def __init__(self):
+ super().__init__(object_hook=self.object_hook)
+
+
@dataclass()
class Repository:
family: str
repo: str
index_url: str
- parse: Callable[[Path], Mapping[str, str]]
+ parse: Callable[[Path], Mapping[str, Version]]
def _full_name(self):
return f'{self.family} {self.repo}'
@@ -31,7 +71,7 @@ class Repository:
def _cache_file(self, name: str) -> Path:
return self._cache_dir() / name
- def get_versions(self) -> Mapping[str, str]:
+ def get_versions(self) -> Mapping[str, Version]:
self._cache_dir().mkdir(parents=True, exist_ok=True)
downloaded_file = self._cache_file('downloaded')
if downloaded_file.exists():
@@ -57,10 +97,11 @@ class Repository:
set_mtime = datetime.datetime.strptime(set_mtime, HTTP_DATE)
os.utime(downloaded_file, (datetime.datetime.now().timestamp(), set_mtime.timestamp()))
+ if response.status_code != requests.codes.not_modified or not parsed_file.exists():
parsed_data = self.parse(downloaded_file)
with gzip.open(parsed_file, 'wt') as f:
- json.dump(parsed_data, f)
+ json.dump(parsed_data, f, cls=JSONEncoder)
return parsed_data
else:
with gzip.open(parsed_file, 'rt') as f:
- return json.load(f)
+ return json.load(f, cls=JSONDecoder)
diff --git a/requirements.txt b/requirements.txt
index 8598ba2..a7e9e12 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
-Flask==1.1.2
-requests==2.25.1
+Flask~=1.1.2
+Jinja2~=2.11.3
+requests~=2.25.1
+semver~=2.13.0
diff --git a/templates/badge.svg.jinja b/templates/badge.svg.jinja
index 64117b8..fe2da3f 100644
--- a/templates/badge.svg.jinja
+++ b/templates/badge.svg.jinja
@@ -1,12 +1,39 @@
+{%- set ROW_HEIGHT = 24 -%}
+{%- set ROW_MARGIN = 2 -%}
+{%- set FONT_SIZE_PERCENT = 75 -%}
+{%- set FONT_SIZE = (ROW_HEIGHT - ROW_MARGIN) * FONT_SIZE_PERCENT / 100 -%}
+{%- set ROWS = 1 + versions|length + (versions.items()|map('length')|sum) -%}
+{%- set OUTER_MARGIN = 5 -%}
+{%- set REPO_LEFT_MARGIN = 15 -%}
+{%- set FONT_WIDENESS = 0.5 -%}
+{# ugh. just ugh. #}
+{%- set ns = namespace(longest_repo = 0, longest_version = 0) -%}
+{%- for family_contents in versions.values() -%}
+ {%- set this_longest_repo = family_contents.keys()|map('length')|max -%}
+ {%- set this_longest_version = family_contents.values()|map('string')|map('length')|max -%}
+ {%- set ns.longest_repo = [ns.longest_repo, this_longest_repo]|max -%}
+ {%- set ns.longest_version = [ns.longest_version, this_longest_version]|max -%}
+{%- endfor -%}
+{%- set VERSION_X = OUTER_MARGIN + REPO_LEFT_MARGIN + ns.longest_repo * FONT_WIDENESS * FONT_SIZE -%}
+{%- set VERSION_WIDTH = OUTER_MARGIN + ns.longest_version * FONT_WIDENESS * FONT_SIZE -%}
+{%- set WIDTH = VERSION_X + VERSION_WIDTH -%}
+{%- set ns = namespace(y = 0) -%}
+{%- macro y(offset=0) -%}{{ ns.y + offset }}{%- endmacro -%}
+{%- macro texty() -%}{{ y(FONT_SIZE) }}{%- endmacro -%}
+{%- macro end_row() -%}{%- set ns.y = ns.y + ROW_HEIGHT %}{%- endmacro -%}
<?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>
+<svg width="{{ WIDTH }}" height="{{ ROWS * ROW_HEIGHT }}" xmlns="http://www.w3.org/2000/svg" fill="white" font-size="{{ FONT_SIZE }}">
+ <rect x="0" y="0" width="100%" height="100%" fill="black"></rect>
+ <text x="5" y="{{ texty() }}" fill="white">Packaging Status</text>
+ <line x1="0" y1="{{ y(ROW_HEIGHT - ROW_MARGIN / 2) }}" x2="{{ WIDTH }}" y2="{{ y(ROW_HEIGHT - ROW_MARGIN / 2) }}"
+ stroke="white" stroke-width="{{ ROW_MARGIN }}"></line>{{ end_row() }}
{% for family, family_contents in versions.items() %}
- <text x="10" y="{{ ns.y }}{% set ns.y = ns.y + 20 %}">{{ family }}</text>
+ <text x="{{ OUTER_MARGIN }}" y="{{ texty() }}">{{ family }}</text>{{ end_row() }}
{% 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>
+ <text x="{{ REPO_LEFT_MARGIN }}" y="{{ texty() }}">{{ repo }}</text>
+ <rect x="{{ VERSION_X }}" y="{{ y() }}" width="{{ VERSION_WIDTH }}" height="{{ ROW_HEIGHT }}"
+ fill="{% if version < newest_version %}red{% else %}green{% endif %}"></rect>
+ <text x="{{ VERSION_X + OUTER_MARGIN }}" y="{{ texty() }}">{{ version }}</text>{{ end_row() }}
{% endfor %}
{% endfor %}
</svg>