diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f4f40..e9fc62e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Blender Cloud changelog -## Version 1.6.4 (in development) +## Version 1.6.5 (in development) - Fixed reloading after upgrading from 1.4.4 (our last public release). - Fixed bug handling a symlinked project path. +- Added support for Manager-defined path replacement variables. ## Version 1.6.4 (2017-04-21) diff --git a/blender_cloud/flamenco/__init__.py b/blender_cloud/flamenco/__init__.py index a4614d1..336d188 100644 --- a/blender_cloud/flamenco/__init__.py +++ b/blender_cloud/flamenco/__init__.py @@ -139,6 +139,9 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, if not await self.authenticate(context): return + import pillarsdk.exceptions + from .sdk import Manager + from ..pillar import pillar_call from ..blender import preferences scene = context.scene @@ -160,15 +163,26 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, if not outfile: return - # Create the job at Flamenco Server. + # Fetch Manager for doing path replacement. + self.log.info('Going to fetch manager %s', self.user_id) prefs = preferences() + manager_id = prefs.flamenco_manager.manager + try: + manager = await pillar_call(Manager.find, manager_id) + except pillarsdk.exceptions.ResourceNotFound: + self.report({'ERROR'}, 'Manager %s not found, refresh your managers in ' + 'the Blender Cloud add-on settings.' % manager_id) + self.quit() + return + + # Create the job at Flamenco Server. context.window_manager.flamenco_status = 'COMMUNICATING' settings = {'blender_cmd': '{blender}', 'chunk_size': scene.flamenco_render_fchunk_size, - 'filepath': str(outfile), + 'filepath': manager.replace_path(outfile), 'frames': scene.flamenco_render_frame_range, - 'render_output': str(render_output), + 'render_output': manager.replace_path(render_output), } # Add extra settings specific to the job type @@ -188,7 +202,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin, try: job_info = await create_job(self.user_id, prefs.project.project, - prefs.flamenco_manager.manager, + manager_id, scene.flamenco_render_job_type, settings, 'Render %s' % filepath.name, diff --git a/blender_cloud/flamenco/sdk.py b/blender_cloud/flamenco/sdk.py index 33f2308..275ffe8 100644 --- a/blender_cloud/flamenco/sdk.py +++ b/blender_cloud/flamenco/sdk.py @@ -1,9 +1,51 @@ +import functools +import pathlib + from pillarsdk.resource import List, Find, Create class Manager(List, Find): """Manager class wrapping the REST nodes endpoint""" path = 'flamenco/managers' + PurePlatformPath = pathlib.PurePath + + @functools.lru_cache() + def _sorted_path_replacements(self) -> list: + import sys + + if self.path_replacement is None: + return [] + + print('SORTING PATH REPLACEMENTS') + + items = self.path_replacement.to_dict().items() + + def by_length(item): + return -len(item[0]), item[0] + + platform = sys.platform + return [(varname, platform_replacements[platform]) + for varname, platform_replacements in sorted(items, key=by_length)] + + def replace_path(self, some_path: pathlib.PurePath) -> str: + """Performs path variable replacement. + + Tries to find platform-specific path prefixes, and replaces them with + variables. + """ + + for varname, path in self._sorted_path_replacements(): + replacement = self.PurePlatformPath(path) + try: + relpath = some_path.relative_to(replacement) + except ValueError: + # Not relative to each other, so no replacement possible + continue + + replacement_root = self.PurePlatformPath('{%s}' % varname) + return (replacement_root / relpath).as_posix() + + return some_path.as_posix() class Job(List, Find, Create): diff --git a/tests/test_path_replacement.py b/tests/test_path_replacement.py new file mode 100644 index 0000000..4064652 --- /dev/null +++ b/tests/test_path_replacement.py @@ -0,0 +1,90 @@ +"""Unittests for blender_cloud.utils. + +This unittest requires bpy to be importable, so build Blender as a module and install it +into your virtualenv. See https://stuvel.eu/files/bconf2016/#/10 for notes how. +""" + +import datetime +import pathlib +import unittest.mock + +import pillarsdk.utils + +from blender_cloud.flamenco import sdk + + +class PathReplacementTest(unittest.TestCase): + def setUp(self): + self.test_manager = sdk.Manager({ + '_created': datetime.datetime(2017, 5, 31, 15, 12, 32, tzinfo=pillarsdk.utils.utc), + '_etag': 'c39942ee4bcc4658adcc21e4bcdfb0ae', + '_id': '592edd609837732a2a272c62', + '_updated': datetime.datetime(2017, 6, 8, 14, 51, 3, tzinfo=pillarsdk.utils.utc), + 'description': 'Manager formerly known as "testman"', + 'job_types': {'sleep': {'vars': {}}}, + 'name': '', + 'owner': '592edd609837732a2a272c63', + 'path_replacement': {'job_storage': {'darwin': '/Volume/shared', + 'linux': '/shared', + 'windows': 's:/'}, + 'render': {'darwin': '/Volume/render/', + 'linux': '/render/', + 'windows': 'r:/'}, + 'longrender': {'darwin': '/Volume/render/long', + 'linux': '/render/long', + 'windows': 'r:/long'}, + }, + 'projects': ['58cbdd5698377322d95eb55e'], + 'service_account': '592edd609837732a2a272c60', + 'stats': {'nr_of_workers': 3}, + 'url': 'http://192.168.3.101:8083/', + 'user_groups': ['58cbdd5698377322d95eb55f'], + 'variables': {'blender': {'darwin': '/opt/myblenderbuild/blender', + 'linux': '/home/sybren/workspace/build_linux/bin/blender ' + '--enable-new-depsgraph --factory-startup', + 'windows': 'c:/temp/blender.exe'}}} + ) + + def test_linux(self): + # (expected result, input) + test_paths = [ + ('/doesnotexistreally', '/doesnotexistreally'), + ('{render}/agent327/scenes/A_01_03_B', '/render/agent327/scenes/A_01_03_B'), + ('{job_storage}/render/agent327/scenes', '/shared/render/agent327/scenes'), + ('{longrender}/agent327/scenes', '/render/long/agent327/scenes'), + ] + + self._do_test(test_paths, 'linux', pathlib.PurePosixPath) + + def test_windows(self): + # (expected result, input) + test_paths = [ + ('c:/doesnotexistreally', 'c:/doesnotexistreally'), + ('c:/some/path', r'c:\some\path'), + ('{render}/agent327/scenes/A_01_03_B', r'R:\agent327\scenes\A_01_03_B'), + ('{render}/agent327/scenes/A_01_03_B', r'r:\agent327\scenes\A_01_03_B'), + ('{render}/agent327/scenes/A_01_03_B', r'r:/agent327/scenes/A_01_03_B'), + ('{job_storage}/render/agent327/scenes', 's:/render/agent327/scenes'), + ('{longrender}/agent327/scenes', 'r:/long/agent327/scenes'), + ] + + self._do_test(test_paths, 'windows', pathlib.PureWindowsPath) + + def test_darwin(self): + # (expected result, input) + test_paths = [ + ('/Volume/doesnotexistreally', '/Volume/doesnotexistreally'), + ('{render}/agent327/scenes/A_01_03_B', r'/Volume/render/agent327/scenes/A_01_03_B'), + ('{job_storage}/render/agent327/scenes', '/Volume/shared/render/agent327/scenes'), + ('{longrender}/agent327/scenes', '/Volume/render/long/agent327/scenes'), + ] + + self._do_test(test_paths, 'darwin', pathlib.PurePosixPath) + + def _do_test(self, test_paths, platform, pathclass): + self.test_manager.PurePlatformPath = pathclass + with unittest.mock.patch('sys.platform', platform): + for expected_result, input_path in test_paths: + self.assertEqual(expected_result, + self.test_manager.replace_path(pathclass(input_path)), + 'for input %s on platform %s' % (input_path, platform))