#!/usr/bin/env python3 # HACK: seems 'requests' module bundled with blender isn't bundled with 'idna' module. So force system python for now import sys sys.path.insert(0, '/usr/lib/python3.6/site-packages') import requests import json import os import pathlib import ast import argparse import zipfile import logging log = logging.getLogger(__name__) REQUIRED_KEYS = set(['name', 'blender', 'version']) SCHEMA_VERSION = 1 class BadAddon(Exception): pass def fetch(url, pipe): # we have to explicitly close the end of the pipe we are NOT using, # otherwise no exception will be generated when the other process closes its end. pipe[0].close() local_repo_path = pathlib.Path(__file__).parent / 'packages' local_repo_path.mkdir(exist_ok=True) try: # TODO: do conditional request re = requests.get(url) pipe[1].send(re.status_code) repo = re.json() repo['etag'] = re.headers.get('etag') repo['last-modified'] = re.headers.get('last-modified') # just stick it here for now.. dump_repo(local_repo_path, repo) finally: pipe[1].close() # class Package: # def __init__(self): # self.bl_info = None # self.url = None # self.type = None # # class Repository: # def __init__(self, name): # self.name = name # self.packages = None # # def json(self): # return json.dumps(self.__dict__) def parse_blinfo(source: str) -> dict: """Parse a python file and return its bl_info dict, if there is one (else return None)""" tree = ast.parse(source) for body in tree.body: if body.__class__ != ast.Assign: continue if len(body.targets) != 1: continue if getattr(body.targets[0], 'id', '') != 'bl_info': continue return ast.literal_eval(body.value) raise BadAddon('No bl_info found') def extract_blinfo(path: pathlib.Path) -> dict: """Extract bl_info dict from addon at path (can be single file, module, or zip)""" source = None # get last component of path addon_name = path.name if path.is_dir(): with open(path / '__init__.py', 'r') as f: source = f.read() else: # HACK: perhaps not the best approach determining filetype..? try: with zipfile.ZipFile(str(path), 'r') as z: for fname in z.namelist(): # HACK: this seems potentially fragile; depends on zipfile listing root contents first if fname.endswith('__init__.py'): source = z.read(fname) break except zipfile.BadZipFile: with path.open() as f: source = f.read() if source == None: raise RuntimeError("Could not read addon '%s'" % addon_name) return parse_blinfo(source) def make_repo(repopath: pathlib.Path): """Make repo.json for files in directory 'repopath'""" repo_data = {} package_data = [] if not repopath.is_dir(): raise FileNotFoundError(repopath) for addon_path in repopath.iterdir(): package_datum = {} addon = addon_path.name try: bl_info = extract_blinfo(addon_path) except BadAddon as err: log.warning('Could not extract bl_info from {}: {}'.format(addon_path, err)) continue if not REQUIRED_KEYS.issubset(set(bl_info)): log.warning( "Required key(s) '{}' not found in bl_info of '{}'".format( "', '".join(REQUIRED_KEYS.difference(set(bl_info))), addon) ) package_datum['bl_info'] = bl_info package_datum['type'] = 'addon' package_data.append(package_datum) repo_data['packages'] = package_data dump_repo(repopath, repo_data) def dump_repo(repo_path: pathlib.Path, repo_data: dict): with (repo_path / 'repo.json').open('w', encoding='utf-8') as repo_file: json.dump(repo_data, repo_file, indent=4, sort_keys=True) def main(): pass # print(args) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Search, install, and manage packages for Blender') subparsers = parser.add_subparsers() make = subparsers.add_parser('make') make.add_argument('path') make.set_defaults(func=lambda args: make_repo(pathlib.Path(args.path))) args = parser.parse_args() args.func(args) main()