Compare commits

..

40 Commits

Author SHA1 Message Date
3ed5f2c187 Bumped version to 1.7.4 2017-09-05 11:26:52 +02:00
0be3bf7f49 Fixed unit test, it still mocked sys.platform
We now use platform.system() to detect the platform.
2017-09-05 11:25:25 +02:00
f207e14664 Added link to changelog 2017-09-05 11:16:22 +02:00
9932003400 Fix T48852: screenshot always shows "Communicating with Blender Cloud" 2017-09-05 11:16:17 +02:00
e7035e6f0c Updated changelog 2017-09-05 11:11:31 +02:00
014a36d24e Fix T52621: class name collision upon add-on registration
This is checked since Blender 2.79.
2017-09-05 11:07:33 +02:00
068451a7aa Mark 1.7.3 as released in changelog 2017-09-05 11:06:28 +02:00
56fb1ec3df Bumped version to 1.7.3 2017-08-08 12:46:07 +02:00
e93094cb88 Default to scene frame range when no frame range is given. 2017-07-03 11:09:31 +02:00
33718a1a35 Removed test print statement 2017-07-03 11:09:00 +02:00
db82dbe730 Updated changelog 2017-07-03 09:16:01 +02:00
8d405330ee Better platform detection.
The sys.platform string is 'win32' even on 64-bit Windows. Furthermore,
we expect 'windows', not 'win32'. platform.system().lower() gives us this.
2017-07-03 09:14:27 +02:00
66ddc7b47b Fixed issue running BAM on Windows.
I found this solution in a Django bug report:
 https://code.djangoproject.com/ticket/24160
2017-07-03 09:13:49 +02:00
2fa8cb4054 Refuse to render on Flamenco before blend file is saved at least once.
The file should have a location on the filesystem before BAM can pick it up.
2017-07-03 08:41:26 +02:00
e7b5c75046 Bumped version to 1.7.2 2017-06-22 15:09:39 +02:00
1d93bd9e5e Allow reloading of the Flamenco module with F8 2017-06-22 15:08:30 +02:00
ac2d0c033c Added missing parameter to function call 2017-06-22 15:08:30 +02:00
61fa63eb1d Compatibility fixes for Blender 2.78c
Blender 2.78c is shipped with a version of the io_blend_utils module that
doesn't have a `pythonpath()` function yet, and that's bundled with an
older version of BAM. To work around this, we ship BAM as wheel, and detect
whether this version is needed to run.

As an added bonus, Blender 2.78c can now also use the file exclude filter
for Flamenco. The `bam_supports_exclude_option()` function is thus no
longer necessary.
2017-06-22 15:08:30 +02:00
7022412889 Allow Pillar server URL overriding from environment 2017-06-14 12:56:34 +02:00
b4f71745b0 Released 1.7.1 2017-06-13 14:50:56 +02:00
1d41fce1ae Updated changelog 2017-06-13 14:47:33 +02:00
e636fde4ce Bumped version to 1.7.1 2017-06-13 14:42:20 +02:00
82a9dc5226 Two-stage timeouts for Pillar calls 2017-06-13 14:39:29 +02:00
1f40915ac8 Added some debug log 2017-06-13 14:39:29 +02:00
32693c0f64 Fixed erroneous return type declaration 2017-06-13 14:39:29 +02:00
c38748eb05 Shorten URLs in debug logging 2017-06-13 14:39:29 +02:00
ac85bea111 Some asyncio tweaks. 2017-06-13 14:39:29 +02:00
7b5613ce77 Fixed issue with multiple asyncio loops on Windows.
The biggest issue was the construction of an asyncio.Semaphore() while the
default loop is alive, and then creating a new loop on win32.

