This commit is contained in:
annatisch 2015-01-13 11:04:30 +13:00
Коммит 7bac13bb72
40 изменённых файлов: 6073 добавлений и 0 удалений

17
.gitattributes поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

216
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,216 @@
#################
## Eclipse
#################
*.pydevproject
.project
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.publishproj
# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
#############
## Windows detritus
#############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac crap
.DS_Store
#############
## Python
#############
*.py[cod]
# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg

Просмотреть файл

@ -0,0 +1,33 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.21005.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blender.Cloud", "Blender.Cloud\Blender.Cloud.csproj", "{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}"
EndProject
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Blender.Client", "Blender.Client\Blender.Client.pyproj", "{16D9E706-0005-4AC4-832C-D0403F6B8008}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{465620A1-3D6E-431A-A116-F7260C8D87F9}"
ProjectSection(SolutionItems) = preProject
CHANGES.txt = CHANGES.txt
LICENSE.txt = LICENSE.txt
README.rst = README.rst
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}.Release|Any CPU.Build.0 = Release|Any CPU
{16D9E706-0005-4AC4-832C-D0403F6B8008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16D9E706-0005-4AC4-832C-D0403F6B8008}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>16d9e706-0005-4ac4-832c-d0403f6b8008</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>package.py</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<Name>Blender.Client</Name>
<RootNamespace>Blender.Client</RootNamespace>
<InterpreterId>
</InterpreterId>
<InterpreterVersion>
</InterpreterVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<PtvsTargetsFile>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets</PtvsTargetsFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="batchapps_blender\assets.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\auth.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\history.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\pools.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_pools.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\submission.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_assets.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_auth.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_history.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_submission.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\props\props_shared.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\draw.py" />
<Compile Include="batchapps_blender\shared.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_assets.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_auth.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_history.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_pools.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_shared.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\ui\ui_submission.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\utils.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="batchapps_blender\__init__.py" />
<Compile Include="package.py">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="batchapps_blender\" />
<Folder Include="batchapps_blender\ui\" />
<Folder Include="batchapps_blender\props\" />
</ItemGroup>
<Import Condition="Exists($(PtvsTargetsFile))" Project="$(PtvsTargetsFile)" />
<Import Condition="!Exists($(PtvsTargetsFile))" Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />
<!-- Uncomment the CoreCompile target to enable the Build command in
Visual Studio and specify your pre- and post-build commands in
the BeforeBuild and AfterBuild targets below. -->
<!--<Target Name="CoreCompile" />-->
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
</Project>

Просмотреть файл

@ -0,0 +1,102 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
bl_info = {
"name": "Batch Apps Blender",
"author": "Microsoft Corp. <bigcompute@microsoft.com>",
"version": (0, 1, 0),
"blender": (2, 7, 2),
"location": "Render Properties",
"description": ("Export Blender files to be rendered externally by "
"Microsoft Batch Apps."),
"category": "Render"}
import bpy
from batchapps import credentials
from batchapps import config as logging
from batchapps_blender.props.props_shared import BatchAppsPreferences
from batchapps_blender.shared import BatchAppsSettings
from batchapps_blender.draw import *
@bpy.app.handlers.persistent
def start_session(self):
"""
Instantiate the Batch Apps session and register all the property
classes to the Blender context.
This is handled in an event to allow it to run under the full
Blender context rather than the limited loading scope.
Once the session has started (or reported an error), this function
is removed from the event handlers.
"""
try:
session = BatchAppsSettings()
def get_session(self):
return session
bpy.types.Scene.batchapps_session = property(get_session)
except Exception as e:
print("BatchApps Addon failed to load.")
print ("Error: {0}".format(e))
finally:
bpy.app.handlers.scene_update_post.remove(start_session)
def register():
"""
Register module and applicable classes.
This method also sets some BatchApps globals. In particular, the
python module Requests that is packaged with blender does not allow
for certificates to be verified, so we have to either turn this off,
or replace the included Requests module (recommended).
Here we also register the User Preferences for the Addon, so it can
be configured in the Blender User Preferences window.
"""
credentials.VERIFY = False
logging.STREAM_LOG = False
bpy.app.handlers.scene_update_post.append(start_session)
bpy.utils.register_class(BatchAppsPreferences)
bpy.utils.register_module(__name__)
def unregister():
"""
Unregister the addon if deselected from the User Preferences window.
"""
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()

Просмотреть файл

@ -0,0 +1,464 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import logging
import tempfile
import os
import re
import string
import random
from batchapps_blender.ui import ui_assets
from batchapps_blender.props import props_assets
from batchapps_blender.utils import BatchAppsOps
class BatchAppsAssets(object):
"""
Manager for all external file handling and displaying of assets.
"""
pages = ["ASSETS"]
def __init__(self, manager):
self.batchapps = manager
self.ops = self._register_ops()
self.props = self._register_props()
self.ui = self._register_ui()
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[bpy.context.scene.batchapps_session.page](ui, layout)
def _register_props(self):
"""
Registers and retrieves the asset property objects.
These properties are assigned to the scene.batchapps_assets context.
It also resets the current job filepath, to prevent this being
persisted between different Blender scenes.
:Returns:
- :class:`.AssetProps`
"""
props = props_assets.register_props()
props.path = ""
return props
def _register_ops(self):
"""
Registers each asset operator with a batchapps_assets prefix.
:Returns:
- A list of the names (str) of the registered asset operators.
"""
ops = []
ops.append(BatchAppsOps.register("assets.page",
"Scene assets",
self._assets))
ops.append(BatchAppsOps.register("assets.refresh",
"Refresh assets",
self._refresh))
ops.append(BatchAppsOps.register("assets.upload",
"Upload selected assets",
self._upload))
ops.append(BatchAppsOps.register("assets.remove",
"Remove asset",
invoke=self._remove))
ops.append(BatchAppsOps.register("assets.add", "Add asset",
self._add_execute,
invoke=self._add_invoke,
filepath=bpy.props.StringProperty(
subtype="FILE_PATH")))
return ops
def _register_ui(self):
"""
Matches the assets page with its corresponding ui function.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_asset_ui(name):
name = name.lower()
return getattr(ui_assets, name)
page_func = map(get_asset_ui, self.pages)
return dict(zip(self.pages, page_func))
def _assets(self, op, context):
"""
The execute method for the assets.page operator.
Sets the page and initiates asset collection.
Also checks whether assets have already been initialized for the
current scene file.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
session = context.scene.batchapps_session
session.page = "ASSETS"
self.props = context.scene.batchapps_assets
new_path = self.get_jobpath()
if new_path != self.props.path:
session.log.debug("New scene, gathering assets.")
self.props.path = new_path
if not self.props.temp:
session.log.debug("Not temp file - saving.")
bpy.ops.wm.save_mainfile()
self.generate_collection()
return {'FINISHED'}
def _refresh(self, op, context):
"""
The execute method for the assets.refresh operator.
Re-initiates asset collection from scratch, regardless of whether it
has been done before for this scene. This means any additional assets
added, or any existing assets removed will be restored to their initial
state.
This also updates the current job file path if it has changed.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
session = context.scene.batchapps_session
self.props = context.scene.batchapps_assets
new_path = self.get_jobpath()
if new_path != self.props.path:
session.log.debug("New scene, resetting path.")
self.props.path = new_path
self.generate_collection()
return {'FINISHED'}
def _upload(self, op, context):
"""
The execute method for the assets.upload operator.
Identifies assets that have been selected for uploaded.
Iterates through uploaded each sequentially, and updates
the UI uploaded checkbox accordingly.
If one asset fails to upload, the operator will continue to
attempt to upload the remaining.
Any other error that occurs will be raised to be handled by
:func:`.BatchAppsOps.session`.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
session = context.scene.batchapps_session
upload = self.pending_upload()
session.log.info("{0} assets to be uploaded".format(len(upload)))
for index in upload:
asset = self.props.collection[index]
display = self.props.assets[index]
try:
session.log.debug("Uploading {0}".format(asset.name))
asset.upload(force=True)
display.upload_check = True
session.log.debug("Upload complete")
except Exception as exp:
print('Failed to upload: {0}'.format(exp))
display.upload_check = False
return {'FINISHED'}
def _add_execute(self, op, context):
"""
The execute method for the assets.add operator.
Adds an asset selected via a file browser.
The file will only be added if it exists and has not already been
added. If the file is already present, it will be skipped and the
operator will finished without error.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
session = context.scene.batchapps_session
session.log.debug("Selected file {0}".format(op.filepath))
user_file = self.batchapps.file_from_path(op.filepath)
if user_file and user_file not in self.props.collection:
self.props.add_asset(user_file)
else:
session.log.warning("File {0} either duplicate or does not "
"exist.".format(user_file.name))
return {'FINISHED'}
def _add_invoke(self, op, context, event):
"""
The invoke method for the assets.add operator.
Invokes the file selector window from the WindowManager.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- Blender-specific value {'RUNNING_MODAL'} to indicate the operator
will continue to process after the completion of this function.
"""
context.window_manager.fileselect_add(op)
return {'RUNNING_MODAL'}
def _remove(self, op, context, event):
'''
The invoke method for the assets.remove operator.
Removes currently selected assets from both the display assets
and the UserFile collection.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
'''
if not self.props.assets:
return {'FINISHED'}
self.props.remove_selected()
return {'FINISHED'}
def collect_assets(self):
"""
Generates a list of the external files referenced by the current
blend file. After collection, the paths are made absolute and
normalized.
This currently includes files from:
- bpy.data.sounds
- bpy.data.fonts
- bpy.data.textures.image
- bpy.data.images
- bpy.data.libraries
:Returns:
- A list of file paths as strings.
"""
asset_list = []
bpy.context.scene.batchapps_session.log.info(
"Collecting external assets.")
for s in bpy.data.sounds:
new_path = os.path.realpath(bpy.path.abspath(s.filepath))
asset_list.append(new_path)
for f in bpy.data.fonts:
if f.filepath != "<builtin>":
new_path = os.path.realpath(bpy.path.abspath(f.filepath))
asset_list.append(new_path)
for t in bpy.data.textures:
if hasattr(t, 'image'):
if t.image:
new_path = bpy.path.abspath(t.image.filepath)
new_path = os.path.normpath(os.path.realpath(new_path))
asset_list.append(new_path)
for i in bpy.data.images:
new_path = os.path.realpath(bpy.path.abspath(i.filepath))
asset_list.append(os.path.normpath(new_path))
for l in bpy.data.libraries:
new_path = os.path.realpath(bpy.path.abspath(l.filepath))
asset_list.append(os.path.normpath(new_path))
bpy.context.scene.batchapps_session.log.info(
"Found %d asset files." % (len(asset_list)))
return asset_list
def name_generator(self, size=8, chars=string.hexdigits):
"""
Generates a random blend filename for a temporary blend file.
:Kwargs:
- size (int): The number of random chars to use. Default is 8.
- chars (string): The chars from which random chars will be
selected. Default is '0123456789abcdefABCDEF'
:Returns:
- A file name (str) with the prefix ``BATCHAPPSTMP_`` and
suffix ".blend".
"""
return "BATCHAPPSTMP_"+''.join(random.choice(chars) for x in range(size))+".blend"
def get_jobpath(self):
"""
Gets the filepath to the job blend file. If currently using a saved
blend file, this path will be returned.
If the current blend file has never been saved (i.e. it has no path),
a temporary path will be generated in the Blender User Preferences
temporary directory.
This temp filepath will not be saved to until submission.
:Returns:
- The file path (str) to the .blend file.
"""
#TODO: Test relative vs. absolute paths.
session = bpy.context.scene.batchapps_session
if bpy.data.filepath == '' and self.props.temp:
session.log.debug(
"Blend path: Using current temp {0}".format(self.props.path))
return self.props.path
elif bpy.data.filepath == '':
temp_dir = bpy.context.user_preferences.filepaths.temporary_directory
temp_path = os.path.join(temp_dir, self.name_generator())
self.props.temp = True
session.log.debug(
"Blend path: Using new temp {0}".format(temp_path))
return temp_path
else:
self.props.temp = False
session.log.debug(
"Blend path: Using saved {0}".format(bpy.data.filepath))
return bpy.data.filepath
def generate_collection(self):
"""
Runs :func:`.collect_assets` and converts the result path list into
BatchApps UserFile objects, which the props class then adds to
the display assets list as well as the UserFile collection.
"""
session = bpy.context.scene.batchapps_session
self.props.reset()
assets = self.collect_assets()
for asset in assets:
session.log.debug("Discovered asset {0}.".format(asset))
user_file = self.batchapps.file_from_path(asset)
if user_file and user_file not in self.props.collection:
self.props.add_asset(user_file)
else:
session.log.warning("File {0} either duplicate or does not "
"exist.".format(user_file.name))
if not self.props.temp:
session.log.debug("Adding blend file as asset.")
jobfile = self.batchapps.file_from_path(self.props.path)
if jobfile and jobfile not in self.props.collection:
self.props.add_asset(jobfile)
def pending_upload(self):
"""
Get a list of the assets that have selected for upload.
:Returns:
- A list of the indexes (int) of the items in the display
assets list that have been selected for upload.
"""
upload_me = []
for index, asset in enumerate(self.props.assets):
if asset.upload_checkbox:
upload_me.append(index)
return upload_me

Просмотреть файл

@ -0,0 +1,402 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import webbrowser
import logging
import threading
from http.server import HTTPServer
from urllib.parse import unquote
from batchapps_blender.utils import (
BatchAppsOps,
OAuthRequestHandler)
from batchapps_blender.ui import ui_auth
from batchapps_blender.props import props_auth
from batchapps import AzureOAuth
from batchapps.exceptions import (
AuthenticationException,
InvalidConfigException)
TIMEOUT = 60 # 1 minute
class BatchAppsAuth(object):
"""
Managers authentication of the session for the BatchApps Blender Addon.
Will attempt to sign in automatically based on data available in the
Batch Apps configuration. If unsuccessful, will prompt use to sign in
via a web browser.
"""
pages = ["LOGIN", "REDIRECT"]
def __init__(self):
self.ops = self._register_ops()
self.props = self._register_props()
self.ui = self._register_ui()
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[bpy.context.scene.batchapps_session.page](ui, layout)
def _register_props(self):
"""
Registers and retrieves the auth property objects.
These properties are assigned to the scene.batchapps_auth context.
:Returns:
- :class:`.AuthProps`
"""
props = props_auth.register_props()
return props
def _register_ops(self):
"""
Registers each auth operator with a batchapps_auth prefix.
:Returns:
- A list of the names (str) of the registered auth operators.
"""
ops = []
ops.append(BatchAppsOps.register("auth.login",
"Login",
self._login))
ops.append(BatchAppsOps.register("auth.logout",
"Logout",
self._logout))
ops.append(BatchAppsOps.register("auth.redirect",
"Redirecting authentication",
modal=self._redirect_modal,
invoke=self._redirect_invoke,
_timer=None))
return ops
def _register_ui(self):
"""
Matches the login and redirection pages with their corresponding
ui functions.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_auth_ui(name):
name = name.lower()
return getattr(ui_auth, name)
page_func = map(get_auth_ui, self.pages)
return dict(zip(self.pages, page_func))
def _redirect_modal(self, op, context, event):
"""
The modal method for the auth.redirect operator to handle running
the authentication redirection server in a separate thread to
prevent the blocking of the Blender UI.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- If the thread has completed, the Blender-specific value
{'FINISHED'} to indicate the operator has completed its action.
- Otherwise the Blender-specific value {'RUNNING_MODAL'} to
indicate the operator wil continue to process after the
completion of this function.
"""
if event.type == 'TIMER':
context.scene.batchapps_session.log.debug("AuthThread complete.")
if not self.props.thread.is_alive():
context.window_manager.event_timer_remove(op._timer)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def _redirect_invoke(self, op, context, event):
"""
The invoke method for the auth.redirect operator.
Starts the authentication redirect thread.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- Blender-specific value {'RUNNING_MODAL'} to indicate the operator
wil continue to process after the completion of this function.
"""
self.props.thread.start()
context.scene.batchapps_session.log.debug("AuthThread initiated.")
context.window_manager.modal_handler_add(op)
op._timer = context.window_manager.event_timer_add(1, context.window)
return {'RUNNING_MODAL'}
def _login(self, op, context, *args):
"""
The execute method for the auth.login operator.
Sets the functions to per performed by the authentication thread and
updates the session page to "REDIRECT" while the thread executes.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
auth_thread = lambda: BatchAppsOps.session(self.web_authentication)
self.props.thread = threading.Thread(name="AuthThread",
target=auth_thread)
bpy.ops.batchapps_auth.redirect('INVOKE_DEFAULT')
if context.scene.batchapps_session.page == "LOGIN":
context.scene.batchapps_session.page = "REDIRECT"
return {'FINISHED'}
def _logout(self, op, context, *args):
"""
The execute method for the auth.logout operator.
Clears any cached credentials and resets the session page back to the
Login screen.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
if self.props.credentials:
self.props.credentials.clear_auth()
self.props.credentials = None
bpy.context.scene.batchapps_session.page = "LOGIN"
bpy.context.scene.batchapps_session.log.info(
"Logged out. Cached sessions cleared.")
return {'FINISHED'}
def auto_authentication(self, cfg, log):
"""
Attempts to authenticate automatically by first searching the Batch Apps
configuration for an unattended session, then a cached session.
:Args:
- cfg (:class:`batchapps.Configuration`): An instance of the Batch
Apps Configuration class, read from the file set in the addon
User Preferences.
- log (:class:`batchapps.log.PickleLog`): A logger object as set in
BatchAppsSettings.
:Returns:
- ``True`` if the addon was successfully authenticated,
else ``False``
"""
try:
log.info("Checking for unattended session...")
self.props.credentials = AzureOAuth.get_unattended_session(config=cfg)
log.info("Found!")
return True
except (AuthenticationException, InvalidConfigException) as exp:
log.info("Could not get unattended session: {0}".format(exp))
try:
log.info("Checking for cached session...")
self.props.credentials = AzureOAuth.get_session(config=cfg)
log.info("Found!")
return True
except (AuthenticationException, InvalidConfigException) as exp:
log.info("Could not get cached session: {0}".format(exp))
return False
def wait_for_request(self):
"""
Once the user has been prompted to authenticate in a web browser
session, start a basic HTTPServer to intercept the AAD redirect call
to collect the server response to the auth request.
The localhost redirect URL will depend on how the client is set up in
the AAD portal.
The server has a timeout of 1 minute.
"""
session = bpy.context.scene.batchapps_session
self.props.code=None
config = bpy.context.scene.batchapps_session.cfg
redirect = config.aad_config()['redirect_uri'].split(':')
server_address = (redirect[0], int(redirect[1]))
web_server = HTTPServer(server_address, OAuthRequestHandler)
session.log.debug("Created web server listening at: {0}, {1}.".format(
redirect[0], int(redirect[1])))
web_server.timeout = TIMEOUT
web_server.handle_request()
web_server.server_close()
session.log.debug("Closed server.")
def open_websession(self):
"""
Open a web browser session to prompt the user to authenticate via their
AAD credentials.
This method of authentication is the 'last resort' after
auto-authentication and unattended authentication have failed.
:Raises:
- :class:`RuntimeError` if authentication fails, which will fail
the loading of the addon as all auth routes have failed. This
could be due to either an
:class:`batchapps.exceptions.AuthenticationException` of a
:class:`batchapps.exceptions.InvalidConfigException`.
"""
session = bpy.context.scene.batchapps_session
try:
url, state = AzureOAuth.get_authorization_url(config=session.cfg)
webbrowser.open(url)
session.log.info("Opened web browser for authentication "
"and waiting for response.")
self.wait_for_request()
except (AuthenticationException, InvalidConfigException) as exp:
session.log.error("Unable to open Web UI auth session: "
"{0}".format(exp))
raise RuntimeError("Failed to authorize addon")
def decode_error(self, val):
"""
Format the auth redirect URL to extract the auth code.
:Args:
- val (str): The redirect URL.
:Returns:
- The auth code (str).
"""
error_idx = self.props.code.find(val)
if error_idx < 0:
return None
strt_idx = error_idx + len(val)
end_idx = self.props.code.find('&', strt_idx)
error_val = self.props.code[strt_idx:end_idx]
return unquote(error_val)
def web_authentication(self):
"""
Prompts user to authenticate via a web browser session, after
auto-authentication and unattended authentication have failed.
If web authentication is successful, the session (i.e. refresh token)
will be cached to enable auto-authentication next time the addon is
used. Session page will be set to the HOME page.
If unsuccessful, the addon will failed to load and the ERROR page will
be display. Error details will be logged to the console.
"""
session = bpy.context.scene.batchapps_session
if self.auto_authentication(session.cfg, session.log):
session.start(self.props.credentials)
session.page = "HOME"
session.redraw()
return
self.open_websession()
if not self.props.code:
session.log.warning("Log in timed out - please try again.")
session.page = "LOGIN"
elif '/?error=' in self.props.code:
error = self.decode_error('/?error=')
details = self.decode_error(
'&error_description=').replace('+', ' ')
session.log.error("Authentication failed: {0}".format(error))
session.log.error(details)
session.page = "ERROR"
else:
session.log.info(
"Received valid authentication response from web browser.")
session.log.info("Now retrieving new authentication token...")
self.props.credentials = AzureOAuth.get_authorization_token(
self.props.code, config=session.cfg)
session.start(self.props.credentials)
session.log.info("Successful! Login complete.")
session.redraw()

