Files
endoflife-date-release-data/src/common/endoflife.py
Marc Wrobel dfb113d589 [git] Refactor script (#227)
Make the script more readable, mostly by:

- using the Product and AutoConfig classes,
- removing the use of functions when unnecessary,
- a little bit of renaming and documentation.

Note that this also changed the module used for regexes in endoflife.py. The regex module is now used because the Python re module does not support identically named groups (as used in the mariadb product). The regex module being backwards-compatible with the standard re module, this should not be an issue.
2023-12-10 22:07:35 +01:00

155 lines
6.1 KiB
Python

import frontmatter
import json
import logging
import os
import regex as re # Python re module does not support identically named groups (as used in the mariadb product)
from datetime import datetime
from glob import glob
from liquid import Template
logging.basicConfig(format=logging.BASIC_FORMAT, 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 = os.environ.get("PRODUCTS_PATH", "website/products")
VERSIONS_PATH = os.environ.get("VERSIONS_PATH", "releases")
class AutoConfig:
def __init__(self, method: str, config: dict):
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]
regexes = [regex.replace("(?<", "(?P<") for regex in regexes] # convert ruby to python regex
self.version_patterns = [re.compile(regex) for regex in regexes]
def first_match(self, version: str) -> re.Match:
for pattern in self.version_patterns:
match = pattern.match(version)
if match:
return match
def render(self, match: re.Match) -> str:
return self.version_template.render(**match.groupdict())
class Product:
"""Model an endoflife.date product.
"""
def __init__(self, name: str, load_product_data: bool = False):
self.name: str = name
self.versions = {}
self.versions_path: str = f"{VERSIONS_PATH}/{name}.json"
self.product_path: str = f"{PRODUCTS_PATH}/{name}.md"
if load_product_data:
if os.path.isfile(self.product_path):
with open(self.product_path) as f:
self.product_data = frontmatter.load(f)
logging.info(f"loaded product data for {self.name} from {self.product_path}")
else:
logging.warning(f"no product data found for {self.name} at {self.product_path}")
self.product_data = None
def get_auto_configs(self, method: str) -> list[AutoConfig]:
configs = []
if "auto" in self.product_data:
for config in self.product_data["auto"]:
if method in config.keys():
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 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.name}")
else:
return # already declared
logging.info(f"adding version {version} ({date}) to {self.name}")
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:
raise ValueError(f"version {version} cannot be replaced as it does not exist for {self.name}")
logging.info(f"replacing version {version} ({self.versions[version]} -> {date}) in {self.name}")
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.name}")
return
logging.info(f"removing version {version} ({self.versions.pop(version)}) from {self.name}")
def write(self) -> None:
versions = {version: date.strftime("%Y-%m-%d") for version, date in self.versions.items()}
with open(self.versions_path, "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))
def __repr__(self) -> str:
return f"<{self.name}>"
def load_product(product_name) -> frontmatter.Post:
"""Load the product's file frontmatter.
"""
with open(f"{PRODUCTS_PATH}/{product_name}.md") as f:
return frontmatter.load(f)
def list_products(method, products_filter=None) -> dict[str, list[dict]]:
"""Return a list of products that are using the same given update method.
"""
products_with_method = {}
for product_file in glob(f"{PRODUCTS_PATH}/*.md"):
product_name = os.path.splitext(os.path.basename(product_file))[0]
if products_filter and product_name != products_filter:
continue
with open(product_file) as f:
data = frontmatter.load(f)
if "auto" in data:
configs = list(filter(
lambda config: method in config.keys(),
data["auto"]
))
if len(configs) > 0:
products_with_method[product_name] = configs
return products_with_method
def write_releases(product, releases, pathname="releases") -> None:
with open(f"{pathname}/{product}.json", "w") as f:
f.write(json.dumps(dict(
# sort by date then version (desc)
sorted(releases.items(), key=lambda x: (x[1], x[0]), reverse=True)
), indent=2))