Files
endoflife-date-release-data/src/common/endoflife.py
Marc Wrobel f6a8349c46 Centralize GitHub Workflow groups declaration (#272)
It may not be the best place for that (gha.py would have been better), but it's the shorter / faster way to do it for now.

Moreover it now uses logging for writing the group. The logger format has been updated for this to work. This was done to fix issues on GitHub Action logs, where groups were declared after the logs.
2023-12-31 17:00:11 +01:00

165 lines
6.2 KiB
Python

import json
import logging
import os
import re
from datetime import datetime, timezone
from pathlib import Path
import frontmatter
from liquid import Template
# Do not update the format: it's also used to declare groups in the GitHub Actions logs.
logging.basicConfig(format="%(message)s", level=logging.INFO)
# Handle versions having at least 2 digits (ex. 1.2) and at most 4 digits (ex. 1.2.3.4), with an optional leading "v".
# Major version must be >= 1.
DEFAULT_VERSION_REGEX = r"^v?(?P<major>[1-9]\d*)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\.(?P<tiny>\d+))?)?$"
DEFAULT_VERSION_PATTERN = re.compile(DEFAULT_VERSION_REGEX)
DEFAULT_VERSION_TEMPLATE = "{{major}}{% if minor %}.{{minor}}{% if patch %}.{{patch}}{% if tiny %}.{{tiny}}{% endif %}{% endif %}{% endif %}"
PRODUCTS_PATH = Path(os.environ.get("PRODUCTS_PATH", "website/products"))
VERSIONS_PATH = Path(os.environ.get("VERSIONS_PATH", "releases"))
class AutoConfig:
def __init__(self, method: str, config: dict) -> None:
self.method = method
self.url = config[method]
self.version_template = Template(config.get("template", DEFAULT_VERSION_TEMPLATE))
regexes = config.get("regex", DEFAULT_VERSION_REGEX)
regexes = regexes if isinstance(regexes, list) else [regexes]
self.version_patterns = [re.compile(regex) for regex in regexes]
def first_match(self, version: str) -> re.Match | None:
for pattern in self.version_patterns:
match = pattern.match(version)
if match:
return match
return None
def render(self, match: re.Match) -> str:
return self.version_template.render(**match.groupdict())
class ProductFrontmatter:
def __init__(self, name: str) -> None:
self.name: str = name
self.path: Path = PRODUCTS_PATH / f"{name}.md"
self.data = None
if self.path.is_file():
with self.path.open() as f:
self.data = frontmatter.load(f)
logging.info(f"loaded product data for {self.name} from {self.path}")
else:
logging.warning(f"no product data found for {self.name} at {self.path}")
def get_auto_configs(self, method: str) -> list[AutoConfig]:
configs = []
if "auto" in self.data:
for config in self.data["auto"]:
if method in config:
configs.append(AutoConfig(method, config))
else:
logging.error(f"mixed auto-update methods declared for {self.name}, this is not yet supported")
return configs
def get_release_date(self, release_cycle: str) -> datetime | None:
for release in self.data["releases"]:
if release["releaseCycle"] == release_cycle:
return release["releaseDate"]
return None
class Product:
def __init__(self, name: str) -> None:
self.name: str = name
self.versions_path: Path = VERSIONS_PATH / f"{name}.json"
self.versions = {}
logging.info(f"::group::{self}")
@staticmethod
def from_file(name: str) -> "Product":
product = Product(name)
if product.versions_path.is_file():
with product.versions_path.open() as f:
for version, date in json.load(f).items():
date_obj = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
product.versions[version] = date_obj
logging.info(f"loaded versions data for {product} from {product.versions_path}")
else:
logging.warning(f"no versions data found for {product} at {product.versions_path}")
return product
def has_version(self, version: str) -> bool:
return version in self.versions
def get_version_date(self, version: str) -> datetime:
return self.versions[version] if version in self.versions else None
def declare_version(self, version: str, date: datetime) -> None:
if version in self.versions:
if self.versions[version] != date:
logging.warning(f"overwriting version {version} ({self.versions[version]} -> {date}) for {self}")
else:
return # already declared
logging.info(f"adding version {version} ({date}) to {self}")
self.versions[version] = date
def declare_versions(self, dates_by_version: dict[str, datetime]) -> None:
for (version, date) in dates_by_version.items():
self.declare_version(version, date)
def replace_version(self, version: str, date: datetime) -> None:
if version not in self.versions:
msg = f"version {version} cannot be replaced as it does not exist for {self}"
raise ValueError(msg)
logging.info(f"replacing version {version} ({self.versions[version]} -> {date}) in {self}")
self.versions[version] = date
def remove_version(self, version: str) -> None:
if not self.has_version(version):
logging.warning(f"version {version} cannot be removed as it does not exist for {self}")
return
logging.info(f"removing version {version} ({self.versions.pop(version)}) from {self}")
def write(self) -> None:
versions = {version: date.strftime("%Y-%m-%d") for version, date in self.versions.items()}
with self.versions_path.open("w") as f:
f.write(json.dumps(dict(
# sort by date then version (desc)
sorted(versions.items(), key=lambda x: (x[1], x[0]), reverse=True),
), indent=2))
logging.info("::endgroup::")
def __repr__(self) -> str:
return self.name
def list_products(method: str, products_filter: str = None) -> list[str]:
"""Return a list of products that are using the same given update method.
"""
products = []
for product_file in PRODUCTS_PATH.glob("*.md"):
product_name = product_file.stem
if products_filter and product_name != products_filter:
continue
with product_file.open() as f:
data = frontmatter.load(f)
if "auto" in data:
matching_configs = list(filter(lambda config: method in config, data["auto"]))
if len(matching_configs) > 0:
products.append(product_name)
return products