Просмотреть файл

@ -0,0 +1,185 @@
#-------------------------------------------------------------------------
# Azure Batch Apps Blender Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
from batchapps.exceptions import SessionExpiredException
class Interface(bpy.types.Panel):
"""
Global Batch Apps Blender Interface. Handles the separate UI
definitions of all the submodules based on the session page.
Also provides custom functions for display props, ops and labels.
"""
bl_label = "BatchApps Rendering"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "render"
COMPAT_ENGINES = ['BLENDER_RENDER', 'CYCLES']
@classmethod
def poll(self, context):
"""
Only displays the addon when a compatible render engine is
selected. Currently: Blender Internal and Cycles only.
"""
render = context.scene.render
return (render.engine in self.COMPAT_ENGINES)
def label(self, label, row, align=None, icon=None, active=True):
"""
Display a text label.
:Args:
- label (str): The text to display.
- row (:class:`bpy.types.UILayout`): The layout component to
add the label to.
:Kwargs:
- align (str): If set, will align the label according to the
Blender enum: {'LEFT', 'RIGHT', 'CENTER', 'EXPAND'}
- icon: (str): If set, will display an icon with the label, from
the Blender icon list.
- active (bool): If true, the UI label will be enabled, else it
will be greyed out. Note that due to how this works in Blender,
this attribute will be applied to the whole UILayout component
not just this label.
"""
if align:
row.alignment = align
if icon:
row.label(text=label, icon=icon)
else:
row.label(text=label)
row.enabled = active
def prop(self, data, prop, row, label="",align=None, active=True,
**kwargs):
"""
Display a Blender property.
:Args:
- data (Blender context): The context of which the property is
an attribute.
- prop (str): The property (attribute name) to be displayed.
- row (:class:`bpy.types.UILayout`): The layout component to
dispaly the property on.
:Kwargs:
- label (str): Any test to accompany the property. Default is "".
- align (str): If set, will align the label according to the
Blender enum: {'LEFT', 'RIGHT', 'CENTER', 'EXPAND'}
- active (bool): If true, the UI prop will be enabled, else it
will be greyed out. Note that due to how this works in Blender,
this attribute will be applied to the whole UILayout component
not just this property.
"""
if align:
row.alignment = align
if label is not None:
row.prop(data, prop, text=label, **kwargs)
else:
row.prop(data, prop, **kwargs)
row.enabled = active
def operator(self, op, label, row, icon="NONE", align=None, active=True):
"""
Dispaly a registered Blender operator as a button.
:Args:
- op (bpy.types.Operator): A Blender operator to display a
button for.
- label (str): The text to display on the button.
- row (:class:`bpy.types.UILayout`): The layout component to
dispaly the button on.
:Kwargs:
- icon: (str): If set, will display an icon on the button, from
the Blender icon list.
- align (str): If set, will align the button according to the
Blender enum: {'LEFT', 'RIGHT', 'CENTER', 'EXPAND'}
- active (bool): If true, the UI button will be enabled, else it
will be greyed out. Note that due to how this works in Blender,
this attribute will be applied to the whole UILayout component
not just this button.
"""
if align:
row.alignment = align
row.operator("batchapps_" + op, text=label, icon=icon)
row.enabled = active
def draw(self, context):
"""
The global draw method. This is called every time Blender's UI
is refreshed.
Which page of the addon is display is set by the Batch Apps session
context.
If the requested page is not recognized, the error page is display.
:Args:
- context (bpy.types.Context): The current Blender runtime context.
"""
if not hasattr(context.scene, 'batchapps_session'):
return None
layout = self.layout
session = context.scene.batchapps_session
if session.page in session.pages:
session.display(self, layout)
elif session.page in session.auth.pages:
session.auth.display(self, layout)
elif session.page in session.submission.pages:
session.submission.display(self, layout)
elif session.page in session.assets.pages:
session.assets.display(self, layout)
elif session.page in session.pools.pages:
session.pools.display(self, layout)
elif session.page in session.history.pages:
session.history.display(self, layout)
else:
session.log.error("Cant load page: {0}. "
"No definition found.".format(session.page))
session.page = "ERROR"
session.display(self, layout)

Просмотреть файл

