This commit is contained in:
alexander
2026-01-22 21:14:16 +01:00
parent 8a26344e20
commit 24d0a9216a
21 changed files with 778 additions and 0 deletions

45
src/build.py Normal file
View File

@@ -0,0 +1,45 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
import shutil
import datetime
import os
from sources import CSItem
from inventory import load_cheatsheet_inventory, prepare_cheatsheets
INVENTORY_FILE = "cheatsheet_inventory.json"
STATIC_DIR = "static"
TEMPLATES_DIR = "templates"
OUTPUT_DIR = "out"
inv_raw = load_cheatsheet_inventory(INVENTORY_FILE)
# Clear output directory
shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
shutil.copytree(STATIC_DIR, OUTPUT_DIR)
inv: list[CSItem] = prepare_cheatsheets(inv_raw, OUTPUT_DIR)
env = Environment(
loader=FileSystemLoader(TEMPLATES_DIR),
autoescape=select_autoescape()
)
index = env.get_template("index.html.j2")
print(f"{len(inv)} Cheatsheets")
for i in inv:
print("-", i)
thisYear = datetime.datetime.now().year
with open(f"{OUTPUT_DIR}/index.html", "w", encoding="utf-8") as f:
f.write(index.render(items=inv, thisYear=thisYear))
with open(f"{OUTPUT_DIR}/impressum.html", "w", encoding="utf-8") as f:
f.write(env.get_template("impressum.html.j2").render(thisYear=thisYear))
with open(f"{OUTPUT_DIR}/license.html", "w", encoding="utf-8") as f:
f.write(env.get_template("license.html.j2").render(thisYear=thisYear))

51
src/inventory.py Normal file
View File

@@ -0,0 +1,51 @@
import os
import traceback
from sources import CSInventoryConfig, CSItem, CheatsheetSourceType
from sources.plain import process_plain_url
from sources.gitea import process_gitea
def load_cheatsheet_inventory(file: str) -> CSInventoryConfig:
if not os.path.exists(file):
res = CSInventoryConfig(items=[])
else:
res = CSInventoryConfig.model_validate_json(
open(file, "r", encoding="utf-8").read()
)
with open(file, "w", encoding="utf-8") as f:
f.write(res.model_dump_json(indent=4))
return res
def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]:
res: list[CSItem] = []
for item in config.items:
new_items = []
try:
match item.source.type:
case CheatsheetSourceType.GITEA_SOURCE:
new_items += process_gitea(item, outdir)
case CheatsheetSourceType.PLAIN_URL:
new_items.append(process_plain_url(item, outdir))
case _:
print("Unknow Source Type:", item.source.type)
except:
traceback.print_exc()
new_item = None
if new_items:
for new_item in new_items:
print("->", new_item)
res.append(new_item)
return res

50
src/sources/__init__.py Normal file
View File

@@ -0,0 +1,50 @@
from enum import Enum
from typing import Literal
from pydantic import BaseModel, Field
from uuid import uuid4
# Configuration
class CheatsheetSourceType(str, Enum):
GITEA_SOURCE = "gitea"
PLAIN_URL = "url"
class CSSourceBase(BaseModel):
type: CheatsheetSourceType
class CSSourceGitea(CSSourceBase):
type: Literal[CheatsheetSourceType.GITEA_SOURCE]
base_url: str
repo: str
owner: str
tag: str
hide_repo: bool = False
class CSSourcePlainURL(CSSourceBase):
type: Literal[CheatsheetSourceType.PLAIN_URL]
url: str
repo_url: str
CSSourceType = CSSourcePlainURL | CSSourceGitea
class CSInventoryItem(BaseModel):
source: CSSourceType
cache: bool
id: str = Field(default_factory=lambda: uuid4().__str__())
title: str
author: str | None
class CSInventoryConfig(BaseModel):
items: list[CSInventoryItem]
# Resulting Rendering icon
class CSItem(BaseModel):
url: str
date: str
commit: str = "N/A"
author: str
title: str
id: str
git_repo: str
git_repo_type: str

128
src/sources/gitea.py Normal file
View File

