#!/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 from collections import MutableMapping log = logging.getLogger(__name__) REQUIRED_KEYS = set(['name', 'blender', 'version']) SCHEMA_VERSION = 1 class BadAddon(Exception): """ Raised when something expected to be an addon turns out not to be """ class RepositoryListError(ValueError): """ Raised when something is amiss with the repo.json file """ class RepositoryNotFoundError(RepositoryListError): """ Raised when looking for a repository which isn't there """ class Package: """ Stores package methods and metadata """ def __init__(self, package_dict:dict = None): self.from_dict(package_dict) def to_dict(self) -> dict: """ Return a dict representation of the package """ return { 'bl_info': self.bl_info, 'url': self.url, } def from_dict(self, package_dict: dict): """ Get attributes from a dict such as produced by `to_dict` """ if package_dict is None: package_dict = {} for attr in ('name', 'url', 'bl_info'): setattr(self, attr, package_dict.get(attr)) class Repository: """ Stores repository metadata (including packages) """ def __init__(self, repo_dict:dict = None): self.from_dict(repo_dict) def refresh(self): """ Requests repo.json from URL and embeds etag/last-modification headers """ req_headers = {} # Do things this way to avoid adding empty objects/None to the req_headers dict if 'etag' in self._headers: req_headers['If-None-Match'] = self._headers['etag'] if 'last-modified' in self._headers: req_headers['If-Modified-Since'] = self._headers['last-modified'] #try resp = requests.get(self.url, headers=req_headers) repodict = json.loads(resp.json()) repodict['etag'] = resp.headers.get('etag') repodict['last-modified'] = resp.headers.get('last-modified') self.from_dict(repodict) def to_dict(self) -> dict: """ Return a dict representation of the repository """ return { 'name': self.name, 'packages': [p.to_dict() for p in self.packages], 'url': self.url, '_headers': self._headers, } def from_dict(self, repodict: dict): """ Get attributes from a dict such as produced by `to_dict` """ if repodict is None: repodict = {} for attr in ('name', 'url', 'packages', '_headers'): if attr == 'package': value = set(Package(pkg) for pkg in repodict.get('packages', [])) else: value = repodict.get(attr) setattr(self, attr, value) def dump(self, path: pathlib.Path): """ Dump repository as a repo.json file in 'path' """ with (path / 'repo.json').open('w', encoding='utf-8') as repo_file: json.dump(self.to_dict(), repo_file, indent=4, sort_keys=True) log.info("repo.json written to %s" % path) # def fetch_repo(url: str, repo: Repository) -> dict: def refresh(url): # 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: fetch_repojson(url) pipe[1].send(re.status_code) 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 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()