@ -0,0 +1,435 @@
#-------------------------------------------------------------------------
# Azure Batch Apps Blender Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import os
import threading
from batchapps_blender.utils import BatchAppsOps
from batchapps_blender.ui import ui_history
from batchapps_blender.props import props_history
from batchapps.exceptions import RestCallException
class BatchAppsHistory(object):
"""
Manger for the retrival and display of the users job history.
"""
pages = ["HISTORY", "LOADING"]
def __init__(self, manager):
self.batchapps = manager
self.ops = self._register_ops()
self.props = self._register_props()
self.ui = self._register_ui()
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[bpy.context.scene.batchapps_session.page](ui, layout)
def _register_props(self):
"""
Registers and retrieves the history property objects.
The dispaly properties are defined in a subclass which is assigned
to the scene.batchapps_history context.
:Returns:
- :class:`.HistoryProps`
"""
props = props_history.register_props()
return props
def _register_ops(self):
"""
Registers each job history operator with a batchapps_history prefix.
:Returns:
- A list of the names (str) of the registered job history
operators.
"""
ops = []
ops.append(BatchAppsOps.register("history.page",
"Job history",
self._history))
ops.append(BatchAppsOps.register("history.first",
"Beginning",
self._first))
ops.append(BatchAppsOps.register("history.last",
"End",
self._last))
ops.append(BatchAppsOps.register("history.more",
"Next",
self._more))
ops.append(BatchAppsOps.register("history.less",
"Previous",
self._less))
ops.append(BatchAppsOps.register("history.refresh",
"Refresh",
self._refresh))
ops.append(BatchAppsOps.register("history.cancel",
"Cancel job",
self._cancel))
ops.append(BatchAppsOps.register("history.loading",
"Loading job history",
modal=self._loading_modal,
invoke=self._loading_invoke,
_timer=None))
return ops
def _register_ui(self):
"""
Matches the history and loading pages with their corresponding
ui functions.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_history_ui(name):
name = name.lower()
return getattr(ui_history, name)
page_func = map(get_history_ui, self.pages)
return dict(zip(self.pages, page_func))
def _loading_modal(self, op, context, event):
"""
The modal method for the history.loading operator to handle running
the downloading of the job history data in a separate thread to
prevent the blocking of the Blender UI.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- If the thread has completed, the Blender-specific value
{'FINISHED'} to indicate the operator has completed its action.
- Otherwise the Blender-specific value {'RUNNING_MODAL'} to
indicate the operator wil continue to process after the
completion of this function.
"""
if event.type == 'TIMER':
context.scene.batchapps_session.log.debug("HistoryThread complete.")
if not self.props.thread.is_alive():
context.window_manager.event_timer_remove(op._timer)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def _loading_invoke(self, op, context, event):
"""
The invoke method for the history.loading operator.
Starts the job data retrieval thread.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- Blender-specific value {'RUNNING_MODAL'} to indicate the operator
wil continue to process after the completion of this function.
"""
self.props.thread.start()
context.scene.batchapps_session.log.debug("HistoryThread initiated.")
context.window_manager.modal_handler_add(op)
op._timer = context.window_manager.event_timer_add(1, context.window)
return {'RUNNING_MODAL'}
def _history(self, op, context, *args):
"""
The execute method for the history.page operator.
Sets the functions to be performed by the job data retrieval thread
and updates the session page to "LOADING" while the thread executes.
Also resets the job display paging controls.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.props.display = context.scene.batchapps_history
self.props.display.selected = -1
self.props.display.index = 0
self.props.display.total_count = 0
history_thread = lambda: BatchAppsOps.session(self.get_job_list)
self.props.thread = threading.Thread(name="HistoryThread",
target=history_thread)
bpy.ops.batchapps_history.loading('INVOKE_DEFAULT')
if context.scene.batchapps_session.page == "HOME":
context.scene.batchapps_session.page = "LOADING"
return {'FINISHED'}
def _first(self, op, context, *args):
"""
The execute method for the history.first operator.
Resets the job display paging index to 0 (to display the first jobs
in the list) and re-loads the accompanying job data.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.props.display.index = 0
self.get_job_list()
return {'FINISHED'}
def _last(self, op, context, *args):
"""
The execute method for the history.last operator.
Resets the job display paging controls to display the last jobs
in the list and re-loads the accompanying job data.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
settings = self.props.display
if (settings.total_count % settings.per_call) == 0:
settings.index = settings.total_count - settings.per_call
else:
div = settings.per_call - (settings.total_count % settings.per_call)
settings.index = settings.total_count - settings.per_call + div
self.get_job_list()
return {'FINISHED'}
def _more(self, op, context, *args):
"""
The execute method for the history.more operator.
Resets the job display paging controls to display the subsequent jobs
in the list and re-loads the accompanying job data.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.props.display.index = self.props.display.index + self.props.display.per_call
self.get_job_list()
return {'FINISHED'}
def _less(self, op, context, *args):
"""
The execute method for the history.less operator.
Resets the job display paging controls to display the previous jobs
in the list and re-loads the accompanying job data.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.props.display.index = self.props.display.index - self.props.display.per_call
self.get_job_list()
return {'FINISHED'}
def _refresh(self, op, context, *args):
"""
The execute method for the history.refresh operator.
Re-loads the current job data.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.get_job_list()
return {'FINISHED'}
def _cancel(self, op, context, *args):
"""
The execute method for the history.cancel operator.
Cancels the currently selected job.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
job = self.get_selected_job()
context.scene.batchapps_session.log.debug(
"Selected job {0}".format(job.id))
job.cancel()
job.update()
context.scene.batchapps_session.log.info(
"Cancelled with ID: {0}".format(job.id))
return {'FINISHED'}
def get_selected_job(self):
"""
Retrieves the job object for the job currently selected in
the dispaly.
:Returns:
- A :class:`batchapps.jobs.SubmittedJob` object.
"""
return self.props.job_list[self.props.display.selected]
def get_job_list(self):
"""
Downlaods a set of job data based on index and default per call parameter,
assigns it to the property job_list and redraws the HISTORY page to
display the new data.
Each job is also registered as an operator class.
#TODO: Unregister previous job classes?
"""
self.props.job_list = []
self.props.display.jobs.clear()
bpy.context.scene.batchapps_session.log.debug(
"Getting job data: index {0}, total {1}, percall {2}".format(
self.props.display.index,
self.props.display.total_count,
self.props.display.per_call))
latest_jobs = self.batchapps.get_jobs(
index=self.props.display.index,
per_call=self.props.display.per_call)
for job in latest_jobs:
self.props.job_list.append(job)
self.props.display.add_job(job)
self.props.display.total_count = len(self.batchapps)
for index, job in enumerate(self.props.display.jobs):
self.register_job(job, index)
bpy.context.scene.batchapps_session.log.info(
"Retrieved {0} of {1} job "
"listings.".format(len(latest_jobs),
self.props.display.total_count))
bpy.context.scene.batchapps_session.page = "HISTORY"
bpy.context.scene.batchapps_session.redraw()
def register_job(self, job, index):
"""
Register a job as an operator class for dispaly in the UI.
:Args:
- job (:class:`batchapps.jobs.SubmittedJob`): The job to register.
- index (int): The index of the job in list currently displayed.
:Returns:
- The newly registered operator name (str).
"""
name = "history.{0}".format(job.id.replace("-", "_"))
label = "Job: {0}".format(job.name)
index_prop = bpy.props.IntProperty(default=index)
def execute(self):
session = bpy.context.scene.batchapps_history
bpy.context.scene.batchapps_session.log.debug(
"Job details opened: {0}, selected: {1}, index {2}".format(
self.enabled,
session.selected,
self.ui_index))
if self.enabled and session.selected == self.ui_index:
session.selected = -1
else:
session.selected = self.ui_index
bpy.context.scene.batchapps_session.log.debug(
"Registering {0}".format(name))
return BatchAppsOps.register_expanding(name, label, execute,
ui_index=index_prop)

Просмотреть файл