I've also taken the opportunity to explicitly pass our loop to some calls,
rather than expecting them to use the correct one automagically, and added
some more explicit timeout handling to the semaphore usage.
2017-06-13 13:35:05 +02:00
ec5f317dac Bumped version to 1.7.0 2017-06-09 11:04:49 +02:00
a51f61d9b5 Added translation: path → path replacement variable 2017-06-09 10:52:56 +02:00
13bc9a89c8 Updated changelog 2017-05-03 15:33:52 +02:00
996b722813 Fixed bug where a symlinked project path caused an issue
Blender would report that the blend file wasn't in the project path, even
though it was. This was caused by resolving symlinks in the project path,
but not in the blendfile path.
2017-05-03 15:33:22 +02:00
e7f2567bfc Clear a LRU cache when (de)activating Flamenco 2017-05-03 15:32:32 +02:00
ff8e71c542 Fixed reloading after upgrading from 1.4.4. 2017-05-03 12:12:58 +02:00
543da5c8d8 Flamenco exclusion filter requires BAM 1.1.7; this is now checked
A warning is shown in the GUI when a BAM version that's too old is used
(instead of simply crashing when an exclusion filter was specified).
2017-05-02 18:48:54 +02:00
01ae0f5f54 Bumped version to 1.6.4 2017-04-21 18:16:14 +02:00
1e80446870 Added file exclusion filter for Flamenco.
A filter like "*.abc;*.mkv;*.mov" can be used to prevent certain files
from being copied to the job storage directory. Requires a Blender that is
bundled with BAM 1.1.7 or newer.
2017-04-21 18:15:59 +02:00
8d5c97931e Fixed capitalisation of label 2017-03-21 14:26:08 +01:00
1a0c00b87a Removed my name from changelog entry 2017-03-21 14:20:15 +01:00
32befc51f8 Include CHANGELOG.md as data file in distribution 2017-03-21 14:18:58 +01:00
15 changed files with 394 additions and 61 deletions

View File

@@ -1,5 +1,44 @@
# Blender Cloud changelog
## Version 1.7.4 (2017-09-05)
- Fix [T52621](https://developer.blender.org/T52621): Fixed class name collision upon add-on
registration. This is checked since Blender 2.79.
- Fix [T48852](https://developer.blender.org/T48852): Screenshot no longer shows "Communicating with
Blender Cloud".
## Version 1.7.3 (2017-08-08)
- Default to scene frame range when no frame range is given.
- Refuse to render on Flamenco before blend file is saved at least once.
- Fixed some Windows-specific issues.
## Version 1.7.2 (2017-06-22)
- Fixed compatibility with Blender 2.78c.
## Version 1.7.1 (2017-06-13)
- Fixed asyncio issues on Windows
## Version 1.7.0 (2017-06-09)
- 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)
- Added file exclusion filter for Flamenco. A filter like `*.abc;*.mkv;*.mov` can be
used to prevent certain files from being copied to the job storage directory.
Requires a Blender that is bundled with BAM 1.1.7 or newer.
## Version 1.6.3 (2017-03-21)
- Fixed bug where local project path wasn't shown for projects only set up for Flamenco
@@ -9,7 +48,7 @@
## Version 1.6.2 (2017-03-17)
- Flamenco: when opening non-existing file path, open parent instead - Sybren A. Stüvel
- Flamenco: when opening non-existing file path, open parent instead
- Fix T50954: Improve Blender Cloud add-on project selector

View File

@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 6, 3),
'version': (1, 7, 4),
'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
@@ -65,21 +65,28 @@ def register():
def reload_mod(name):
modname = '%s.%s' % (__name__, name)
module = importlib.reload(sys.modules[modname])
sys.modules[modname] = module
return module
try:
old_module = sys.modules[modname]
except KeyError:
# Wasn't loaded before -- can happen after an upgrade.
new_module = importlib.import_module(modname)
else:
new_module = importlib.reload(old_module)
sys.modules[modname] = new_module
return new_module
reload_mod('blendfile')
reload_mod('home_project')
reload_mod('utils')
blender = reload_mod('blender')
async_loop = reload_mod('async_loop')
flamenco = reload_mod('flamenco')
attract = reload_mod('attract')
texture_browser = reload_mod('texture_browser')
settings_sync = reload_mod('settings_sync')
image_sharing = reload_mod('image_sharing')
attract = reload_mod('attract')
flamenco = reload_mod('flamenco')
blender = reload_mod('blender')
else:
from . import (blender, texture_browser, async_loop, settings_sync, blendfile, home_project,
image_sharing, attract, flamenco)
@@ -88,11 +95,11 @@ def register():
async_loop.register()
flamenco.register()
attract.register()
texture_browser.register()
blender.register()
settings_sync.register()
image_sharing.register()
attract.register()
blender.register()
blender.handle_project_update()

