from typing import Any, Iterator import os import json import functools from shutil import make_archive from ftplib import FTP_TLS from pathlib import Path import requests from rich.console import Console PackageT = tuple[dict[str, Any], Path] ManifestT = dict[str, Any] PackageIndexT = dict[str, Any] FTP_URL = 'panoramix.gajim.org' FTP_USER = os.environ['FTP_USER'] FTP_PASS = os.environ['FTP_PASS'] REPOSITORY_FOLDER = 'plugins/master' PACKAGE_INDEX_URL = 'https://ftp.gajim.org/plugins/master/package_index.json' REPO_ROOT = Path(__file__).parent.parent BUILD_PATH = REPO_ROOT / 'build' REQUIRED_KEYS = { 'authors', 'description', 'homepage', 'name', 'platforms', 'requirements', 'short_name', 'version' } console = Console() def ftp_connection(func: Any) -> Any: @functools.wraps(func) def func_wrapper(*args: Any) -> None: ftp = FTP_TLS(FTP_URL, FTP_USER, FTP_PASS) console.print('Successfully connected to', FTP_URL) func(ftp, *args) ftp.quit() console.print('Quit') return return func_wrapper def is_manifest_valid(manifest: ManifestT) -> bool: manifest_keys = set(manifest.keys()) return REQUIRED_KEYS.issubset(manifest_keys) def download_package_index() -> ManifestT: console.print('Download package index') r = requests.get(PACKAGE_INDEX_URL) r.raise_for_status() index = r.json() return index def iter_manifests() -> Iterator[PackageT]: for path in REPO_ROOT.rglob('manifest.json'): with path.open() as f: manifest = json.load(f) yield manifest, path.parent def find_plugins_to_publish(index: PackageIndexT) -> list[PackageT]: packages_to_publish: list[PackageT] = [] for manifest, path in iter_manifests(): if not is_manifest_valid(manifest): exit('Invalid manifest found') short_name = manifest['short_name'] version = manifest['version'] try: index['plugins'][short_name][version] except KeyError: packages_to_publish.append((manifest, path)) console.print('Found package to publish:', path.stem) return packages_to_publish def get_release_zip_name(manifest: ManifestT) -> str: short_name = manifest['short_name'] version = manifest['version'] return f'{short_name}_{version}' def get_dir_list(ftp: FTP_TLS) -> set[str]: return {x[0] for x in ftp.mlsd()} def upload_file(ftp: FTP_TLS, filepath: Path) -> None: name = filepath.name console.print('Upload file', name) with open(filepath, 'rb') as f: ftp.storbinary('STOR ' + name, f) def create_release_folder(ftp: FTP_TLS, packages_to_publish: list[PackageT]) -> None: folders = {manifest['short_name'] for manifest, _ in packages_to_publish} dir_list = get_dir_list(ftp) missing_folders = folders - dir_list for folder in missing_folders: ftp.mkd(folder) @ftp_connection def deploy(ftp: FTP_TLS, packages_to_publish: list[PackageT]) -> None: ftp.cwd(REPOSITORY_FOLDER) create_release_folder(ftp, packages_to_publish) for manifest, path in packages_to_publish: package_name = manifest['short_name'] zip_name = get_release_zip_name(manifest) zip_path = BUILD_PATH / f'{zip_name}.zip' image_path = path / f'{package_name}.png' make_archive(str(BUILD_PATH / zip_name), 'zip', path) ftp.cwd(package_name) upload_file(ftp, zip_path) upload_file(ftp, image_path) ftp.cwd('..') console.print('Deployed', package_name) if __name__ == '__main__': index = download_package_index() packages_to_publish = find_plugins_to_publish(index) if not packages_to_publish: console.print('No new packages deployed') else: deploy(packages_to_publish)