@ -0,0 +1,273 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import os
import threading
from batchapps_blender.utils import BatchAppsOps
from batchapps_blender.ui import ui_pools
from batchapps_blender.props import props_pools
from batchapps.exceptions import RestCallException
class BatchAppsPools(object):
"""
Manager for the display and creation of Batch Apps instance pools.
"""
pages = ["POOLS", "CREATE"]
def __init__(self, manager):
self.batchapps = manager
self.ops = self._register_ops()
self.props = self._register_props()
self.ui = self._register_ui()
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[bpy.context.scene.batchapps_session.page](ui, layout)
def _register_props(self):
"""
Registers and retrieves the pools property objects.
The dispaly properties are defined in a subclass which is assigned
to the scene.batchapps_pools context.
:Returns:
- :class:`.PoolsProps`
"""
props = props_pools.register_props()
return props
def _register_ops(self):
"""
Registers each pool operator with a batchapps_pools prefix.
:Returns:
- A list of the names (str) of the registered pool operators.
"""
ops = []
ops.append(BatchAppsOps.register("pools.page",
"Running pools",
self._pools))
ops.append(BatchAppsOps.register("pools.start",
"Start new pool",
self._start))
ops.append(BatchAppsOps.register("pools.delete",
"Delete pool",
self._delete))
ops.append(BatchAppsOps.register_expanding("pools.create",
"Create pool",
self._create))
return ops
def _register_ui(self):
"""
Matches the pools and create pool pages with their corresponding
ui functions.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_pools_ui(name):
name = name.lower()
return getattr(ui_pools, name)
page_func = map(get_pools_ui, self.pages)
return dict(zip(self.pages, page_func))
def _pools(self, op, context):
"""
The execute method for the pools.page operator.
Downloads the data on the pools currently running in the service and
registers each as an operator for display in the UI.
Sets the page to POOLS.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.props.display = bpy.context.scene.batchapps_pools
self.props.display.pools.clear()
self.props.display.selected = -1
context.scene.batchapps_session.log.debug("Getting pool data.")
self.props.pools = self.batchapps.get_pools()
context.scene.batchapps_session.log.info(
"Retrieved {0} pool references.".format(len(self.props.pools)))
for pool in self.props.pools:
self.props.display.add_pool(pool)
for index, pool in enumerate(self.props.display.pools):
self.register_pool(pool, index)
context.scene.batchapps_session.page = "POOLS"
return {'FINISHED'}
def _start(self, op, context):
"""
The execute method for the pools.start operator.
Starts a newly created pool, then calls the pools.page operator
to refresh the pool list in the display and return to the POOLS page.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
new_pool = self.batchapps.create(
target_size=self.props.display.pool_size)
context.scene.batchapps_session.log.info(
"Started new pool with ID: {0}".format(new_pool.id))
return bpy.ops.batchapps_pools.page()
def _delete(self, op, context):
"""
The execute method for the pools.delete operator.
Delete the currently selected pool, then calls the pools.page
operator to refresh the pool list in the display.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
pool = self.get_selected_pool()
context.scene.batchapps_session.log.debug(
"Selected pool {0}".format(pool.id))
pool.delete()
context.scene.batchapps_session.log.info(
"Deleted pool with ID: {0}".format(pool.id))
return bpy.ops.batchapps_pools.page()
def _create(self, op):
"""
The execute method for the pools.create operator.
Display the UI components to create a new pool by setting the page
to CREATE.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
session = bpy.context.scene.batchapps_session
session.page = "POOLS" if op.enabled else "CREATE"
return {'FINISHED'}
def get_selected_pool(self):
"""
Retrieves the pool object for the pool currently selected in
the dispaly.
:Returns:
- A :class:`batchapps.pools.Pool` object.
"""
return self.props.pools[self.props.display.selected]
def register_pool(self, pool, index):
"""
Register a pool as an operator class for dispaly in the UI.
:Args:
- pool (:class:`batchapps.jobs.SubmittedJob`): The pool to
register.
- index (int): The index of the job in list currently displayed.
:Returns:
- The newly registered operator name (str).
"""
name = "pools.{0}".format(pool.id.replace("-", "_"))
label = "Pool: {0}".format(pool.id)
index_prop = bpy.props.IntProperty(default=index)
def execute(self):
session = bpy.context.scene.batchapps_pools
bpy.context.scene.batchapps_session.log.debug(
"Pool details opened: {0}, selected: {1}, index {2}".format(
self.enabled,
session.selected,
self.ui_index))
if self.enabled and session.selected == self.ui_index:
session.selected = -1
else:
session.selected = self.ui_index
bpy.context.scene.batchapps_session.log.debug(
"Registering {0}".format(name))
return BatchAppsOps.register_expanding(name, label, execute,
ui_index=index_prop)

Просмотреть файл

@ -0,0 +1,193 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
@bpy.app.handlers.persistent
def on_load(*args):
"""
Handler to ensure assets list is reset inbetween .blend files.
Also resets the job file path inbetween blend files.
Run on blend file load.
"""
bpy.context.scene.batchapps_assets.path = ""
if bpy.context.scene.batchapps_session.page == "ASSETS":
bpy.ops.batchapps_assets.refresh()
def format_date(asset):
"""
Format an assets last modified date for the UI.
:Args:
- asset (:class:`batchapps.files.UserFile`): Asset whos date we
want to format.
:Returns:
- The last modified date as a string. If formatting fails,
an empty string.
"""
try:
datelist = asset.get_last_modified().split('T')
datelist[1] = datelist[1].split('.')[0]
return ' '.join(datelist)
except:
bpy.context.scene.batchapps_session.log.debug(
"Couldn't format date {0}.".format(asset.get_last_modified()))
return ""
class AssetDisplayProps(bpy.types.PropertyGroup):
"""
A display object representing an asset.
Displayed by :class:`.ui_assets.AssetListUI`.
"""
name = bpy.props.StringProperty(
description="Asset filename")
fullpath = bpy.props.StringProperty(
description="Asset full path")
upload_checkbox = bpy.props.BoolProperty(
description = "Check to upload asset",
default = False)
upload_check = bpy.props.BoolProperty(
description="Selected for upload",
default=False)
timestamp = bpy.props.StringProperty(
description="Asset last modified timestamp",
default="")
class AssetProps(bpy.types.PropertyGroup):
"""
Asset Properties,
Once instantiated, this class is set to both the Blender context, and
assigned to assets.BatchAppsAssets.props.
"""
collection = []
path = bpy.props.StringProperty(
description="Blend file path to be rendered")
temp = bpy.props.BoolProperty(
description="Whether we're using a temp blend file",
default=False)
assets = bpy.props.CollectionProperty(
type=AssetDisplayProps,
description="Asset display list")
index = bpy.props.IntProperty(
description="Selected asset index")
def add_asset(self, asset):
"""
Add an asset to both the display and object lists.
"""
log = bpy.context.scene.batchapps_session.log
log.debug("Adding asset to ui list {0}.".format(asset.name))
uploaded = asset.is_uploaded()
self.collection.append(asset)
self.assets.add()
entry = self.assets[-1]
entry.name = asset.name
entry.timestamp = format_date(asset)
entry.fullpath = asset.path
entry.upload_check = False if uploaded is None else True
log.debug("Total assets now {0}.".format(len(self.assets)))
def remove_selected(self):
"""
Remove selected asset from both display and object lists.
"""
bpy.context.scene.batchapps_session.log.debug(
"Removing index {0}.".format(self.index))
self.collection.pop(self.index)
self.assets.remove(self.index)
self.index = max(self.index - 1, 0)
def get_jobfile(self):
"""
Get the asset object whos path is the job file path.
"""
log = bpy.context.scene.batchapps_session.log
for asset in self.collection:
if asset.path == self.path:
log.debug("Found job asset at {0}".format(self.path))
return asset
else:
log.debug("Found no job asset, using {0}".format(self.path))
return self.path
def reset(self):
"""
Clear both asset display and object lists.
"""
self.collection.clear()
self.assets.clear()
self.index = 0
bpy.context.scene.batchapps_session.log.debug("Reset asset lists.")
def set_uploaded(self):
"""
Mark all assets as having been uploaded.
"""
for asset in self.assets:
asset.upload_check = True
def register_props():
"""
Register the asset property classes and assign to the blender
context under "batchapps_assets".
:Returns:
- A :class:`.AssetProps` object
"""
bpy.types.Scene.batchapps_assets = \
bpy.props.PointerProperty(type=AssetProps)
bpy.app.handlers.load_post.append(on_load)
return bpy.context.scene.batchapps_assets

Просмотреть файл

@ -0,0 +1,57 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
class AuthProps(object):
"""
Auth Properties.
Once instantiated, this class is set to both the Blender context, and
assigned to auth.BatchAppsAuth.props.
"""
thread = None
code = None
credentials = None
def register_props():
"""
Register the auth property class and assign to the blender
context under "batchapps_auth".
:Returns:
- A :class:`.AuthProps` object
"""
props_obj = AuthProps()
def get_props(self):
return props_obj
bpy.types.Scene.batchapps_auth = property(get_props)
return props_obj

Просмотреть файл

@ -0,0 +1,176 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
def format_date(job):
"""
Format a job submitted date for the UI.
:Args:
- jon (:class:`batchapps.job.SubmittedJob`): Job whos date we want to
format.
:Returns:
- The submitted date as a string. If formatting fails,
an empty string.
"""
try:
datelist = job.time_submitted.split('T')
datelist[1] = datelist[1].split('.')[0]
return ' '.join(datelist)
except:
bpy.context.scene.batchapps_session.log.debug(
"Couldn't format date {0}.".format(job.time_submitted))
return ""
class HistoryDetails(bpy.types.PropertyGroup):
"""A display object representing a job."""
id = bpy.props.StringProperty(
description="Job ID",
default="")
name = bpy.props.StringProperty(
description="Job Name",
default="")
type = bpy.props.StringProperty(
description="Job Type",
default="")
status = bpy.props.StringProperty(
description="Job Status",
default="")
timestamp = bpy.props.StringProperty(
description="Time Submitted",
default="")
percent = bpy.props.IntProperty(
description="Percent Complete",
default=0)
pool_id = bpy.props.StringProperty(
description="Job Pool ID",
default="Unknown")
tasks = bpy.props.IntProperty(
description="Number of Tasks",
default=0)
class HistoryDisplayProps(bpy.types.PropertyGroup):
"""
Display object representing a job list.
This class is added to the Blender context.
"""
selected = bpy.props.IntProperty(
description="Selected Job",
default=-1)
jobs = bpy.props.CollectionProperty(
type=HistoryDetails,
description="Job History")
per_call = bpy.props.IntProperty(
description="Jobs Per Call",
default=10,
min=1,
soft_min=1,
max=50,
soft_max=50)
total_count = bpy.props.IntProperty(
description="Total Job Count",
default=0)
index = bpy.props.IntProperty(
description="Job display index",
default=0)
icons = {
'inprogress': 'PREVIEW_RANGE',
'complete': 'FILE_TICK',
'cancelled': 'CANCEL',
'error': 'ERROR',
'notstarted': 'TIME'
}
def add_job(self, job):
"""
Add a job to the job display list.
"""
log = bpy.context.scene.batchapps_session.log
log.debug("Adding job to ui list {0}".format(job.id))
self.jobs.add()
entry = self.jobs[-1]
entry.id = job.id
entry.name = job.name
entry.type = job.type
entry.status = job.status
entry.tasks = job.number_tasks
entry.percent = job.percentage if job.percentage else 0
entry.timestamp = format_date(job)
if job.pool_id:
entry.pool_id = job.pool_id
class HistoryProps(object):
"""
History properties.
Once instantiated, this class is assigned to assets.BatchAppsAssets.props
but is not added to the Blender context.
"""
job_list = []
display = None
thread = None
def register_props():
"""
Register the history property classes and assign to the blender
context under "batchapps_history".
:Returns:
- A :class:`.HistoryProps` object
"""
props_obj = HistoryProps()
bpy.types.Scene.batchapps_history = \
bpy.props.PointerProperty(type=HistoryDisplayProps)
props_obj.display = bpy.context.scene.batchapps_history
return props_obj

Просмотреть файл

@ -0,0 +1,159 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
@bpy.app.handlers.persistent
def on_load(*args):
"""
Event handler to refresh pools when a new blender scene is opened.
Run on blend file load when page is POOLS or CREATE.
"""
if bpy.context.scene.batchapps_session.page in ["POOLS", "CREATE"]:
bpy.ops.batchapps_pools.page()
def format_date(pool):
"""
Format a pool created date for the UI.
:Args:
- pool (:class:`batchapps.pool.Pool`): Pool whos date we want to
format.
:Returns:
- The created date as a string. If formatting fails,
an empty string.
"""
try:
datelist = pool.created.split('T')
datelist[1] = datelist[1].split('.')[0]
return ' '.join(datelist)
except:
bpy.context.scene.batchapps_session.log.debug(
"Couldn't format date {0}.".format(pool.created))
return ""
class PoolDetails(bpy.types.PropertyGroup):
"""
A display object representing a pool.
his class is added to the Blender context.
"""
id = bpy.props.StringProperty(
description="Pool ID",
default="")
auto = bpy.props.BoolProperty(
description="Auto Pool or manually provisioned",
default=True)
created = bpy.props.StringProperty(
description="When pool was created",
default="")
target = bpy.props.IntProperty(
description="Pool target size",
default=0)
current = bpy.props.IntProperty(
description="Pool current size",
default=0)
state = bpy.props.StringProperty(
description="Pool State",
default="")
queue = bpy.props.IntProperty(
description="Pool Queue",
default=0)
class PoolDisplayProps(bpy.types.PropertyGroup):
"""Display object representing a pool list"""
selected = bpy.props.IntProperty(
description="Selected pool",
default=-1)
pool_size = bpy.props.IntProperty(
description="Number of instances in new pool",
default=3,
min=3,
max=20)
pools = bpy.props.CollectionProperty(
type=PoolDetails,
description="Pools currently running")
def add_pool(self, pool):
"""
Add a pool reference to the pool display list.
"""
log = bpy.context.scene.batchapps_session.log
log.debug("Adding pool to ui list {0}".format(pool.id))
self.pools.add()
entry = self.pools[-1]
entry.id = pool.id
entry.auto = pool.auto
entry.created = format_date(pool)
entry.target = pool.target_size
entry.current = pool.current_size
entry.state = pool.state
entry.queue = len(pool.jobs)
class PoolsProps(object):
"""
Pools Properties.
Once instantiated, this class is assigned to pools.BatchAppsPools.props
but is not added to the Blender context.
"""
pools = []
display = None
thread = None
def register_props():
"""
Register the pool property classes and assign to the blender
context under "batchapps_pools".
Also registers pool event handlers.
:Returns:
- A :class:`.PoolsProps` object
"""
props_obj = PoolsProps()
bpy.types.Scene.batchapps_pools = \
bpy.props.PointerProperty(type=PoolDisplayProps)
props_obj.display = bpy.context.scene.batchapps_pools
bpy.app.handlers.load_post.append(on_load)
return props_obj

Просмотреть файл

@ -0,0 +1,115 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import os
class BatchAppsPreferences(bpy.types.AddonPreferences):
"""BatchApps Blender addon user preferences."""
bl_idname = __package__.split('.')[0]
ini_file = bpy.props.StringProperty(
name="Configuration file",
description="BatchApps config file",
subtype='FILE_NAME',
default="batch_apps.ini")
data_dir = bpy.props.StringProperty(
name="Data directory",
description="Location of config file and log file",
subtype='DIR_PATH',
default=os.path.join(os.path.expanduser('~'), 'BatchAppsData'))
log_level = bpy.props.EnumProperty(items=(('10', 'Debug', ''),
('20', 'Info', ''),
('30', 'Warning', ''),
('40', 'Error', ''),
('50', 'Critical', '')),
name="Logging level",
description="Level of logging detail",
default="30")
account = bpy.props.StringProperty(
name="Unattended Account",
description="Batch Apps Unattended Account",
default="")
key = bpy.props.StringProperty(
name="Unattended Key",
description="Batch Apps Unattended Account key",
default="")
endpoint = bpy.props.StringProperty(
name="Service URL",
description="Batch Apps service endpoint",
default="")
client_id = bpy.props.StringProperty(
name="Client ID",
description="AAD Client ID",
default="")
tenant = bpy.props.StringProperty(
name="Tenant",
description="AAD auth tenant",
default="")
redirect = bpy.props.StringProperty(
name="Redirect URI",
description="AAD auth redirect URI",
default="")
def draw(self, context):
"""
Draw the display for the settings in the User Preferences
with next to the Addon entry.
:Args:
- context (bpy.types.Context): Blenders current runtime
context.
"""
layout = self.layout
layout.label(text="Blender will need to be restarted "
"for changes to take effect.")
layout.prop(self, "data_dir")
layout.prop(self, "ini_file")
layout.prop(self, "log_level")
layout.label(text="")
layout.label(text="Service Authentication configuration. "
"These settings will override those in the config file.")
layout.prop(self, "endpoint")
layout.prop(self, "account")
layout.prop(self, "key")
layout.prop(self, "client_id")
layout.prop(self, "tenant")
layout.prop(self, "redirect")

Просмотреть файл

@ -0,0 +1,206 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
@bpy.app.handlers.persistent
def framecheck(*args):
"""
Event handler to detect when global frame range has been changed
and updates addon UI accordingly.
Also issues a warning if the selected frame range falls outside the
global frame range.
Run when scene is updated and page is SUBMIT.
"""
submission = bpy.context.scene.batchapps_submission
session = bpy.context.scene.batchapps_session
if bpy.context.scene.batchapps_session.page not in ["SUBMIT"]:
return
if submission.end_f < submission.start_f:
session.log.warning("Start frame can't be greater than end frame.")
submission.end_f = submission.start_f
if ((submission.start_f < bpy.context.scene.frame_start) or
(submission.end_f > bpy.context.scene.frame_end)):
session.log.warning("Selected frame range falls outside global range.")
submission.valid_range = False
else:
submission.valid_range = True
@bpy.app.handlers.persistent
def formatcheck(*args):
"""
Event handler to detect when the global render output format has been
changed and update UI accordingly.
Also issues a warning if the selected output format is not supported.
Run when scene is updated and page is SUBMIT.
"""
submission = bpy.context.scene.batchapps_submission
session = bpy.context.scene.batchapps_session
if session.page not in ["SUBMIT"]:
return
format = bpy.context.scene.render.image_settings.file_format
if format not in submission.supported_formats:
session.log.warning("Invalid output format - using PNG instead.")
submission.valid_format = False
submission.image_format = 'PNG'
else:
submission.valid_format = True
submission.image_format = format
@bpy.app.handlers.persistent
def on_load(*args):
"""
Event handler to update the frame range when a new blender scene
has been opened.
Run on blend file load.
"""
submission = bpy.context.scene.batchapps_submission
submission.start_f = bpy.context.scene.frame_start
submission.end_f = bpy.context.scene.frame_end
class SubmissionDisplayProps(bpy.types.PropertyGroup):
"""
A display object representing a new job submission.
This class is added to the Blender context.
"""
title = bpy.props.StringProperty(
description="Job Title",
maxlen=64,
default="")
start_f = bpy.props.IntProperty(
description="Start Frame",
default=1,
min=0,
soft_min=0)
end_f = bpy.props.IntProperty(
description="End Frame",
default=1,
min=0,
soft_min=0)
image_format = bpy.props.StringProperty(
description="Image Format",
default='PNG')
supported_formats = {
'PNG': 'PNG',
'JPEG': 'JPEG',
'BMP': 'BMP',
'CINEON': 'CINEON',
'DPX': 'DPX',
'HDR': 'HDR',
'IRIS': 'IRIS',
'OPEN_EXR': 'EXR',
'TARGA': 'TGA',
'TIFF': 'TIFF',
'OPEN_EXR_MULTILAYER': 'MULTILAYER',
'TARGA_RAW': 'RAWTGA'}
number_cores = bpy.props.IntProperty(
description="Number of instances in pool",
default=3,
min=3,
max=20)
valid_range = bpy.props.BoolProperty(
description="Valid frame range",
default=True)
valid_format = bpy.props.BoolProperty(
description="Valid image format",
default=True)
pool = bpy.props.EnumProperty(
items=[("new", "Auto Pool", "Auto provision a pool for this job"),
("reuse", "Use Pool ID", "Reuse an existing persistent pool"),
("create", "Create Pool", "Create a new persistent pool")],
description="Pool on which job will run",
options={'ENUM_FLAG'},
default={"new"})
pool_id = bpy.props.StringProperty(
description="Existing Pool ID",
default="")
class SubmissionProps(object):
"""
Submission Properties.
Once instantiated, this class is assigned to submission.BatchAppsSubmission.props
but is not added to the Blender context.
"""
thread = None
display = None
def register_handlers(self):
"""
Register submission event handlers.
"""
bpy.app.handlers.load_post.append(on_load)
bpy.app.handlers.scene_update_post.append(framecheck)
bpy.app.handlers.scene_update_post.append(formatcheck)
on_load(None)
def register_props():
"""
Register the submission property classes and assign to the blender
context under "batchapps_submission".
Also registers submission event handlers.
:Returns:
- A :class:`.SubmissionProps` object
"""
props_obj = SubmissionProps()
bpy.types.Scene.batchapps_submission = \
bpy.props.PointerProperty(type=SubmissionDisplayProps)
props_obj.display = bpy.context.scene.batchapps_submission
props_obj.register_handlers()
return props_obj

Просмотреть файл

@ -0,0 +1,304 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import logging
import webbrowser
import os
from batchapps_blender.ui import ui_shared
from batchapps_blender.auth import BatchAppsAuth
from batchapps_blender.submission import BatchAppsSubmission
from batchapps_blender.assets import BatchAppsAssets
from batchapps_blender.history import BatchAppsHistory
from batchapps_blender.pools import BatchAppsPools
from batchapps_blender.utils import BatchAppsOps
from batchapps import (
JobManager,
FileManager,
PoolManager,
Configuration)
from batchapps.exceptions import InvalidConfigException
def override_config(cfg, **kwargs):
"""
Override a particular AAD setting in the config file, if it has been
set in the Blender User Preferences.
:Args:
- cfg (:class:`batchapps.Configuration`): The configuration to be
updated.
:Kwargs:
- The auth setting in the config to be changed, with it's updated
value.
:Returns:
- The updated config (:class:`batchapps.Configuration`).
"""
try:
cfg.aad_config(**kwargs)
except InvalidConfigException:
pass
finally:
return cfg
class BatchAppsSettings(object):
"""
Initializes and manages the Batch Apps addon session.
Registers all classes and handles all sub-pages.
Defines the display of the global HOME and ERROR pages.
Also configures logging and User Preferences.
"""
pages = ["HOME", "ERROR"]
def __init__(self):
self.ops = self._register_ops()
self.ui = self._register_ui()
self.props = self._register_props()
self.log = self._configure_logging()
self.cfg = self._configure_addon()
self.page = "LOGIN"
self.auth = BatchAppsAuth()
self.submission = None
self.history = None
self.assets = None
self.pools = None
if self.auth.auto_authentication(self.cfg, self.log):
self.start(self.auth.props.credentials)
def _configure_addon(self):
"""
Configures the addon based on the current User Preferences
and the supplied Batch Apps ini file configuration.
:Returns:
- A :class:`.batchapps.Configuration` object.
"""
try:
data_dir = os.path.split(self.props.data_dir)
cfg = Configuration(jobtype='Blender',
data_path=data_dir[0],
log_level=int(self.props.log_level),
name=self.props.ini_file,
datadir=data_dir[1])
except (InvalidConfigException, IndexError) as exp:
self.log.warning("Warning failed to load config file, "
"creating new default config.")
self.log.warning(str(exp))
cfg = Configuration(jobtype='Blender', log_level='warning')
finally:
if self.props.endpoint:
cfg = override_config(cfg, endpoint=self.props.endpoint)
if self.props.account:
cfg = override_config(cfg, account=self.props.account)
if self.props.key:
cfg = override_config(cfg, key=self.props.key)
if self.props.client_id:
cfg = override_config(cfg, client_id=self.props.client_id)
if self.props.tenant:
cfg = override_config(cfg, tenant=self.props.tenant)
if self.props.redirect:
cfg = override_config(cfg, redirect=self.props.redirect)
cfg.save_config()
return cfg
def _configure_logging(self):
"""
Configures the logger for the addon based on the User Preferences.
Sets up a stream handler to log to Blenders console and a file
handler to log to the Batch Apps log file.
:Returns:
- A :class:`batchapps.log.PickleLog` object.
"""
logger = logging.getLogger('BatchAppsBlender')
console_format = logging.Formatter(
"BatchApps: [%(levelname)s] %(message)s")
file_format = logging.Formatter(
"%(asctime)-15s [%(levelname)s] %(module)s: %(message)s")
console_logging = logging.StreamHandler()
console_logging.setFormatter(console_format)
logger.addHandler(console_logging)
logfile = os.path.join(self.props.data_dir, "batch_apps.log")
file_logging = logging.FileHandler(logfile)
file_logging.setFormatter(file_format)
logger.addHandler(file_logging)
logger.setLevel(int(self.props.log_level))
return logger
def _register_ops(self):
"""
Registers the shared operators with a batchapps_shared prefix.
:Returns:
- A list of the names (str) of the registered operators.
"""
ops = []
ops.append(BatchAppsOps.register("shared.home",
"Home",
self._home))
ops.append(BatchAppsOps.register("shared.management_portal",
"Management Portal",
self._management_portal))
return ops
def _register_props(self):
"""
Retrieves the shared addon properties - in this case the User
Preferences.
:Returns:
- :class:`.BatchAppsPreferences`
"""
return bpy.context.user_preferences.addons[__package__].preferences
def _register_ui(self):
"""
Matches the HOME and ERROR pages with their corresponding
ui functions.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_shared_ui(name):
name = name.lower()
return getattr(ui_shared, name)
page_func = map(get_shared_ui, self.pages)
return dict(zip(self.pages, page_func))
def _home(self, op, context):
"""
The execute method for the shared.home operator.
Sets the session page to HOME.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
self.page = "HOME"
return {'FINISHED'}
def _management_portal(self, op, context):
"""
The execute method for the shared.management_portal operator.
Opens the Management Portal in a web browser.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
webbrowser.open("https://manage.batchapps.windows.net", 2, True)
return {'FINISHED'}
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[self.page](ui, layout)
def start(self, creds):
"""
Initialize all the addon subpages after authentication is
complete.
Sets page to HOME.
:Args:
- creds (:class:`batchapps.Credentials`): Authorised credentials
with which API calls will be made.
"""
job_mgr = JobManager(creds, cfg=self.cfg)
asset_mgr = FileManager(creds, cfg=self.cfg)
pool_mgr = PoolManager(creds, cfg=self.cfg)
self.submission = BatchAppsSubmission(job_mgr, asset_mgr, pool_mgr)
self.log.debug("Initialised submission module")
self.assets = BatchAppsAssets(asset_mgr)
self.log.debug("Initialised assets module")
self.history = BatchAppsHistory(job_mgr)
self.log.debug("Initialised history module")
self.pools = BatchAppsPools(pool_mgr)
self.log.debug("Initialised pool module")
self.page = "HOME"
def redraw(self):
"""
Somewhat hacky way to force Blender to redraw the UI.
"""
bpy.context.scene.objects.active = bpy.context.scene.objects.active