View File

@@ -33,17 +33,12 @@ _loop_kicking_operator_running = False
def setup_asyncio_executor():
"""Sets up AsyncIO to run on a single thread.
"""Sets up AsyncIO to run properly on each platform."""
This ensures that only one Pillar HTTP call is performed at the same time. Other
calls that could be performed in parallel are queued, and thus we can
reliably cancel them.
"""
import sys
executor = concurrent.futures.ThreadPoolExecutor()
if sys.platform == 'win32':
asyncio.get_event_loop().close()
# On Windows, the default event loop is SelectorEventLoop, which does
# not support subprocesses. ProactorEventLoop should be used instead.
# Source: https://docs.python.org/3/library/asyncio-subprocess.html
@@ -51,9 +46,15 @@ def setup_asyncio_executor():
asyncio.set_event_loop(loop)
else:
loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
loop.set_default_executor(executor)
# loop.set_debug(True)
from . import pillar
# No more than this many Pillar calls should be made simultaneously
pillar.pillar_semaphore = asyncio.Semaphore(3, loop=loop)
def kick_async_loop(*args) -> bool:
"""Performs a single iteration of the asyncio event loop.

View File

@@ -173,7 +173,7 @@ class AttractPollMixin:
return attract_is_active
class ToolsPanel(AttractPollMixin, Panel):
class AttractToolsPanel(AttractPollMixin, Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
@@ -974,7 +974,7 @@ def register():
bpy.types.SEQUENCER_PT_edit.append(draw_strip_movie_meta)
bpy.utils.register_class(ToolsPanel)
bpy.utils.register_class(AttractToolsPanel)
bpy.utils.register_class(AttractShotRelink)
bpy.utils.register_class(AttractShotDelete)
bpy.utils.register_class(AttractStripUnlink)

View File

@@ -32,8 +32,7 @@ import rna_prop_ui
from . import pillar, async_loop, flamenco
from .utils import pyside_cache, redraw
PILLAR_WEB_SERVER_URL = 'https://cloud.blender.org/'
# PILLAR_WEB_SERVER_URL = 'http://pillar-web:5001/'
PILLAR_WEB_SERVER_URL = os.environ.get('BCLOUD_SERVER', 'https://cloud.blender.org/')
PILLAR_SERVER_URL = '%sapi/' % PILLAR_WEB_SERVER_URL
ADDON_NAME = 'blender_cloud'
@@ -228,6 +227,11 @@ class BlenderCloudPreferences(AddonPreferences):
default='//../')
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup)
flamenco_exclude_filter = StringProperty(
name='File Exclude Filter',
description='Filter like "*.abc;*.mkv" to prevent certain files to be packed '
'into the output directory',
default='')
# TODO: before making Flamenco public, change the defaults to something less Institute-specific.
# NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute.
@@ -418,13 +422,17 @@ class BlenderCloudPreferences(AddonPreferences):
# This is only needed when the project is set up for either Attract or Flamenco.
project_box.prop(self, 'cloud_project_local_path',
text='Local Cloud project path')
text='Local Cloud Project Path')
def draw_flamenco_buttons(self, flamenco_box, bcp: flamenco.FlamencoManagerGroup, context):
from .flamenco import bam_interface
header_row = flamenco_box.row(align=True)
header_row.label('Flamenco:', icon_value=icon('CLOUD'))
manager_box = flamenco_box.row(align=True)
manager_split = flamenco_box.split(0.32, align=True)
manager_split.label('Manager:')
manager_box = manager_split.row(align=True)
if bcp.status in {'NONE', 'IDLE'}:
if not bcp.available_managers or not bcp.manager:
@@ -432,26 +440,32 @@ class BlenderCloudPreferences(AddonPreferences):
text='Find Flamenco Managers',
icon='FILE_REFRESH')
else:
manager_box.prop(bcp, 'manager', text='Manager')
manager_box.prop(bcp, 'manager', text='')
manager_box.operator('flamenco.managers',
text='',
icon='FILE_REFRESH')
else:
manager_box.label('Fetching available managers.')
path_box = flamenco_box.row(align=True)
path_box.prop(self, 'flamenco_job_file_path')
path_split = flamenco_box.split(0.32, align=True)
path_split.label(text='Job File Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_file_path', text='')
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_file_path
job_output_box = flamenco_box.column(align=True)
path_box = job_output_box.row(align=True)
path_box.prop(self, 'flamenco_job_output_path')
path_split = job_output_box.split(0.32, align=True)
path_split.label(text='Job Output Path:')
path_box = path_split.row(align=True)
path_box.prop(self, 'flamenco_job_output_path', text='')
props = path_box.operator('flamenco.explore_file_path', text='', icon='DISK_DRIVE')
props.path = self.flamenco_job_output_path
job_output_box.prop(self, 'flamenco_exclude_filter')
job_output_box.prop(self, 'flamenco_job_output_strip_components',
text='Strip Components')
prop_split = job_output_box.split(0.32, align=True)
prop_split.label('Strip Components:')
prop_split.prop(self, 'flamenco_job_output_strip_components', text='')
from .flamenco import render_output_path

View File

@@ -20,11 +20,26 @@
The preferences are managed blender.py, the rest of the Flamenco-specific stuff is here.
"""
import functools
import logging
import os
from pathlib import Path, PurePath
import typing
if "bpy" in locals():
import importlib
try:
bam_interface = importlib.reload(bam_interface)
sdk = importlib.reload(sdk)
except NameError:
from . import bam_interface, sdk
else:
from . import bam_interface, sdk
import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
@@ -32,6 +47,7 @@ from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolPropert
from .. import async_loop, pillar
from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
@@ -136,9 +152,20 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
log = logging.getLogger('%s.FLAMENCO_OT_render' % __name__)
async def async_execute(self, context):
# Refuse to start if the file hasn't been saved. It's okay if
# it's dirty, but we do need a filename and a location.
if not os.path.exists(context.blend_data.filepath):
self.report({'ERROR'}, 'Please save your Blend file before using '
'the Blender Cloud addon.')
self.quit()
return
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 +187,28 @@ 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'
frame_range = scene.flamenco_render_frame_range.strip() or scene_frame_range(context)
settings = {'blender_cmd': '{blender}',
'chunk_size': scene.flamenco_render_fchunk_size,
'filepath': str(outfile),
'frames': scene.flamenco_render_frame_range,
'render_output': str(render_output),
'filepath': manager.replace_path(outfile),
'frames': frame_range,
'render_output': manager.replace_path(render_output),
}
# Add extra settings specific to the job type
@@ -188,7 +228,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,
@@ -287,7 +327,6 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
from datetime import datetime
from ..blender import preferences
from . import bam_interface
prefs = preferences()
@@ -300,6 +339,8 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
outdir = Path(prefs.flamenco_job_file_path) / unique_dir
outfile = outdir / filepath.name
exclusion_filter = prefs.flamenco_exclude_filter or None
try:
outdir.mkdir(parents=True)
except Exception as ex:
@@ -309,7 +350,7 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return None, []
try:
missing_sources = await bam_interface.bam_copy(filepath, outfile)
missing_sources = await bam_interface.bam_copy(filepath, outfile, exclusion_filter)
except bam_interface.CommandExecutionError as ex:
self.log.exception('Unable to execute BAM pack')
self.report({'ERROR'}, 'Unable to execute BAM pack: %s' % ex)
@@ -319,6 +360,13 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
return outfile, missing_sources
def scene_frame_range(context) -> str:
"""Returns the frame range string for the current scene."""
s = context.scene
return '%i-%i' % (s.frame_start, s.frame_end)
class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
"""Sets the scene frame range as the Flamenco render frame range."""
bl_idname = 'flamenco.scene_to_frame_range'
@@ -326,8 +374,7 @@ class FLAMENCO_OT_scene_to_frame_range(FlamencoPollMixin, Operator):
bl_description = __doc__.rstrip('.')
def execute(self, context):
s = context.scene
s.flamenco_render_frame_range = '%i-%i' % (s.frame_start, s.frame_end)
context.scene.flamenco_render_frame_range = scene_frame_range(context)
return {'FINISHED'}
@@ -343,14 +390,15 @@ class FLAMENCO_OT_copy_files(Operator,
async def async_execute(self, context):
from pathlib import Path
from . import bam_interface
from ..blender import preferences
context.window_manager.flamenco_status = 'PACKING'
exclusion_filter = preferences().flamenco_exclude_filter or None
missing_sources = await bam_interface.bam_copy(
Path(context.blend_data.filepath),
Path(preferences().flamenco_job_file_path),
exclusion_filter
)
if missing_sources:
@@ -475,7 +523,13 @@ def _render_output_path(
return None
try:
proj_rel = blend_filepath.parent.relative_to(project_path)
blend_abspath = blend_filepath.resolve().absolute()
except FileNotFoundError:
# Path.resolve() will raise a FileNotFoundError if the path doesn't exist.
return None
try:
proj_rel = blend_abspath.parent.relative_to(project_path)
except ValueError:
return None
@@ -594,6 +648,7 @@ def activate():
global flamenco_is_active
log.info('Activating Flamenco')
flamenco_is_active = True
_render_output_path.cache_clear()
def deactivate():
@@ -602,6 +657,7 @@ def deactivate():
global flamenco_is_active
log.info('Deactivating Flamenco')
flamenco_is_active = False
_render_output_path.cache_clear()
def register():

View File

@@ -14,7 +14,30 @@ class CommandExecutionError(Exception):
pass
async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[Path]:
def wheel_pythonpath_278() -> str:
"""Returns the value of a PYTHONPATH environment variable needed to run BAM from its wheel file.
Workaround for Blender 2.78c not having io_blend_utils.pythonpath()
"""
import os
from ..wheels import wheel_filename
# Find the wheel to run.
wheelpath = wheel_filename('blender_bam')
log.info('Using wheel %s to run BAM-Pack', wheelpath)
# Update the PYTHONPATH to include that wheel.
existing_pypath = os.environ.get('PYTHONPATH', '')
if existing_pypath:
return os.pathsep.join((existing_pypath, wheelpath))
return wheelpath
async def bam_copy(base_blendfile: Path, target_blendfile: Path,
exclusion_filter: str) -> typing.List[Path]:
"""Uses BAM to copy the given file and dependencies to the target blendfile.
Due to the way blendfile_pack.py is programmed/structured, we cannot import it
@@ -27,6 +50,7 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[
"""
import asyncio
import os
import shlex
import subprocess
@@ -41,12 +65,29 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path) -> typing.List[
'--mode', 'FILE',
]
if exclusion_filter:
args.extend(['--exclude', exclusion_filter])
cmd_to_log = ' '.join(shlex.quote(s) for s in args)
log.info('Executing %s', cmd_to_log)
# Workaround for Blender 2.78c not having io_blend_utils.pythonpath()
if hasattr(io_blend_utils, 'pythonpath'):
pythonpath = io_blend_utils.pythonpath()
else:
pythonpath = wheel_pythonpath_278()
env = {
'PYTHONPATH': pythonpath,
# Needed on Windows because http://bugs.python.org/issue8557
'PATH': os.environ['PATH'],
}
if 'SYSTEMROOT' in os.environ: # Windows http://bugs.python.org/issue20614
env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
proc = await asyncio.create_subprocess_exec(
*args,
env={'PYTHONPATH': io_blend_utils.pythonpath()},
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,

View File

@@ -1,9 +1,50 @@
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 platform
if self.path_replacement is None:
return []
items = self.path_replacement.to_dict().items()
def by_length(item):
return -len(item[0]), item[0]
this_platform = platform.system().lower()
return [(varname, platform_replacements[this_platform])
for varname, platform_replacements in sorted(items, key=by_length)
if this_platform in platform_replacements]
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):

View File

@@ -112,7 +112,11 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async def async_execute(self, context):
"""Entry point of the asynchronous operator."""
self.report({'INFO'}, 'Communicating with Blender Cloud')
# We don't want to influence what is included in the screen shot.
if self.target == 'SCREENSHOT':
print('Blender Cloud add-on is communicating with Blender Cloud')
else:
self.report({'INFO'}, 'Communicating with Blender Cloud')
try:
# Refresh credentials

46
blender_cloud/pillar.py Normal file → Executable file
View File

@@ -115,6 +115,12 @@ def with_existing_dir(filename: str, open_mode: str, encoding=None):
yield file_object
def _shorten(somestr: str, maxlen=40) -> str:
"""Shortens strings for logging"""
return (somestr[:maxlen - 3] + '...') if len(somestr) > maxlen else somestr
def save_as_json(pillar_resource, json_filename):
with with_existing_dir(json_filename, 'w') as outfile:
log.debug('Saving metadata to %r' % json_filename)
@@ -200,8 +206,11 @@ def pillar_api(pillar_endpoint: str = None, caching=True) -> pillarsdk.Api:
return _pillar_api[caching]
# No more than this many Pillar calls should be made simultaneously
pillar_semaphore = asyncio.Semaphore(3)
# This is an asyncio.Semaphore object, which is late-instantiated to be sure
# the asyncio loop has been created properly. On Windows we create a new one,
# which can cause this semaphore to still be linked against the old default
# loop.
pillar_semaphore = None
async def pillar_call(pillar_func, *args, caching=True, **kwargs):
@@ -214,8 +223,21 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs):
partial = functools.partial(pillar_func, *args, api=pillar_api(caching=caching), **kwargs)
loop = asyncio.get_event_loop()
async with pillar_semaphore:
# Use explicit calls to acquire() and release() so that we have more control over
# how long we wait and how we handle timeouts.
try:
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=10, loop=loop)
except asyncio.TimeoutError:
log.info('Waiting for semaphore to call %s', pillar_func.__name__)
try:
await asyncio.wait_for(pillar_semaphore.acquire(), timeout=50, loop=loop)
except asyncio.TimeoutError:
raise RuntimeError('Timeout waiting for Pillar Semaphore!')
try:
return await loop.run_in_executor(None, partial)
finally:
pillar_semaphore.release()
def sync_call(pillar_func, *args, caching=True, **kwargs):
@@ -441,9 +463,9 @@ async def download_to_file(url, filename, *,
log.debug('Downloading was cancelled before doing the GET')
raise asyncio.CancelledError('Downloading was cancelled')
log.debug('Performing GET %s', url)
log.debug('Performing GET %s', _shorten(url))
response = await loop.run_in_executor(None, perform_get_request)
log.debug('Status %i from GET %s', response.status_code, url)
log.debug('Status %i from GET %s', response.status_code, _shorten(url))
response.raise_for_status()
if response.status_code == 304:
@@ -457,9 +479,9 @@ async def download_to_file(url, filename, *,
log.debug('Downloading was cancelled before downloading the GET response')
raise asyncio.CancelledError('Downloading was cancelled')
log.debug('Downloading response of GET %s', url)
log.debug('Downloading response of GET %s', _shorten(url))
await loop.run_in_executor(None, download_loop)
log.debug('Done downloading response of GET %s', url)
log.debug('Done downloading response of GET %s', _shorten(url))
# We're done downloading, now we have something cached we can use.
log.debug('Saving header cache to %s', header_store)
@@ -534,7 +556,8 @@ async def fetch_texture_thumbs(parent_node_uuid: str, desired_size: str,
for texture_node in texture_nodes)
# raises any exception from failed handle_texture_node() calls.
await asyncio.gather(*coros)
loop = asyncio.get_event_loop()
await asyncio.gather(*coros, loop=loop)
log.info('fetch_texture_thumbs: Done downloading texture thumbnails')
@@ -747,7 +770,8 @@ async def download_texture(texture_node,
future=future)
downloaders.append(dlr)
return await asyncio.gather(*downloaders, return_exceptions=True)
loop = asyncio.get_event_loop()
return await asyncio.gather(*downloaders, return_exceptions=True, loop=loop)
async def upload_file(project_id: str, file_path: pathlib.Path, *,
@@ -773,9 +797,9 @@ async def upload_file(project_id: str, file_path: pathlib.Path, *,
log.debug('Uploading was cancelled before doing the POST')
raise asyncio.CancelledError('Uploading was cancelled')
log.debug('Performing POST %s', url)
log.debug('Performing POST %s', _shorten(url))
response = await loop.run_in_executor(None, upload)
log.debug('Status %i from POST %s', response.status_code, url)
log.debug('Status %i from POST %s', response.status_code, _shorten(url))
response.raise_for_status()
resp = response.json()

View File

@@ -457,7 +457,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
return menu_item
def update_menu_item(self, node, *args) -> MenuItem:
def update_menu_item(self, node, *args):
node_uuid = node['_id']
# Just make this thread-safe to be on the safe side.
@@ -538,6 +538,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.add_menu_item(node, None, 'SPINNER', texture_node['name'])
def thumbnail_loaded(node, file_desc, thumb_path):
self.log.debug('Node %s thumbnail loaded', node['_id'])
self.update_menu_item(node, file_desc, thumb_path)
await pillar.fetch_texture_thumbs(node_uuid, 's', directory,

View File

@@ -44,6 +44,12 @@ def load_wheel(module_name, fname_prefix):
module_name, module.__file__, fname_prefix)
return
sys.path.append(wheel_filename(fname_prefix))
module = __import__(module_name)
log.debug('Loaded %s from %s', module_name, module.__file__)
def wheel_filename(fname_prefix: str) -> str:
path_pattern = os.path.join(my_dir, '%s*.whl' % fname_prefix)
wheels = glob.glob(path_pattern)
if not wheels:
@@ -51,9 +57,7 @@ def load_wheel(module_name, fname_prefix):
# If there are multiple wheels that match, load the latest one.
wheels.sort()
sys.path.append(wheels[-1])
module = __import__(module_name)
log.debug('Loaded %s from %s', module_name, module.__file__)
return wheels[-1]
def load_wheels():

View File

@@ -3,6 +3,7 @@
lockfile==0.12.2
pillarsdk==1.6.1
wheel==0.29.0
blender-bam==1.1.7
# Secondary requirements:
cffi==1.6.0

View File

@@ -101,6 +101,11 @@ class BuildWheels(Command):
log.info('Downloading Pillar Python SDK wheel')
self.download_wheel(requirements['pillarsdk'])
# Download BAM from pypi. This is required for compatibility with Blender 2.78.
if not list(self.wheels_path.glob('blender_bam*.whl')):
log.info('Downloading BAM wheel')
self.download_wheel(requirements['blender-bam'])
# Build CacheControl.
if not list(self.wheels_path.glob('CacheControl*.whl')):
log.info('Building CacheControl in %s', self.cachecontrol_path)
@@ -227,11 +232,11 @@ setup(
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.6.3',
version='1.7.4',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),
data_files=[('blender_cloud', ['README.md', 'README-flamenco.md']),
data_files=[('blender_cloud', ['README.md', 'README-flamenco.md', 'CHANGELOG.md']),
('blender_cloud/icons', glob.glob('blender_cloud/icons/*'))],
scripts=[],
url='https://developer.blender.org/diffusion/BCA/',

View File

@@ -0,0 +1,95 @@
"""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': '<script>alert("this is a manager")</script>',
'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
def mocked_system():
return platform
with unittest.mock.patch('platform.system', mocked_system):
for expected_result, input_path in test_paths:
as_path_instance = pathclass(input_path)
self.assertEqual(expected_result,
self.test_manager.replace_path(as_path_instance),
'for input %r on platform %s' % (as_path_instance, platform))