@@ -0,0 +1,128 @@
from sources import CSSourceGitea, CSItem, CSInventoryItem
from sources.util import cache_cheatsheet, get_datestring
import requests
from pathlib import Path
def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None:
source: CSSourceGitea = item.source
commit_hash = get_release_commit_sha(source.base_url, source.owner, source.repo, source.tag)
asserts = list_release_assets(source.base_url, source.owner, source.repo, source.tag)
asserts = filter(lambda a: a[1].endswith(".pdf"), asserts)
res = []
for a in asserts:
res_url = a[0]
if item.cache:
cache_url = cache_cheatsheet(a[0], outdir)
if cache_url:
res_url = cache_url
else:
continue
name = Path(a[1]).stem
res.append(CSItem(
url = res_url,
date=get_datestring(),
commit=commit_hash[:10] if commit_hash else "",
author=item.author if item.author else source.owner,
title=f"{name}",
id=item.id,
git_repo=f"{source.base_url}/{source.owner}/{source.repo}" if not source.hide_repo else "",
git_repo_type="Gitea"
))
return res
def get_release_commit_sha(base_url, owner, repo, tag_name, token=None):
"""
Resolve the commit SHA for a Gitea release tag.
:param base_url: e.g. "https://gitea.example.com"
:param owner: repo owner
:param repo: repository name
:param tag_name: release tag (e.g. "v1.2.3")
:param token: optional API token
:return: commit SHA (str)
"""
headers = {}
if token:
headers["Authorization"] = f"token {token}"
session = requests.Session()
session.headers.update(headers)
# 1) List tags and find the matching tag
tags_url = f"{base_url}/api/v1/repos/{owner}/{repo}/tags"
resp = session.get(tags_url)
resp.raise_for_status()
tags = resp.json()
tag = next((t for t in tags if t["name"] == tag_name), None)
if not tag:
raise ValueError(f"Tag '{tag_name}' not found")
# Lightweight tags usually already contain the commit SHA
commit_sha = tag.get("commit", {}).get("sha")
tag_obj_sha = tag.get("id")
# If commit.sha looks valid, return it
if commit_sha:
return commit_sha
# 2) Annotated tag: dereference via /git/tags/{sha}
if not tag_obj_sha:
raise RuntimeError("Tag object SHA missing; cannot dereference annotated tag")
git_tag_url = f"{base_url}/api/v1/repos/{owner}/{repo}/git/tags/{tag_obj_sha}"
resp = session.get(git_tag_url)
resp.raise_for_status()
annotated = resp.json()
# The object pointed to by the tag (usually a commit)
target = annotated.get("object", {})
if target.get("type") != "commit":
raise RuntimeError(f"Tag points to a {target.get('type')} instead of a commit")
return target.get("sha")
def list_release_assets(base_url, owner, repo, tag, token=None):
"""
Return a list of (download_url, filename) for all assets of a Gitea release.
:param base_url: Gitea host URL, e.g. "https://gitea.example.com"
:param owner: repository owner
:param repo: repository name
:param tag: release tag name
:param token: optional API token
:returns: list of (download_url, filename) tuples
"""
headers = {}
if token:
headers["Authorization"] = f"token {token}"
# 1) Get release by tag
rel_url = f"{base_url}/api/v1/repos/{owner}/{repo}/releases/tags/{tag}"
rel_resp = requests.get(rel_url, headers=headers)
rel_resp.raise_for_status()
release = rel_resp.json()
assets = release.get("assets", [])
result = []
for asset in assets:
# Gitea asset info usually contains:
# - "browser_download_url" → direct URL
# - "name" → filename
download_url = asset.get("browser_download_url")
filename = asset.get("name")
if download_url and filename:
result.append((download_url, filename))
return result

24
src/sources/plain.py Normal file
View File

@@ -0,0 +1,24 @@
from sources import CSInventoryItem, CSSourcePlainURL, CSItem
from sources.util import cache_cheatsheet, get_datestring
def process_plain_url(item: CSInventoryItem, outdir: str) -> CSItem | None:
source: CSSourcePlainURL = item.source
res_url = source.url
if item.cache:
cache_url = cache_cheatsheet(source.url, outdir)
if cache_url:
res_url = cache_url
else:
return None
return CSItem(
url = res_url,
date=get_datestring(),
config=item,
author=item.author,
title=item.title,
id=item.id,
git_repo=source.repo_url,
git_repo_type="Git"
)

29
src/sources/util.py Normal file
View File

@@ -0,0 +1,29 @@
import hashlib
import requests
import datetime
import os
def get_datestring() -> str:
return datetime.datetime.now().strftime("%d.%m.%y")
def cache_cheatsheet(url, outdir: str) -> str | None:
r = requests.get(url)
if not r.ok and r.headers.get("Content-Type") != "application/pdf":
return None
data = r.content
hashdata = hashlib.sha256(data)
filesname = os.path.join("cache", f"{hashdata.hexdigest()}.pdf")
if not os.path.exists(os.path.join(outdir, "cache")):
os.mkdir(os.path.join(outdir, "cache"))
with open(os.path.join(outdir, filesname), "wb") as f:
f.write(data)
print("Saved file to", filesname)
return filesname