Просмотреть файл

@ -0,0 +1,373 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
import logging
import random
import os
import string
import threading
import time
from batchapps_blender.ui import ui_submission
from batchapps_blender.props import props_submission
from batchapps_blender.utils import BatchAppsOps
from batchapps.exceptions import (
AuthenticationException,
InvalidConfigException)
class BatchAppsSubmission(object):
"""
Manages the creation and submission of a new job.
"""
pages = ['SUBMIT', 'PROCESSING', 'SUBMITTED']
def __init__(self, job_mgr, file_mgr, pool_mgr):
self.batchapps_job = job_mgr
self.batchapps_files = file_mgr
self.batchapps_pool = pool_mgr
self.ops = self._register_ops()
self.props = self._register_props()
self.ui = self._register_ui()
def display(self, ui, layout):
"""
Invokes the corresponding ui function depending on the session's
current page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
:Returns:
- Runs the display function for the applicable page.
"""
return self.ui[bpy.context.scene.batchapps_session.page](ui, layout)
def _register_props(self):
"""
Registers and retrieves the submission property objects.page
The dispaly properties are defined in a subclass which is assigned
to the scene.batchapps_submission context.
:Returns:
- :class:`.SubmissionProps`
"""
props = props_submission.register_props()
return props
def _register_ops(self):
"""
Registers each job submission operator with a batchapps_submission
prefix.
:Returns:
- A list of the names (str) of the registered job submission
operators.
"""
ops = []
ops.append(BatchAppsOps.register("submission.page",
"Create new job",
self._submission))
ops.append(BatchAppsOps.register("submission.start",
"Submit job",
self._start))
ops.append(BatchAppsOps.register("submission.processing",
"Submitting new job",
modal=self._processing_modal,
invoke=self._processing_invoke,
_timer=None))
return ops
def _register_ui(self):
"""
Matches the submit, processing and completed pages with their
corresponding ui functions.
:Returns:
- A dictionary mapping the page name to its corresponding
ui function.
"""
def get_ui(name):
name = name.lower()
return getattr(ui_submission, name)
page_func = map(get_ui, self.pages)
return dict(zip(self.pages, page_func))
def _processing_modal(self, op, context, event):
"""
The modal method for the submission.processing operator to handle
running the submission of a job in a separate thread to prevent
the blocking of the Blender UI.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- If the thread has completed, the Blender-specific value
{'FINISHED'} to indicate the operator has completed its action.
- Otherwise the Blender-specific value {'RUNNING_MODAL'} to
indicate the operator wil continue to process after the
completion of this function.
"""
if event.type == 'TIMER':
context.scene.batchapps_session.log.debug("SubmitThread complete.")
if not self.props.thread.is_alive():
context.window_manager.event_timer_remove(op._timer)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def _processing_invoke(self, op, context, event):
"""
The invoke method for the submission.processing operator.
Starts the job submission thread.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
- event (:class:`bpy.types.Event`): The blender invocation event.
:Returns:
- Blender-specific value {'RUNNING_MODAL'} to indicate the operator
wil continue to process after the completion of this function.
"""
self.props.thread.start()
context.scene.batchapps_session.log.debug("SubmitThread initiated.")
context.window_manager.modal_handler_add(op)
op._timer = context.window_manager.event_timer_add(1, context.window)
return {'RUNNING_MODAL'}
def _start(self, op, context, *args):
"""
The execute method for the submission.start operator.
Sets the functions to be performed by the job submission thread
and updates the session page to "PROCESSING" while the thread
executes.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
submit_thread = lambda: BatchAppsOps.session(self.submit_job)
self.props.thread = threading.Thread(name="SubmitThread",
target=submit_thread)
bpy.ops.batchapps_submission.processing('INVOKE_DEFAULT')
if context.scene.batchapps_session.page == "SUBMIT":
context.scene.batchapps_session.page = "PROCESSING"
return {'FINISHED'}
def _submission(self, op, context, *args):
"""
The execute method for the submission.page operator.
Sets the page to SUBMIT.
:Args:
- op (:class:`bpy.types.Operator`): An instance of the current
operator class.
- context (:class:`bpy.types.Context`): The current blender
context.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
"""
context.scene.batchapps_session.page = "SUBMIT"
return {'FINISHED'}
def gather_parameters(self):
"""
Gathers the operating parameters for the job.
:Returns:
- A dictionary of parameters (stR).
"""
session = bpy.context.scene.batchapps_session
params = {}
params["output"] = bpy.path.clean_name(self.props.display.title)
params["start"] = str(self.props.display.start_f)
params["end"] = str(self.props.display.end_f)
params["format"] = self.props.display.supported_formats[
self.props.display.image_format]
return params
def get_pool(self):
"""
Retrieve the pool id to be used for the job, or create an auto
pool if necessary.
:Returns:
- The pool id (string).
"""
session = bpy.context.scene.batchapps_session
pools = bpy.context.scene.batchapps_pools
pool = None
if self.props.display.pool == {"reuse"} and self.props.display.pool_id:
pool = self.props.display.pool_id
session.log.info("Using existing pool with ID: {0}".format(pool))
return pool
elif self.props.display.pool == {"create"}:
session.log.info("Creating new pool.")
pool = self.batchapps_pool.create(target_size=pools.pool_size)
session.log.info("Created pool with ID: {0}".format(pool.id))
self.props.display.pool = {"reuse"}
self.props.display.pool_id = pool.id
return pool
elif self.props.display.pool == {"new"}:
return pool
else:
raise ValueError("Invalid pool settings.")
def get_title(self):
"""
Retrieve the job title if specified, or set to "Untitled_Job".
"""
if self.props.display.title == "":
self.props.display.title = "Untitled_Job"
else:
self.props.display.title = bpy.path.clean_name(
self.props.display.title)
return self.props.display.title
def upload_assets(self, new_job):
"""
Upload all assets required by the job.
:Args:
- new_job (:class:`JobSubmission`): The job for which all assets
will be uploaded.
:Raises:
- ValueError if one or more assets fails to upload.
"""
session = bpy.context.scene.batchapps_session
session.log.info("Uploading any required files.")
failed = new_job.required_files.upload()
if failed:
[session.log.error("{0}: {1}".format(f[0], f[1])) for f in failed]
raise ValueError("Some required assets failed to upload.")
def configure_assets(self, new_job):
"""
Gather the assets required for the job and allocate the job file from
which the rendering will be run.
"""
session = bpy.context.scene.batchapps_session
assets = bpy.context.scene.batchapps_assets
if assets.path == '':
session.log.info("No assets referenced yet. Checking now.")
bpy.ops.batchapps_assets.refresh()
file_set = self.batchapps_files.create_file_set(assets.collection)
new_job.add_file_collection(file_set)
if bpy.context.scene.batchapps_assets.temp:
session.log.debug("Using temp blend file {0}".format(assets.path))
bpy.ops.wm.save_as_mainfile(filepath=assets.path,
check_existing=False,
copy=True)
jobfile = self.batchapps_files.file_from_path(assets.path)
new_job.add_file(jobfile)
new_job.set_job_file(-1)
else:
session.log.debug("Using saved blend file {0}".format(assets.path))
jobfile = bpy.context.scene.batchapps_assets.get_jobfile()
new_job.set_job_file(jobfile)
self.upload_assets(new_job)
def submit_job(self):
"""
The job submission process including the uploading of any required
assets and the instantiation of an auto-pool if necessary.
Sets the page to COMPLETE if successful.
"""
self.props.display = bpy.context.scene.batchapps_submission
session = bpy.context.scene.batchapps_session
assets = bpy.context.scene.batchapps_assets
session.log.info("Starting new job submission.")
new_job = self.batchapps_job.create_job(self.get_title())
self.configure_assets(new_job)
new_job.pool = self.get_pool()
new_job.instances = self.props.display.number_cores
new_job.params = self.gather_parameters()
new_job.params['jobfile'] = new_job.source
session.log.info("Preparation complete, submitting job.")
session.log.debug("Submission details: {0}".format(
new_job._create_job_message()))
submission = new_job.submit()
session.log.info(
"New job submitted with ID: {0}".format(submission['id']))
session.page = "SUBMITTED"
assets.set_uploaded()
session.redraw()

Просмотреть файл

@ -0,0 +1,170 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
class AssetListUI(bpy.types.UIList):
"""Ui List element for display assets"""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index, flt_flag):
"""Draw UI List"""
asset = item
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(asset.name)
if not asset.upload_check:
col = layout.column()
col.prop(asset,
"upload_checkbox",
text="",
index=index)
else:
col = layout.column()
col.label("", icon="FILE_TICK")
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
if flt_flag:
layout.enabled = False
layout.label(text="", icon_value=icon)
else:
layout.label(text="",
translate=False,
icon_value=icon)
def uilist_controls(ui, layout):
"""
Displays the buttons and labels for the UI List.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
batchapps_assets = bpy.context.scene.batchapps_assets
num_assets = len(batchapps_assets.assets)
num_display = "Displaying {0} assets".format(num_assets)
ui.label(num_display, layout.row(align=True), align='CENTER')
row = layout.row(align=True)
div = row.split()
ui.operator("assets.refresh", "Reset", div, "FILE_REFRESH")
div = row.split()
active = any(a.upload_checkbox for a in batchapps_assets.assets)
ui.operator('assets.upload', "Upload", div, "MOVE_UP_VEC", active=active)
div = row.split()
ui.operator("assets.add", "", div, "ZOOMIN")
div = row.split()
active = (num_assets > 0)
ui.operator('assets.remove', "", div, "ZOOMOUT", active=active)
def display_uilist(ui, layout):
"""
Displays the UI List that will show all collected assets.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
batchapps_assets = bpy.context.scene.batchapps_assets
outerBox = layout.box()
row = outerBox.row()
ui.label("Filename", row)
ui.label("Uploaded", row, "RIGHT")
outerBox.template_list("AssetListUI",
"",
batchapps_assets,
"assets",
batchapps_assets,
"index")
if len(batchapps_assets.assets) > 0:
display_details(ui, outerBox)
def display_details(ui, outerBox):
"""
Displays the details of the asset selected in the UI List.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- outerBox (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components. In this case the box the details are listed within.
"""
batchapps_assets = bpy.context.scene.batchapps_assets
col = outerBox.column(align=True)
if batchapps_assets.index < len(batchapps_assets.assets):
selected = batchapps_assets.assets[batchapps_assets.index]
uploaded = "Uploaded" if selected.upload_check else "Not Uploaded"
ui.label("Asset: {0}".format(selected.name), col)
ui.label("Status: {0}".format(uploaded), col)
ui.label("Date Modified: {0}".format(selected.timestamp), col)
ui.label("Full Path: {0}".format(selected.fullpath), col)
def assets(ui, layout):
"""
Display asset management page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
uilist_controls(ui, layout)
display_uilist(ui, layout)
ui.operator("shared.home", "Return Home", layout)

Просмотреть файл

@ -0,0 +1,95 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
def pre_login(ui, layout):
"""
Display login operations.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
ui.label("", layout)
ui.operator("auth.login", "Sign In", layout, "TRIA_RIGHT")
ui.label("", layout)
def post_login(ui, layout):
"""
Display pending authentication message.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
ui.label("Waiting for authentication", layout.row(), "CENTER")
ui.label("Timeout: 1 minute", layout.row(), "CENTER")
ui.label("", layout)
def login(ui, layout):
"""
Display login page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
sublayout = layout.box()
ui.label("Sign in below to start", sublayout.row(align=True), "CENTER")
ui.label("", sublayout)
pre_login(ui, sublayout)
def redirect(ui, layout):
"""
Display auth redirect pending page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
sublayout = layout.box()
ui.label("Sign in below to start", sublayout.row(align=True), "CENTER")
ui.label("", sublayout)
post_login(ui, sublayout)

Просмотреть файл

@ -0,0 +1,177 @@
#-------------------------------------------------------------------------
# Azure Batch Apps Blender Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
def status_icon(job):
"""
Get the appropriate operator icon based on a jobs status.
:Args:
- job (:class:`.HistoryDetails`): The selected job to display.
:Returns:
- The required icon name (str).
"""
status = job.status.lower()
icons = bpy.context.scene.batchapps_history.icons
return icons.get(status, "")
def details(ui, layout, job):
"""
Display the detailed information on an indivual selected job.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- job (:class:`.HistoryDetails`): The selected job to display.
"""
if job.status in ["InProgress", "Error", "Cancelled"]:
status = """Status: {0} - {1}% complete""".format(
job.status, job.percent)
else:
status = "Status: {0}".format(job.status)
ui.label(status, layout)
ui.label("Submitted: {0}".format(job.timestamp), layout)
ui.label("ID: {0}".format(job.id), layout)
ui.label("Type: {0}".format(job.type), layout)
ui.label("Number of Tasks: {0}".format(job.tasks), layout)
ui.label("Pool: {0}".format(job.pool_id), layout)
if job.status.lower() in ["notstarted", "inprogress"]:
ui.operator("history.cancel", "Cancel Job", layout)
def page_controls(ui, layout, num_jobs):
"""
Display the job history list paging controls.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- num_jobs (int): The total number of jobs in the users history.
"""
history = bpy.context.scene.batchapps_history
if history.total_count == 0:
start = 0
end = 0
elif history.total_count != 0 and history.index == 0:
start = 1
end = num_jobs
else:
start = history.index + 1
end = (start+num_jobs)-1
job_display = "Displaying jobs {start}-{end} of {total}".format(
start=start, end=end, total=history.total_count)
ui.label(job_display, layout.row(align=True), "CENTER")
split = layout.split(percentage=0.333)
row = split.row(align=True)
ui.operator("history.first", "", row, "REW", "LEFT", (history.index != 0))
ui.operator("history.less", "", row, "PREV_KEYFRAME",
"LEFT", (history.index != 0))
row = split.row(align=True)
ui.operator("history.refresh", "Refresh", row, "FILE_REFRESH", "CENTER")
row = split.row(align=True)
enabled = (history.index + num_jobs) != history.total_count
ui.operator("history.more", "", row, "NEXT_KEYFRAME", "RIGHT", enabled)
ui.operator("history.last", "", row, "FF", "RIGHT", enabled)
def history(ui, layout):
"""
Display job history page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
jobs = bpy.context.scene.batchapps_history.jobs
page_controls(ui, layout, len(jobs))
outer_box = layout.box()
if not jobs:
ui.label("No jobs to display.", outer_box.row(), "CENTER")
else:
for index, job in enumerate(jobs):
if index == bpy.context.scene.batchapps_history.selected:
inner_box = outer_box.box()
ui.operator("history."+job.id.replace("-", "_"), (" "+job.name),
inner_box, status_icon(job))
details(ui, inner_box, job)
else:
ui.operator("history."+job.id.replace("-", "_"), (" "+job.name),
outer_box, status_icon(job))
ui.label("", layout)
ui.operator("shared.home", "Return Home", layout)
def loading(ui, layout):
"""
Display job history loading page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
page_controls(ui, layout, 0)
outer_box = layout.box()
ui.label("Loading...", outer_box.row(align=True), "CENTER")
ui.label("", layout)
ui.operator("shared.home", "Return Home", layout, active=False)

Просмотреть файл

@ -0,0 +1,153 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
def details(ui, layout, pool):
"""
Display details on an individual selected pool.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- pool (:class:`.PoolDetails`): The selected pool to display.
"""
types = {True:'Auto Provisioned',
False: 'Persistent Pool'}
if not pool.auto:
split = layout.split(percentage=0.1)
ui.label("ID: ", split.row(align=True))
proprow = split.row(align=True)
proprow.active=False
ui.prop(pool, 'id', proprow)
else: ui.label("ID: {0}".format(pool.id), layout)
ui.label("Type: {0}".format(types[pool.auto]), layout)
ui.label("State: {0}".format(pool.state), layout)
ui.label("Currently running: {0} jobs".format(pool.queue), layout)
ui.label("", layout)
ui.label("Created: {0}".format(pool.created), layout)
split = layout.split(percentage=0.5)
ui.label("Target Size: {0}".format(pool.target), split.row(align=True))
ui.label("Current Size: {0}".format(pool.current), split.row(align=True))
row = layout.row(align=True)
if pool.queue > 0:
row.alert=True
ui.operator("pools.delete", "Delete Pool", row)
def display_pools(ui, layout):
"""
Display pool list.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
batchapps_pools = bpy.context.scene.batchapps_pools
icons_right = {True: 'DISCLOSURE_TRI_RIGHT_VEC', False: 'TRIA_RIGHT'}
icons_down = {True: 'DISCLOSURE_TRI_DOWN_VEC', False: 'TRIA_DOWN'}
if not batchapps_pools.pools:
ui.label("No pools found", layout)
else:
for index, pool in enumerate(batchapps_pools.pools):
if index == batchapps_pools.selected:
inner_box = layout.box()
ui.operator("pools."+pool.id.replace("-", "_"), "Hide details",
inner_box, icons_down[pool.auto])
details(ui, inner_box, pool)
else:
ui.operator("pools."+pool.id.replace("-", "_"), (' '+pool.id),
layout, icons_right[pool.auto])
def pools(ui, layout):
"""
Display pools page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
batchapps_pools = bpy.context.scene.batchapps_pools
ui.operator("pools.create", "Create New Pool", layout)
ui.label("", layout)
display_pools(ui, layout)
ui.label("", layout)
ui.operator("pools.page", "Refresh Pools", layout)
ui.operator("shared.home", "Return Home", layout)
def create(ui, layout):
"""
Display create new pool panel in pools page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
batchapps_pools = bpy.context.scene.batchapps_pools
ui.operator("pools.create", "Create New Pool", layout.row())
box = layout.box()
ui.label("New Pool", box)
ui.prop(batchapps_pools, "pool_size", box, "Pool Size")
ui.operator("pools.start", "Start Pool", box)
ui.label("", layout)
display_pools(ui, layout)
ui.label("", layout)
ui.operator("pools.page", "Refresh Pools", layout)
ui.operator("shared.home", "Return Home", layout)

Просмотреть файл

@ -0,0 +1,73 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
def home(ui, layout):
"""
Display home page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
col = layout.column()
ui.operator("submission.page", "Submit New Job", col)
ui.operator("history.page", "Jobs", col)
ui.operator("assets.page", "Assets", col)
ui.operator("pools.page", "Batch Apps Pools", col)
ui.operator("shared.management_portal", "Management Portal", col)
ui.operator("auth.logout", "Logout", col)
ui.label("", layout)
def error(ui, layout):
"""
Display error page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
sublayout = layout.box()
ui.label("An error occurred", sublayout.row(align=True), "CENTER")
ui.label("Please see console for details.", sublayout.row(align=True), "CENTER")
if bpy.context.scene.batchapps_auth.credentials:
ui.operator("shared.home", "Return Home", sublayout)
else:
ui.operator("auth.logout", "Return to Login", sublayout)
ui.label("", sublayout)

Просмотреть файл

@ -0,0 +1,218 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
def static(ui, layout, active):
"""
Display static job details reflecting global render settings.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- active (bool): Whether UI components are enabled.
"""
width = int(bpy.context.scene.render.resolution_x*\
(bpy.context.scene.render.resolution_percentage/100))
height = int(bpy.context.scene.render.resolution_y*\
(bpy.context.scene.render.resolution_percentage/100))
output = bpy.context.scene.batchapps_submission.image_format
ui.label("Width: {0}".format(width), layout.row(), active=active)
ui.label("Height: {0}".format(height), layout.row(), active=active)
ui.label("Output: {0}".format(output), layout.row(), active=active)
def variable(ui, layout, active):
"""
Display frame selection controls.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- active (bool): Whether UI components are enabled.
"""
ui.prop(bpy.context.scene.batchapps_submission, "start_f", layout.row(),
label="Start Frame ", active=active)
ui.prop(bpy.context.scene.batchapps_submission, "end_f", layout.row(),
label="End Frame ", active=active)
def pool_select(ui, layout, active):
"""
Display pool selection controls.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
- active (bool): Whether UI components are enabled.
"""
ui.label("", layout)
ui.prop(bpy.context.scene.batchapps_submission, "pool", layout.row(),
label=None, expand=True, active=active)
if bpy.context.scene.batchapps_submission.pool == {"reuse"}:
ui.label("Use an existing persistant pool by ID", layout.row(), active=active)
ui.prop(bpy.context.scene.batchapps_submission, "pool_id",
layout.row(), active=active)
elif bpy.context.scene.batchapps_submission.pool == {"create"}:
ui.label("Create a new persistant pool", layout.row(), active=active)
ui.prop(bpy.context.scene.batchapps_pools, "pool_size",
layout.row(), "Number of instances:", active=active)
else:
ui.label("Auto provision a pool for this job", layout.row(),
active=active)
ui.prop(bpy.context.scene.batchapps_submission, "number_cores",
layout.row(), "Number of instances:", active=active)
def pre_submission(ui, layout):
"""
Display any warnings before job submission.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
if not bpy.context.scene.batchapps_submission.valid_format:
ui.label("Warning: Output format {0}".format(
bpy.context.scene.render.image_settings.file_format), layout)
ui.label("not supported. Using PNG instead", layout)
row = layout.row(align=True)
row.alert=True
ui.operator("submission.start", "Submit Job", row)
elif not bpy.context.scene.batchapps_submission.valid_range:
ui.label("Warning: Selected frame range falls", layout)
ui.label("outside global render range", layout)
row = layout.row(align=True)
row.alert=True
ui.operator("submission.start", "Submit Job", row)
else:
ui.label("", layout)
ui.label("", layout)
ui.operator("submission.start", "Submit Job", layout)
ui.operator("shared.home", "Return Home", layout)
def post_submission(ui, layout):
"""
Display the job processing message.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
ui.label("Submission now processing.", layout.row(align=True), "CENTER")
ui.label("See console for progress.", layout.row(align=True), "CENTER")
ui.label("Please don't close blender.", layout.row(align=True), "CENTER")
def submit(ui, layout):
"""
Display new job submission page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
ui.prop(bpy.context.scene.batchapps_submission, "title", layout,
label="Job name ", active=True)
static(ui, layout, True)
variable(ui, layout, True)
pool_select(ui, layout, True)
pre_submission(ui, layout)
def processing(ui, layout):
"""
Display job submission processing page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
ui.prop(bpy.context.scene.batchapps_submission, "title", layout.row(),
label="Job name ", active=False)
static(ui, layout, False)
variable(ui, layout, False)
pool_select(ui, layout, False)
post_submission(ui, layout)
ui.label("", layout)
def submitted(ui, layout):
"""
Display job submitted page.
:Args:
- ui (blender :class:`.Interface`): The instance of the Interface
panel class.
- layout (blender :class:`bpy.types.UILayout`): The layout object,
derived from the Interface panel. Used for creating ui
components.
"""
sublayout = layout.box()
ui.label("Submission Successfull!", sublayout.row(align=True), "CENTER")
ui.label("", sublayout, "CENTER")
ui.operator("shared.home", "Return Home", layout)

Просмотреть файл

@ -0,0 +1,207 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
import bpy
from http.server import BaseHTTPRequestHandler
from urllib.parse import unquote
import sys
import socket
from batchapps.exceptions import SessionExpiredException
class BatchAppsOps(object):
"""
Static class for registering operators and executing them in a
error-safe way.
"""
@staticmethod
def session(func, *args, **kwargs):
"""
Execute an operator function.
Can be invoke, execute, modal or a thread function.
:Args:
- func (function): The function to be executed along with
any args and kwargs.
:Returns:
- Blender-specific value {'FINISHED'} to indicate the operator has
completed its action.
- If a :class:`batchapps.exceptions.SessionExpiredException` is
raised, returns {'CANCELLED'}.
"""
session = bpy.context.scene.batchapps_session
try:
return func(*args, **kwargs)
except SessionExpiredException:
session.log.error(
"Warning: Session Expired - please log back in again.")
session.page = "LOGIN"
session.redraw()
return {'CANCELLED'}
except Exception as exp:
session.page = "ERROR"
session.log.error("Error occurred: {0}".format(exp))
session.redraw()
return {'CANCELLED'}
@staticmethod
def register(name, label, execute=None, modal=None, invoke=None, **kwargs):
"""
Register a custom operator.
:Args:
- name (str): The id name of the operator (bl_idname).
- label (str): The description of the operator (bl_label).
:Kwargs:
- execute (func): The execute function if applicable.
- modal (func): The modal function if applicable.
- invoke (func): The invoke function if applicable.
- Any additional attributes or functions to be added to the class.
:Returns:
- The ID name of the registered operator with the
prefix ``batchapps_``.
"""
name = "batchapps_" + str(name)
op_spec = {"bl_idname": name, "bl_label": label}
if execute:
def op_execute(self, context):
return BatchAppsOps.session(execute, self, context)
op_spec["execute"] = op_execute
if modal:
def op_modal(self, context, event):
return BatchAppsOps.session(modal, self, context, event)
op_spec["modal"] = op_modal
if invoke:
def op_invoke(self, context, event):
return BatchAppsOps.session(invoke, self, context, event)
op_spec["invoke"] = op_invoke
op_spec.update(kwargs)
new_op = type("BatchAppsOp",
(bpy.types.Operator, ),
op_spec)
bpy.utils.register_class(new_op)
return name
@staticmethod
def register_expanding(name, label, execute, modal=None,
invoke=None, **kwargs):
"""
Register an operator that can be used as an expanding UI
component (for example a job in the history page).
:Args:
- name (str): The id name of the operator (bl_idname).
- label (str): The description of the operator (bl_label).
- execute (func): The execute function.
:Kwargs:
- modal (func): The modal function if applicable.
- invoke (func): The invoke function if applicable.
- Any additional attributes or functions to be added to the class.
:Returns:
- The ID name of the registered operator with the
prefix ``batchapps_``.
"""
kwargs.update({'enabled':bpy.props.BoolProperty(default=False)})
def op_execute(self, context):
execute(self)
self.enabled = not self.enabled
return {'FINISHED'}
return BatchAppsOps.register(name, label, op_execute, modal,
invoke, **kwargs)
class OAuthRequestHandler(BaseHTTPRequestHandler):
"""
A custom HTTP server request handler to handler the AAD redirects
for authentication.
"""
def log_message(self, format, *args):
"""
Override logging to silence messages from base class.
"""
return
def do_GET(s):
"""
Handle a GET request from the AAD server. If a code is returned,
assigns to the batchapps_auth.code property and returns status 200
along with a simple HTML message.
If an error is returned, assigns to properties for logging the
returns status 401 and an HTML message.
"""
session = bpy.context.scene.batchapps_session
session.log.debug("Received AAD request {0}".format(s.path))
if s.path.startswith('/?code'):
bpy.context.scene.batchapps_auth.code = s.path
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()
s.wfile.write(b"<html><head><title>Authentication Successful</title></head>")
s.wfile.write(b"<body><p>Authentication successful.</p>")
s.wfile.write(b"<p>You can now return to Blender where your log in</p>")
s.wfile.write(b"<p>will be complete in just a moment.</p>")
s.wfile.write(b"</body></html>")
else:
bpy.context.scene.batchapps_auth.code = s.path
s.send_response(401)
s.send_header("Content-type", "text/html")
s.end_headers()
s.wfile.write(b"<html><head><title>Authentication Failed</title></head>")
s.wfile.write(b"<body><p>Authentication unsuccessful.</p>")
s.wfile.write(b"<p>Check the Blender console for details.</p>")
s.wfile.write(b"</body></html>")

69
Blender.Client/package.py Normal file
Просмотреть файл

@ -0,0 +1,69 @@
#-------------------------------------------------------------------------
# The Blender Batch Apps Sample
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the ""Software""), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
#--------------------------------------------------------------------------
"""Zip up Blender Addon"""
import sys
import os
import subprocess
import shutil
import zipfile
VERSION = "0.1.0"
def main():
"""Build Blender Addon package"""
python_exe = os.path.join(sys.prefix, "python.exe")
if not os.path.exists(python_exe):
print("Cannot find python.exe at path: {0}".format(python_exe))
return
print("Building package...")
package_dir = os.path.abspath("build")
if not os.path.isdir(package_dir):
try:
os.mkdir(package_dir)
except:
print("Cannot create build dir at path: {0}".format(package_dir))
return
package = os.path.join(package_dir, "batchapps-blender-{0}.zip".format(VERSION))
source = os.path.abspath("batchapps_blender")
with zipfile.ZipFile(package, mode='w') as blend_zip:
for root, dirs, files in os.walk(source):
if root.endswith("__pycache__"):
continue
for file in files:
blend_zip.write(os.path.relpath(os.path.join(root, file)))
print("Package complete!")
if __name__ == '__main__':
main()

Просмотреть файл

@ -0,0 +1,45 @@
///-------------------------------------------------------------------------
/// The Blender Batch Apps Sample
///
/// Copyright (c) Microsoft Corporation. All rights reserved.
///
/// The MIT License (MIT)
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the ""Software""), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
///The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
///
///--------------------------------------------------------------------------
using System;
using System.Linq;
using Microsoft.Azure.Batch.Apps.Cloud;
namespace Blender.Cloud
{
public class ApplicationDefinition
{
public static readonly CloudApplication Application = new ParallelCloudApplication
{
ApplicationName = "Blender",
JobType = "Blender",
JobSplitterType = typeof(BlenderJobSplitter),
TaskProcessorType = typeof(BlenderTaskProcessor)
};
}
}

Просмотреть файл

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5C019C8B-B5E5-4B98-9D75-8B5D7E6C83D0}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Blender.Cloud</RootNamespace>
<AssemblyName>Blender.Cloud</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Azure.Batch.Apps.Cloud">
<HintPath>..\packages\Microsoft.Azure.Batch.Apps.Cloud.1.0.0\lib\net45\Microsoft.Azure.Batch.Apps.Cloud.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ApplicationDefinition.cs" />
<Compile Include="Exceptions\InvalidParameterException.cs" />
<Compile Include="Exceptions\NoOutputsFoundException.cs" />
<Compile Include="Exceptions\ZipException.cs" />
<Compile Include="JobSplitter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="BlenderParameters.cs" />
<Compile Include="TaskProcessor.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

Просмотреть файл

@ -0,0 +1,242 @@
///-------------------------------------------------------------------------
/// The Blender Batch Apps Sample
///
/// Copyright (c) Microsoft Corporation. All rights reserved.
///
/// The MIT License (MIT)
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the ""Software""), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
///The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
///
///--------------------------------------------------------------------------
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Azure.Batch.Apps.Cloud;
namespace Blender.Cloud
{
public abstract class BlenderParameters
{
public static readonly IList<String> SupportedFormats = new List<String> { ".png", ".bmp", ".jpg", ".tga", ".exr" };
public abstract bool Valid { get; }
public abstract int Start { get; }
public abstract int End { get; }
public abstract string JobFile { get; }
public abstract string Prefix { get; }
public abstract string Format { get; }
public abstract string ErrorText { get; }
public static BlenderParameters FromJob(IJob job)
{
var errors = new List<string>();
int start = GetInt32Parameter(job.Parameters, "start", errors);
int end = GetInt32Parameter(job.Parameters, "end", errors);
string jobfile = GetStringParameter(job.Parameters, "jobfile", errors);
string prefix = GetStringParameter(job.Parameters, "output", errors);
string format = GetStringParameter(job.Parameters, "format", errors);
if (errors.Any())
{
return new InvalidBlenderParameters(string.Join(Environment.NewLine, errors.Select(e => "* " + e)));
}
return new ValidBlenderParameters(start, end, jobfile, prefix, format);
}
public static BlenderParameters FromTask(ITask task)
{
var errors = new List<string>();
string jobfile = GetStringParameter(task.Parameters, "jobfile", errors);
string prefix = GetStringParameter(task.Parameters, "output", errors);
string format = GetStringParameter(task.Parameters, "format", errors);
if (errors.Any())
{
return new InvalidBlenderParameters(string.Join(Environment.NewLine, errors.Select(e => "* " + e)));
}
return new ValidBlenderParameters(jobfile, prefix, format);
}
private static int GetInt32Parameter(IDictionary<string,string> parameters, string parameterName, List<string> errors)
{
int value = 0;
try
{
string text = parameters[parameterName];
value = int.Parse(text, CultureInfo.InvariantCulture);
if (value < 0)
{
errors.Add(parameterName + " parameter is not a positive integer");
}
}
catch (KeyNotFoundException)
{
errors.Add(parameterName + " parameter not specified");
}
catch (FormatException)
{
errors.Add(parameterName + " parameter is not a valid integer");
}
catch (Exception ex)
{
errors.Add("Unexpected error reading parameter " + parameterName + ": " + ex.Message);
}
return value;
}
private static string GetStringParameter(IDictionary<string, string> parameters, string parameterName, List<string> errors)
{
string text = "";
try
{
text = parameters[parameterName];
}
catch (KeyNotFoundException)
{
errors.Add(parameterName + " parameter not specified");
}
catch (Exception ex)
{
errors.Add("Unexpected error reading parameter " + parameterName + ": " + ex.Message);
}
return text;
}
private class ValidBlenderParameters : BlenderParameters
{
private readonly int _start;
private readonly int _end;
private readonly string _jobfile;
private readonly string _prefix;
private readonly string _format;
public ValidBlenderParameters(int start, int end, string jobfile, string prefix, string format)
{
_start = start;
_end = end;
_jobfile = jobfile;
_prefix = prefix;
_format = format;
}
public ValidBlenderParameters(string jobfile, string prefix, string format)
{
_jobfile = jobfile;
_prefix = prefix;
_format = format;
}
public override bool Valid
{
get { return true; }
}
public override int Start
{
get { return _start; }
}
public override int End
{
get { return _end; }
}
public override string JobFile
{
get { return _jobfile; }
}
public override string Prefix
{
get { return _prefix; }
}
public override string Format
{
get { return _format; }
}
public override string ErrorText
{
get { throw new InvalidOperationException("ErrorText does not apply to valid parameters"); }
}
}
private class InvalidBlenderParameters : BlenderParameters
{
private readonly string _errorText;
public InvalidBlenderParameters(string errorText)
{
_errorText = errorText;
}
public override bool Valid
{
get { return false; }
}
public override int Start
{
get { throw new InvalidOperationException("Start does not apply to invalid parameters"); }
}
public override int End
{
get { throw new InvalidOperationException("End does not apply to invalid parameters"); }
}
public override string JobFile
{
get { throw new InvalidOperationException("JobFile does not apply to invalid parameters"); }
}
public override string Prefix
{
get { throw new InvalidOperationException("Prefix does not apply to invalid parameters"); }
}
public override string Format
{
get { throw new InvalidOperationException("Format does not apply to invalid parameters"); }
}
public override string ErrorText
{
get { return _errorText; }
}
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace Blender.Cloud.Exceptions
{
[Serializable]
public class InvalidParameterException : Exception
{
public InvalidParameterException()
{
}
public InvalidParameterException(string message)
: base(message)
{
}
protected InvalidParameterException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace Blender.Cloud.Exceptions
{
[Serializable]
public class NoOutputsFoundException : Exception
{
public NoOutputsFoundException()
{
}
public NoOutputsFoundException(string message)
: base(message)
{
}
protected NoOutputsFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace Blender.Cloud.Exceptions
{
[Serializable]
public class ZipException : Exception
{
public ZipException()
{
}
public ZipException(string message)
: base(message)
{
}
public ZipException(string message, Exception innerException)
: base(message, innerException)
{
}
protected ZipException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

Просмотреть файл

@ -0,0 +1,71 @@
///-------------------------------------------------------------------------
/// The Blender Batch Apps Sample
///
/// Copyright (c) Microsoft Corporation. All rights reserved.
///
/// The MIT License (MIT)
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the ""Software""), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
///The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
///
///--------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Threading.Tasks;
using Blender.Cloud.Exceptions;
using Microsoft.Azure.Batch.Apps.Cloud;
namespace Blender.Cloud
{
public class BlenderJobSplitter : JobSplitter
{
/// <summary>
/// Splits a job into more granular tasks to be processed in parallel.
/// </summary>
/// <param name="job">The job to be split.</param>
/// <param name="settings">Contains information and services about the split request.</param>
/// <returns>A sequence of tasks to be run on compute nodes.</returns>
protected override IEnumerable<TaskSpecifier> Split(IJob job, JobSplitSettings settings)
{
var jobParameters = BlenderParameters.FromJob(job);
if (!jobParameters.Valid)
{
Log.Error(jobParameters.ErrorText);
throw new InvalidParameterException(jobParameters.ErrorText);
}
for (var i = jobParameters.Start; i <= jobParameters.End; i++)
{
yield return new TaskSpecifier
{
TaskIndex = i,
Parameters = job.Parameters,
RequiredFiles = job.Files,
};
}
}
}
}

Просмотреть файл

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Blender.Cloud")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Blender.Cloud")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("240340f5-790c-471e-b643-9b50744129b6")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Просмотреть файл

@ -0,0 +1,277 @@
///-------------------------------------------------------------------------
/// The Blender Batch Apps Sample
///
/// Copyright (c) Microsoft Corporation. All rights reserved.
///
/// The MIT License (MIT)
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the ""Software""), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
///The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
///
///--------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Globalization;
using System.IO.Compression;
using System.Threading.Tasks;
using System.Diagnostics;
using Blender.Cloud.Exceptions;
using Microsoft.Azure.Batch.Apps.Cloud;
namespace Blender.Cloud
{
public class BlenderTaskProcessor : ParallelTaskProcessor
{
/// <summary>
/// Path to the Blender executable
/// </summary>
private string RenderPath
{
get { return @"Blender\blender.exe"; }
}
/// <summary>
/// Args with which to run Blender
/// </summary>
private string RenderArgs
{
get { return @"-b ""{0}"" -o ""{1}_####"" -F {2} -f {3} -t 0"; }
}
/// <summary>
/// Executes the external process for processing the task
/// </summary>
/// <param name="task">The task to be processed.</param>
/// <param name="settings">Contains information about the processing request.</param>
/// <returns>The result of task processing.</returns>
protected override TaskProcessResult RunExternalTaskProcess(ITask task, TaskExecutionSettings settings)
{
var taskParameters = BlenderParameters.FromTask(task);
var initialFiles = CollectFiles(LocalStoragePath);
if (!taskParameters.Valid)
{
Log.Error(taskParameters.ErrorText);
return new TaskProcessResult
{
Success = TaskProcessSuccess.PermanentFailure,
ProcessorOutput = "Parameter error: " + taskParameters.ErrorText,
};
}
var inputFile = LocalPath(taskParameters.JobFile);
var outputFile = LocalPath(taskParameters.Prefix);
string externalProcessPath = ExecutablePath(RenderPath);
string externalProcessArgs = string.Format(CultureInfo.InvariantCulture, RenderArgs, inputFile, outputFile, taskParameters.Format, task.TaskIndex);
Log.Info("Calling '{0}' with Args '{1}' for Task '{2}' / Job '{3}' .", RenderPath, externalProcessArgs, task.TaskId, task.JobId);
var processResult = ExecuteProcess(externalProcessPath, externalProcessArgs);
if (processResult == null)
{
return new TaskProcessResult { Success = TaskProcessSuccess.RetryableFailure };
}
var newFiles = GetNewFiles(initialFiles, LocalStoragePath);
var result = TaskProcessResult.FromExternalProcessResult(processResult, newFiles);
var thumbnail = CreateThumbnail(task, newFiles);
if (!string.IsNullOrEmpty(thumbnail))
{
var taskPreview = new TaskOutputFile
{
FileName = thumbnail,
Kind = TaskOutputFileKind.Preview
};
result.OutputFiles.Add(taskPreview);
}
return result;
}
/// <summary>
/// Method to execute the external processing for merging the tasks output into job output
/// </summary>
/// <param name="mergeTask">The merge task.</param>
/// <param name="settings">Contains information about the processing request.</param>
/// <returns>The job outputs resulting from the merge process.</returns>
protected override JobResult RunExternalMergeProcess(ITask mergeTask, TaskExecutionSettings settings)
{
var taskParameters = BlenderParameters.FromTask(mergeTask);
if (!taskParameters.Valid)
{
Log.Error(taskParameters.ErrorText);
throw new InvalidParameterException(taskParameters.ErrorText);
}
var inputFilter = string.Format( CultureInfo.InvariantCulture, "{0}*", taskParameters.Prefix);
var inputFiles = CollectFiles(LocalStoragePath, inputFilter);
var outputFile = LocalPath("output.zip");
var result = ZipOutputs(inputFiles, outputFile);
result.PreviewFile = CreateThumbnail(mergeTask, inputFiles.ToArray());
return result;
}
/// <summary>
/// Retrieve a list of files that currently exist in a given location, accoding to a
/// given naming pattern.
/// </summary>
/// <param name="location">The directory to list the contents of.</param>
/// <param name="pattern">The naming convention the returned files names adhere to.</param>
/// <returns>A HashSet of the paths of the files in the directory.</returns>
private static HashSet<string> CollectFiles(string location, string pattern = "*")
{
return new HashSet<string>(Directory.GetFiles(location, pattern));
}
/// <summary>
/// Performs a difference between the current contents of a directory and a supplied file list.
/// </summary>
/// <param name="oldFiles">Set of file paths to compare.</param>
/// <param name="location">Path to the directory.</param>
/// <returns>An array of files paths in the directory that do not appear in the supplied set.</returns>
private static string[] GetNewFiles(HashSet<string> oldFiles, string location)
{
var filesNow = CollectFiles(location);
filesNow.RemoveWhere(oldFiles.Contains);
filesNow.RemoveWhere(f => f.EndsWith(".temp"));
filesNow.RemoveWhere(f => f.EndsWith(".stdout"));
filesNow.RemoveWhere(f => f.EndsWith(".log"));
filesNow.RemoveWhere(f => f.EndsWith(".xml"));
return filesNow.ToArray();
}
/// <summary>
/// Zips up a supplied list of output files.
/// </summary>
/// <param name="inputs">List of files to include in the zip.</param>
/// <param name="output">The output zip file path.</param>
/// <returns>A JobResult with the new zip assigned as OutputFile.</returns>
private static JobResult ZipOutputs(HashSet<string> inputs, string output)
{
if (inputs.Count < 1)
{
throw new NoOutputsFoundException("No job outputs found.");
}
try
{
using (ZipArchive outputs = ZipFile.Open(output, ZipArchiveMode.Create))
{
foreach (var input in inputs)
{
outputs.CreateEntryFromFile(input, Path.GetFileName(input), CompressionLevel.Optimal);
}
}
return new JobResult { OutputFile = output };
}
catch (Exception ex)
{
var error = string.Format("Failed to zip outputs: {0}", ex.ToString());
throw new ZipException(error, ex);
}
}
/// <summary>
/// Create an image thumbnail for the task. If supplied image format is incompatible, no thumb
/// will be created and no error thrown.
/// </summary>
/// <param name="task">The task that needs a thumbnail.</param>
/// <param name="inputName">The task output from which to generate the thumbnail.</param>
/// <returns>The path to the new thumbnail if created, else an empty string.</returns>
protected string CreateThumbnail(ITask task, string[] inputs)
{
var filtered = inputs.Where(x => BlenderParameters.SupportedFormats.Contains(Path.GetExtension(x)));
if (filtered.Count() < 1)
{
Log.Info("No thumbnail compatible images found.");
return string.Empty;
}
var thumbInput = filtered.First();
var thumbOutput = LocalPath(string.Format("{0}_{1}_thumbnail.png", task.JobId, task.TaskIndex));
var thumbnailerPath = ExecutablePath(@"ImageMagick\convert.exe");
var thumbnailerArgs = string.Format(@"""{0}"" -thumbnail 200x150> ""{1}""", thumbInput, thumbOutput);
var processResult = ExecuteProcess(thumbnailerPath, thumbnailerArgs);
if (processResult != null)
{
Log.Info("Generated thumbnail from {0} at {1}", thumbInput, thumbOutput);
return thumbOutput;
}
Log.Info("No thumbnail generated");
return string.Empty;
}
/// <summary>
/// Run a, executable with a given set of arguments.
/// </summary>
/// <param name="exePath">Path the executable.</param>
/// <param name="exeArgs">The commandline arguments.</param>
/// <returns>The ExternalProcessResult if run successfully, or null if an error was thrown.</returns>
private ExternalProcessResult ExecuteProcess(string exePath, string exeArgs)
{
var process = new ExternalProcess
{
CommandPath = exePath,
Arguments = exeArgs,
WorkingDirectory = LocalStoragePath
};
try
{
return process.Run();
}
catch (ExternalProcessException ex)
{
string outputInfo = "No program output";
if (!string.IsNullOrEmpty(ex.StandardError) || !string.IsNullOrEmpty(ex.StandardOutput))
{
outputInfo = Environment.NewLine + "stderr: " + ex.StandardError + Environment.NewLine + "stdout: " + ex.StandardOutput;
}
Log.Error("Failed to invoke command {0} {1}: exit code was {2}. {3}", ex.CommandPath, ex.Arguments, ex.ExitCode, outputInfo);
return null;
}
catch (Exception ex)
{
Log.Error("Error in task processor: {0}", ex.ToString());
return null;
}
}
}
}

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Azure.Batch.Apps.Cloud" version="1.0.0" targetFramework="net45" />
</packages>

1
CHANGES.txt Normal file
Просмотреть файл

@ -0,0 +1 @@
v0.1.0, 2014-12-xx - Beta Release.

25
LICENSE.txt Normal file
Просмотреть файл

@ -0,0 +1,25 @@
The Blender Batch Apps Sample ver. 0.1.0
Copyright (c) Microsoft Corporation
All rights reserved.
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the ""Software""), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

221
README.rst Normal file
Просмотреть файл

@ -0,0 +1,221 @@
===============================
Azure Batch Apps Blender Sample
===============================
Microsoft Azure Batch Apps is an Azure service offering on-demand capacity for compute-intensive workloads.
This is a sample project utilizing the Azure Batch Apps SDK and the Azure Batch Apps Python client to show how
one could set up a cloud-based rendering platform using Blender.
The sample involves two parts, the cloud assembly project, and the Blender client project for job submission.
For more information on Batch Apps concepts, terms, and project structure `check out this article <http://azure.microsoft.com/en-us/documentation/articles/batch-dotnet-get-started/#tutorial2>`_.
The client project is a python 'Addon' for Blender, to allow for a seamless user experience for submitting render
jobs to the cloud from within Blender.
License
========
This project is licensed under the MIT License.
For details see LICENSE.txt or visit `<http://opensource.org/licenses/MIT>`_.
Blender
========
Blender is a free and open source 3D animation suite.
It supports the entirety of the 3D pipeline—modeling, rigging, animation, simulation, rendering, compositing and motion tracking, even video editing and game creation.
Advanced users employ Blenders API for Python scripting to customize the application and write specialized tools; often these are included in Blenders future releases.
Blender is well suited to individuals and small studios who benefit from its unified pipeline and responsive development process.
For more information and to download Blender, visit `<http://www.blender.org>`_.
Set up
======
In order to build the projects you will need to have the following tools:
- `Microsoft Azure Batch Apps Cloud SDK <http://www.nuget.org/packages/Microsoft.Azure.Batch.Apps.Cloud/>`_
- `Python Tools for Visual Studio <http://pytools.codeplex.com/>`_
- `Azure Batch Apps Python Client and it's required packages <https://github.com/Azure/azure-batch-apps-python>`_
Part 1. Blender.Cloud
======================
This project builds a cloud assembly for running rendering jobs using Blender.
A "cloud assembly" is a zip file containing an application-specific DLL with logic for splitting
jobs into tasks, and for executing each of those tasks. In this sample, we split the job into
a task for each frame to be rendered, and execute each task by running the blender.exe program.
The cloud assembly goes hand in hand with an "application image," a zip file
containing the program or programs to be executed. In this sample, we have used Blender and
ImageMagick in the application image.
Building the Cloud Assembly
---------------------------
To build the cloud assembly zip file:
1. Build the Blender.Cloud project.
2. Open the output folder of the Blender.Cloud project.
3. Select all the DLLs (and optionally PDB files) in the output folder.
4. Right-click and choose Send To > Compressed Folder.
Building the Application Image
-------------------------------
The application image contains the following applications:
* `Blender <http://www.blender.org/download/>`_ - The application we want to cloud-enable.
It is easiest in this scenario to download as a zip rather than the installer.
* `ImageMagick <http://www.imagemagick.org/script/binary-releases.php#windows>`_- (Optional) Tool for creating preview thumbnails
of the rendered frames. Locate the `portable Win32 static build <http://www.imagemagick.org/download/binaries/ImageMagick-6.9.0-2-Q16-x86-windows.zip>`_.
To build the application image zip file:
1. Open http://www.blender.org/download/.
2. Locate the 64bit zip download for the latest Blender release. `Direct download of 2.72b here <http://mirror.cs.umn.edu/blender.org/release/Blender2.72/blender-2.72b-windows64.zip>`_.
3. Extract the subfolder (blender-2.7x-windows64) to a location of choice and rename it 'Blender'.
4. Open http://www.imagemagick.org/script/binary-releases.php#windows
5. Locate the portable Win32 static build. It is important to use the portable build! `Direct
download of 6.9.0-3 here <http://www.imagemagick.org/download/binaries/ImageMagick-6.9.0-2-Q16-x86-windows.zip>`_.
6. Extract the subfolder (ImageMagick-6.x.x) next to the earlier 'Blender' directory, and rename it to 'ImageMagick'.
7. Select both the 'Blender' and 'ImageMagick' directories, right-click and choose Send To > Compressed Folder.
8. Rename the resulting zip file to Blender.zip
The final application image zip file should have the following structure:
Blender.zip
|
| -- Blender
| |
| | -- blender.exe
| | -- 2.7x directory
| | -- other Blender components
|
| -- ImageMagick
|
| -- convert.exe
| -- other ImageMagick components
Uploading the Application to Your Batch Apps Service
-----------------------------------------------------
1. Open the Azure management portal (manage.windowsazure.com).
2. Select Batch Services in the left-hand menu.
3. Select your service in the list and click "Manage Batch Apps." This opens the Batch Apps management
portal.
4. Select Services in the left-hand menu.
5. Select your service in the list and click View Details.
6. Choose the Manage Applications tab.
7. Click New Application.
8. Under "Select and upload a cloud assembly," choose your cloud assembly zip file and click Upload.
9. Under "Select and upload an application image," choose your application image zip file and click Upload.
(Be sure to leave the version as "default".)
10. Click Done.
Part 2. Blender.Client
=======================
No that the Blender rendering service is configured in Batch Apps, we need a way to submit Blender files
to be rendered.
The sample client is an Addon for Blender written in Python, that can be used on multiple platforms.
Python Setup
-------------
The Addon requires some additional Python packages in order to run.
By default, Blender is shipped with it's own Python environment, so it's into this environment that these
packages will need to be installed.
There are several approaches one could take:
- If there is already an installation of Python 3.4 on the machine, one can use pip to install the required
packages, then copy them into the Blender bundled Python environment.
Source: ~\Python34\Lib\site-packages -> Destination: ~\Blender Foundation\blender\2.7x\python\lib\site-packages
- Download the packages directly from `<pypi.python.org>`_, and copy them into the Blender bundled Python environment.
Destination: ~\Blender Foundation\blender\2.7x\python\lib\site-packages
The required packages are the following:
- `Batch Apps Python Client <https://pypi.python.org/pypi/azure-batch-apps>`_
- `Keyring <https://pypi.python.org/pypi/keyring>`_
- `OAuthLib <https://pypi.python.org/pypi/oauthlib>`_
- `Requests-OAuthLib <https://pypi.python.org/pypi/requests-oauthlib>`_
- Note: additional package `Requests <https://pypi.python.org/pypi/requests>`_ already comes bundled with Blender.
Building and Installing the Addon
----------------------------------
Set Blender.Client as the 'Start-up Project' in Visual Studio.
By running the Blender.Client project, the Addon will be packaged into a Zip located in Blender.Client\build
To install the Addon:
1. Run Blender
2. Open File -> User Preferences
3. Navigate to the Addons tab
4. Click 'Install from File...' at the bottom of the dialog window.
5. Navigate to and select the packaged client zip.
6. The Addon 'Batch Apps Blender' will now be registered under the 'Render' category. Once located, select the
check box to activate the Addon.
7. Once activated, the Addon UI will appear in the 'Render Properties' panel - by default, in the lower right corner
of the screen.
Addon Logging and Configuration
--------------------------------
The sample addon logs to both Blenders stdout and to file.
By default this log file will be saved to $HOME/BatchAppsData. This directory is also the location of the Addon
configuration file.
This directory, the config file to use, and the level of logging detail are all configurable within the Blender UI.
The authentication configuration settings of the file can also be overridden in the Blender UI.
1. Run Blender
2. Open File -> User Preferences
3. Navigate to the Addons tab
4. Either search for 'Batch Apps Blender', or navigate to the Addon under the 'Render' category.
5. Select the arrow next to the Addon to open the details drop down - here you will find info on the version and installation directory.
6. Listed here you will also find the configuration preferences. If modified, click 'Save User Settings' at the bottom
of the dialog window (Note: this will also cause the Batch Apps Blender Addon to be activated on Blender start-up).
7. Once saved, restart Blender for the changes to take effect.
Authentication
---------------
To run this addon you will need:
- Your Batch Apps service URL
- Unattended account credentials for your Batch Apps service
1. Open the Azure management portal (manage.windowsazure.com).
2. Select Batch Services in the left-hand menu.
3. Select your service in the list and click "Manage Batch Apps." This opens the Batch Apps management
portal.
4. Select Services in the left-hand menu.
5. Select your service in the list and click View Details.
6. Copy the service URL from the page and paste it into the 'Service URL' field in the Blender User Preferences.
7. Click the Unattended Account button at the bottom of the page.
8. Copy the Account ID from the page and paste it into the 'Unattended Account' field in the Blender User Preferences.
9. Below the Account Keys list, select "3 months" and click the Add Key button.
Copy the generated key and paste it into the project App.config UnattendedAccountKey field.
NOTE: the generated key will be shown only once! If you accidentally close the page
before copying the key, just reopen it and add a new key.
Addon Documentation
--------------------
The Addon User Guide can be found `here <PENDING_URL>`_.
Auto generated Sphinx documentation for the Addon code can be found `here <PENDING_URL>`_.

Двоичные данные
docs.zip Normal file

Двоичный файл не отображается.