diff --git a/bcloud/__init__.py b/bcloud/__init__.py new file mode 100644 index 0000000..cb267ec --- /dev/null +++ b/bcloud/__init__.py @@ -0,0 +1 @@ +"""Blender Cloud server.""" diff --git a/bcloud/attract/__init__.py b/bcloud/attract/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bcloud/attract/cli.py b/bcloud/attract/cli.py new file mode 100644 index 0000000..07db1b2 --- /dev/null +++ b/bcloud/attract/cli.py @@ -0,0 +1,107 @@ +"""Commandline interface for Attract.""" + +from __future__ import print_function, division + +import copy +import logging + +from bson import ObjectId +from eve.methods.put import put_internal +from flask import current_app +from pillar.cli import manager + +log = logging.getLogger(__name__) + + +def _get_project(project_url): + """Find a project in the database, or SystemExit()s. + + :param project_url: UUID of the project + :type: str + :return: the project + :rtype: dict + """ + + projects_collection = current_app.data.driver.db['projects'] + + # Find the project in the database. + project = projects_collection.find_one({'url': project_url}) + if not project: + log.error('Project %s does not exist.', project_url) + raise SystemExit() + + return project + + +def _update_project(project): + """Updates a project in the database, or SystemExit()s. + + :param project: the project data, should be the entire project document + :type: dict + :return: the project + :rtype: dict + """ + + from pillar.api.utils import remove_private_keys + + project_id = ObjectId(project['_id']) + project = remove_private_keys(project) + result, _, _, status_code = put_internal('projects', project, _id=project_id) + + if status_code != 200: + log.error("Can't update project %s, issues: %s", project_id, result['_issues']) + raise SystemExit() + + +@manager.command +@manager.option('-r', '--replace', dest='replace', action='store_true', default=False) +def setup_for_attract(project_url, replace=False): + """Adds Attract node types to the project. + + Use --replace to replace pre-existing Attract node types + (by default already existing Attract node types are skipped). + """ + + # TODO: move those node types into this extension. + from pillar.api.node_types.act import node_type_act + from pillar.api.node_types.scene import node_type_scene + from pillar.api.node_types.shot import node_type_shot + + # Copy permissions from the project, then give everyone with PUT + # access also DELETE access. + project = _get_project(project_url) + permissions = copy.deepcopy(project['permissions']) + + for perms in permissions.values(): + for perm in perms: + methods = set(perm['methods']) + if 'PUT' not in perm['methods']: + continue + methods.add('DELETE') + perm['methods'] = list(methods) + + # Make a copy of the node types when setting the permissions, as + # we don't want to mutate the global node type objects. + node_type_act = dict(permissions=permissions, **node_type_act) + node_type_scene = dict(permissions=permissions, **node_type_scene) + node_type_shot = dict(permissions=permissions, **node_type_shot) + + # Add the missing node types. + for node_type in (node_type_act, node_type_scene, node_type_shot): + found = [nt for nt in project['node_types'] + if nt['name'] == node_type['name']] + if found: + assert len(found) == 1, 'node type name should be unique (found %ix)' % len(found) + + # TODO: validate that the node type contains all the properties Attract needs. + if replace: + log.info('Replacing existing node type %s', node_type['name']) + project['node_types'].remove(found[0]) + else: + continue + + project['node_types'].append(node_type) + + _update_project(project) + + log.info('Project %s was updated for Attract.', project_url) diff --git a/bcloud/attract/extension.py b/bcloud/attract/extension.py new file mode 100644 index 0000000..546f94a --- /dev/null +++ b/bcloud/attract/extension.py @@ -0,0 +1,43 @@ +"""Pillar extension for Attract.""" + +from pillar.extension import PillarExtension + + +class AttractExtension(PillarExtension): + @property + def name(self): + return u'Attract' + + def flask_config(self): + """Returns extension-specific defaults for the Flask configuration. + + Use this to set sensible default values for configuration settings + introduced by the extension. + + :rtype: dict + """ + + # Just so that it registers the management commands. + from . import cli + + return {} + + def blueprints(self): + """Returns the list of top-level blueprints for the extension. + + These blueprints will be mounted at the url prefix given to + app.load_extension(). + + :rtype: list of flask.Blueprint objects. + """ + return [] + + def eve_settings(self): + """Returns extensions to the Eve settings. + + Currently only the DOMAIN key is used to insert new resources into + Eve's configuration. + + :rtype: dict + """ + return {} diff --git a/cloud.py b/cloud.py index 18722db..b9f7b9d 100755 --- a/cloud.py +++ b/cloud.py @@ -1,8 +1,12 @@ #!/usr/bin/env python from pillar import PillarServer +from bcloud.attract.extension import AttractExtension + +attract = AttractExtension() app = PillarServer('.') +app.load_extension(attract, '/attract') app.process_extensions() if __name__ == '__main__': diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9451bb5 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +"""Setup file for testing, not for packaging/distribution.""" + +import setuptools + +setuptools.setup( + name='blender-cloud', + version='1.0', + packages=setuptools.find_packages('.', exclude=['tests']), + tests_require=[ + 'pytest>=2.9.1', + 'responses>=0.5.1', + 'pytest-cov>=2.2.1', + 'mock>=2.0.0', + ], + zip_safe=False, +)