168 lines
4.4 KiB
Python
Executable File
168 lines
4.4 KiB
Python
Executable File
#!/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()
|
|
|