Compare commits
14 Commits
version-1.
...
version-1.
Author | SHA1 | Date | |
---|---|---|---|
e7b5c75046 | |||
1d93bd9e5e | |||
ac2d0c033c | |||
61fa63eb1d | |||
7022412889 | |||
b4f71745b0 | |||
1d41fce1ae | |||
e636fde4ce | |||
82a9dc5226 | |||
1f40915ac8 | |||
32693c0f64 | |||
c38748eb05 | |||
ac85bea111 | |||
7b5613ce77 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,6 +1,16 @@
|
||||
# Blender Cloud changelog
|
||||
|
||||
|
||||
## 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).
|
||||
@@ -10,7 +20,7 @@
|
||||
|
||||
## Version 1.6.4 (2017-04-21)
|
||||
|
||||
- Added file exclusion filter for Flamenco. A filter like "*.abc;*.mkv;*.mov" can be
|
||||
- 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.
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
bl_info = {
|
||||
'name': 'Blender Cloud',
|
||||
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
|
||||
'version': (1, 7, 0),
|
||||
'version': (1, 7, 2),
|
||||
'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 '
|
||||
|
@@ -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.
|
||||
|
@@ -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'
|
||||
@@ -462,16 +461,7 @@ class BlenderCloudPreferences(AddonPreferences):
|
||||
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
|
||||
|
||||
show_warning = bool(self.flamenco_exclude_filter and
|
||||
not bam_interface.bam_supports_exclude_option())
|
||||
job_output_box.alert = show_warning
|
||||
job_output_box.prop(self, 'flamenco_exclude_filter',
|
||||
icon='ERROR' if show_warning else 'NONE')
|
||||
if show_warning:
|
||||
job_output_box.label(
|
||||
text='Warning, the exclusion filter requires a newer version of Blender!')
|
||||
job_output_box.alert = False
|
||||
job_output_box.prop(self, 'flamenco_exclude_filter')
|
||||
|
||||
prop_split = job_output_box.split(0.32, align=True)
|
||||
prop_split.label('Strip Components:')
|
||||
|
@@ -25,6 +25,19 @@ import logging
|
||||
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 +45,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.
|
||||
@@ -301,7 +315,6 @@ class FLAMENCO_OT_render(async_loop.AsyncModalOperatorMixin,
|
||||
|
||||
from datetime import datetime
|
||||
from ..blender import preferences
|
||||
from . import bam_interface
|
||||
|
||||
prefs = preferences()
|
||||
|
||||
@@ -359,14 +372,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:
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""BAM packing interface for Flamenco."""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import typing
|
||||
@@ -15,26 +14,26 @@ class CommandExecutionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
if 'bam_supports_exclude_option' in locals():
|
||||
locals()['bam_supports_exclude_option'].cache_clear()
|
||||
def wheel_pythonpath_278() -> str:
|
||||
"""Returns the value of a PYTHONPATH environment variable needed to run BAM from its wheel file.
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def bam_supports_exclude_option() -> bool:
|
||||
"""Returns True if the version of BAM bundled with Blender supports --exclude.
|
||||
|
||||
This feature was added to BAM 1.1.7, so we can do a simple version check.
|
||||
Workaround for Blender 2.78c not having io_blend_utils.pythonpath()
|
||||
"""
|
||||
|
||||
try:
|
||||
import io_blend_utils
|
||||
except ImportError:
|
||||
# If this happens, BAM won't work at all. However, this function can be called from
|
||||
# the GUI; by being a bit careful while importing, we avoid breaking Blender's GUI.
|
||||
log.exception('Error importing io_blend_utils module.')
|
||||
return False
|
||||
import os
|
||||
from ..wheels import wheel_filename
|
||||
|
||||
return io_blend_utils.bl_info['version'] >= (1, 1, 7)
|
||||
# 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,
|
||||
@@ -66,18 +65,20 @@ async def bam_copy(base_blendfile: Path, target_blendfile: Path,
|
||||
]
|
||||
|
||||
if exclusion_filter:
|
||||
if bam_supports_exclude_option():
|
||||
args.extend(['--exclude', exclusion_filter])
|
||||
else:
|
||||
log.warning('Your version of Blender does not support the exclusion filter, '
|
||||
'copying all files.')
|
||||
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()
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*args,
|
||||
env={'PYTHONPATH': io_blend_utils.pythonpath()},
|
||||
env={'PYTHONPATH': pythonpath},
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
|
46
blender_cloud/pillar.py
Normal file → Executable file
46
blender_cloud/pillar.py
Normal file → Executable 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()
|
||||
|
@@ -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,
|
||||
|
@@ -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():
|
||||
|
@@ -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
|
||||
|
7
setup.py
7
setup.py
@@ -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,7 +232,7 @@ setup(
|
||||
'wheels': BuildWheels},
|
||||
name='blender_cloud',
|
||||
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
|
||||
version='1.7.0',
|
||||
version='1.7.2',
|
||||
author='Sybren A. Stüvel',
|
||||
author_email='sybren@stuvel.eu',
|
||||
packages=find_packages('.'),
|
||||
|
Reference in New Issue
Block a user