Init
This commit is contained in:
45
src/build.py
Normal file
45
src/build.py
Normal 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
51
src/inventory.py
Normal 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
50
src/sources/__init__.py
Normal 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
128
src/sources/gitea.py
Normal 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
24
src/sources/plain.py
Normal 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
29
src/sources/util.py
Normal 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
|
||||
Reference in New Issue
Block a user