"""BAT🦇 packing interface for Flamenco.""" import asyncio import logging import pathlib import re import threading import typing import urllib.parse import bpy from blender_asset_tracer import pack from blender_asset_tracer.pack import progress, transfer, shaman log = logging.getLogger(__name__) _running_packer = None # type: pack.Packer _packer_lock = threading.RLock() # For using in other parts of the add-on, so only this file imports BAT. Aborted = pack.Aborted FileTransferError = transfer.FileTransferError parse_shaman_endpoint = shaman.parse_endpoint class BatProgress(progress.Callback): """Report progress of BAT Packing to the UI. Uses asyncio.run_coroutine_threadsafe() to ensure the UI is only updated from the main thread. This is required since we run the BAT Pack in a background thread. """ def __init__(self) -> None: super().__init__() self.loop = asyncio.get_event_loop() def _set_attr(self, attr: str, value): async def do_it(): setattr(bpy.context.window_manager, attr, value) asyncio.run_coroutine_threadsafe(do_it(), loop=self.loop) def _txt(self, msg: str): """Set a text in a thread-safe way.""" self._set_attr('flamenco_status_txt', msg) def _status(self, status: str): """Set the flamenco_status property in a thread-safe way.""" self._set_attr('flamenco_status', status) def _progress(self, progress: int): """Set the flamenco_progress property in a thread-safe way.""" self._set_attr('flamenco_progress', progress) def pack_start(self) -> None: self._txt('Starting BAT Pack operation') def pack_done(self, output_blendfile: pathlib.Path, missing_files: typing.Set[pathlib.Path]) -> None: if missing_files: self._txt('There were %d missing files' % len(missing_files)) else: self._txt('Pack of %s done' % output_blendfile.name) def pack_aborted(self, reason: str): self._txt('Aborted: %s' % reason) self._status('ABORTED') def trace_blendfile(self, filename: pathlib.Path) -> None: """Called for every blendfile opened when tracing dependencies.""" self._txt('Inspecting %s' % filename.name) def trace_asset(self, filename: pathlib.Path) -> None: if filename.stem == '.blend': return self._txt('Found asset %s' % filename.name) def rewrite_blendfile(self, orig_filename: pathlib.Path) -> None: self._txt('Rewriting %s' % orig_filename.name) def transfer_file(self, src: pathlib.Path, dst: pathlib.Path) -> None: self._txt('Transferring %s' % src.name) def transfer_file_skipped(self, src: pathlib.Path, dst: pathlib.Path) -> None: self._txt('Skipped %s' % src.name) def transfer_progress(self, total_bytes: int, transferred_bytes: int) -> None: self._progress(round(100 * transferred_bytes / total_bytes)) def missing_file(self, filename: pathlib.Path) -> None: # TODO(Sybren): report missing files in a nice way pass class ShamanPacker(shaman.ShamanPacker): """Packer with support for getting an auth token from Flamenco Server.""" def __init__(self, bfile: pathlib.Path, project: pathlib.Path, target: str, endpoint: str, checkout_id: str, *, manager_id: str, **kwargs) -> None: self.manager_id = manager_id super().__init__(bfile, project, target, endpoint, checkout_id, **kwargs) def _get_auth_token(self) -> str: """get a token from Flamenco Server""" from ..blender import PILLAR_SERVER_URL from ..pillar import blender_id_subclient, uncached_session, SUBCLIENT_ID url = urllib.parse.urljoin(PILLAR_SERVER_URL, 'flamenco/jwt/generate-token/%s' % self.manager_id) auth_token = blender_id_subclient()['token'] resp = uncached_session.get(url, auth=(auth_token, SUBCLIENT_ID)) resp.raise_for_status() return resp.text async def copy(context, base_blendfile: pathlib.Path, project: pathlib.Path, target: str, exclusion_filter: str, *, relative_only: bool, packer_class=pack.Packer, **packer_args) \ -> typing.Tuple[pathlib.Path, typing.Set[pathlib.Path]]: """Use BAT🦇 to copy the given file and dependencies to the target location. :raises: FileTransferError if a file couldn't be transferred. :returns: the path of the packed blend file, and a set of missing sources. """ global _running_packer loop = asyncio.get_event_loop() wm = bpy.context.window_manager packer = packer_class(base_blendfile, project, target, compress=True, relative_only=relative_only, **packer_args) with packer: with _packer_lock: if exclusion_filter: # There was a mistake in an older version of the property tooltip, # showing semicolon-separated instead of space-separated. We now # just handle both. filter_parts = re.split('[ ;]+', exclusion_filter.strip(' ;')) packer.exclude(*filter_parts) packer.progress_cb = BatProgress() _running_packer = packer log.debug('awaiting strategise') wm.flamenco_status = 'INVESTIGATING' await loop.run_in_executor(None, packer.strategise) log.debug('awaiting execute') wm.flamenco_status = 'TRANSFERRING' await loop.run_in_executor(None, packer.execute) log.debug('done') wm.flamenco_status = 'DONE' with _packer_lock: _running_packer = None return packer.output_path, packer.missing_files def abort() -> None: """Abort a running copy() call. No-op when there is no running copy(). Can be called from any thread. """ with _packer_lock: if _running_packer is None: log.debug('No running packer, ignoring call to bat_abort()') return log.info('Aborting running packer') _running_packer.abort()