Adding project files to project template

This commit is contained in:
2024-03-27 09:55:27 +01:00
parent 23aaf60b09
commit 87105a06c1
22 changed files with 2690 additions and 1 deletions

14
.idea/PushBlendPull.iml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts">
<sourceFolder url="file://$MODULE_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.6 (PushBlendPull)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

70
.idea/markdown-navigator.xml generated Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownProjectSettings">
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
<PanelProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
</PanelProvider>
</PreviewSettings>
<ParserSettings>
<PegdownExtensions>
<option name="ABBREVIATIONS" value="false" />
<option name="ANCHORLINKS" value="true" />
<option name="ASIDE" value="false" />
<option name="ATXHEADERSPACE" value="true" />
<option name="AUTOLINKS" value="true" />
<option name="DEFINITIONS" value="false" />
<option name="FENCED_CODE_BLOCKS" value="true" />
<option name="FOOTNOTES" value="false" />
<option name="HARDWRAPS" value="false" />
<option name="INSERTED" value="false" />
<option name="QUOTES" value="false" />
<option name="RELAXEDHRULES" value="true" />
<option name="SMARTS" value="false" />
<option name="STRIKETHROUGH" value="true" />
<option name="SUBSCRIPT" value="false" />
<option name="SUPERSCRIPT" value="false" />
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
<option name="SUPPRESS_INLINE_HTML" value="false" />
<option name="TABLES" value="true" />
<option name="TASKLISTITEMS" value="true" />
<option name="TOC" value="false" />
<option name="WIKILINKS" value="true" />
</PegdownExtensions>
<ParserOptions>
<option name="COMMONMARK_LISTS" value="false" />
<option name="DUMMY" value="false" />
<option name="EMOJI_SHORTCUTS" value="true" />
<option name="FLEXMARK_FRONT_MATTER" value="false" />
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="true" />
<option name="GFM_TABLE_RENDERING" value="true" />
<option name="GITBOOK_URL_ENCODING" value="false" />
<option name="GITHUB_EMOJI_URL" value="false" />
<option name="GITHUB_LISTS" value="true" />
<option name="GITHUB_WIKI_LINKS" value="true" />
<option name="JEKYLL_FRONT_MATTER" value="false" />
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
</ParserOptions>
</ParserSettings>
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true">
<GeneratorProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
</GeneratorProvider>
<headerTop />
<headerBottom />
<bodyTop />
<bodyBottom />
</HtmlSettings>
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssTextEnabled="false" isDynamicPageWidth="true">
<StylesheetProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
</StylesheetProvider>
<ScriptProviders />
<cssText />
</CssSettings>
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" />
<LinkMapSettings>
<textMaps />
</LinkMapSettings>
</component>
</project>

View File

@@ -0,0 +1,3 @@
<component name="MarkdownNavigator.ProfileManager">
<settings default="" pdf-export="" />
</component>

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (PushBlendPull)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/PushBlendPull.iml" filepath="$PROJECT_DIR$/.idea/PushBlendPull.iml" />
</modules>
</component>
</project>

940
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,940 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="2dd0ebfd-c64a-47fb-842f-d234330d06b7" name="Default" comment="" />
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="TRACKING_ENABLED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileEditorManager">
<splitter split-orientation="horizontal" split-proportion="0.4599529">
<split-first>
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
<file leaf-file-name="dispatcher.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="170">
<caret line="10" column="19" lean-forward="false" selection-start-line="10" selection-start-column="19" selection-end-line="10" selection-end-column="19" />
<folding>
<element signature="e#482#718#0" expanded="false" />
<element signature="e#2807#2905#0" expanded="false" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="client.py" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/client.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="27">
<caret line="65" column="28" lean-forward="true" selection-start-line="65" selection-start-column="28" selection-end-line="65" selection-end-column="28" />
<folding>
<element signature="e#0#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="epollreactor.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/internet/epollreactor.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3689">
<caret line="227" column="0" lean-forward="false" selection-start-line="227" selection-start-column="0" selection-end-line="227" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="threading.py" pinned="false" current-in-tab="false">
<entry file="file:///usr/lib/python3.6/threading.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8160">
<caret line="486" column="6" lean-forward="false" selection-start-line="486" selection-start-column="6" selection-end-line="486" selection-end-column="6" />
<folding />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="_pydev_execfile.py" pinned="false" current-in-tab="false">
<entry file="file://$APPLICATION_HOME_DIR$/helpers/pydev/_pydev_imps/_pydev_execfile.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="289">
<caret line="17" column="0" lean-forward="false" selection-start-line="17" selection-start-column="0" selection-end-line="17" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="resource.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/resource.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="6919">
<caret line="407" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="407" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="http.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/http.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="14042">
<caret line="826" column="32" lean-forward="false" selection-start-line="826" selection-start-column="32" selection-end-line="826" selection-end-column="32" />
<folding />
</state>
</provider>
</entry>
</file>
<file leaf-file-name="test_dispatcher.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/tests/test_dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1139">
<caret line="78" column="43" lean-forward="false" selection-start-line="78" selection-start-column="43" selection-end-line="78" selection-end-column="43" />
<folding>
<element signature="e#0#9#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="README.md" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="238">
<caret line="14" column="27" lean-forward="false" selection-start-line="14" selection-start-column="27" selection-end-line="14" selection-end-column="27" />
<folding />
</first_editor>
<second_editor>
<js_state />
</second_editor>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="worker.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8092">
<caret line="476" column="0" lean-forward="false" selection-start-line="476" selection-start-column="0" selection-end-line="476" selection-end-column="0" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
</leaf>
</split-first>
<split-second>
<leaf SIDE_TABS_SIZE_LIMIT_KEY="300">
<file leaf-file-name="worker.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3077">
<caret line="283" column="104" lean-forward="false" selection-start-line="283" selection-start-column="104" selection-end-line="283" selection-end-column="104" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="test.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/test.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2910">
<caret line="228" column="8" lean-forward="false" selection-start-line="228" selection-start-column="8" selection-end-line="229" selection-end-column="62" />
<folding>
<element signature="e#0#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="test_dispatcher.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/tests/test_dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1938">
<caret line="114" column="17" lean-forward="false" selection-start-line="114" selection-start-column="17" selection-end-line="114" selection-end-column="17" />
<folding>
<element signature="e#0#9#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="dispatcher.py" pinned="false" current-in-tab="true">
<entry file="file://$PROJECT_DIR$/dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-3186">
<caret line="262" column="29" lean-forward="false" selection-start-line="262" selection-start-column="29" selection-end-line="262" selection-end-column="29" />
<folding>
<element signature="e#482#718#0" expanded="false" />
<element signature="e#2807#2905#0" expanded="false" />
</folding>
</state>
</provider>
</entry>
</file>
<file leaf-file-name="client.py" pinned="false" current-in-tab="false">
<entry file="file://$PROJECT_DIR$/client.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="50" column="72" lean-forward="false" selection-start-line="50" selection-start-column="72" selection-end-line="50" selection-end-column="72" />
<folding>
<element signature="e#0#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
</file>
</leaf>
</split-second>
</splitter>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="FindInProjectRecents">
<findStrings>
<find>queue</find>
<find>remo</find>
<find>htt</find>
<find>dec</find>
<find>canceled</find>
<find>http</find>
<find>class</find>
<find>cgi</find>
<find>render</find>
<find>Job</find>
<find>curre</find>
<find>rege</find>
<find>get_worker</find>
<find>don</find>
<find>time</find>
<find>worker</find>
<find>process_joblist</find>
<find>onli</find>
<find>loc</find>
<find>str(</find>
<find>proc</find>
<find>synch</find>
<find>debu</find>
<find>pol</find>
<find>done</find>
<find>Pop</find>
<find>RLock</find>
<find>time.</find>
<find>col</find>
<find>layou</find>
</findStrings>
<replaceStrings>
<replace>action_queue</replace>
</replaceStrings>
</component>
<component name="IdeDocumentHistory">
<option name="CHANGED_PATHS">
<list>
<option value="$PROJECT_DIR$/server.py" />
<option value="$PROJECT_DIR$/tests/test_dispatcher.py" />
<option value="$PROJECT_DIR$/dispatcher.py" />
<option value="$PROJECT_DIR$/worker.py" />
<option value="$PROJECT_DIR$/test.py" />
<option value="$PROJECT_DIR$/README.md" />
<option value="$PROJECT_DIR$/client.py" />
</list>
</option>
</component>
<component name="ProjectFrameBounds" extendedState="6">
<option name="x" value="1920" />
<option name="width" value="1920" />
<option name="height" value="1050" />
</component>
<component name="ProjectView">
<navigator currentView="ProjectPane" proportions="" version="1">
<flattenPackages />
<showMembers />
<showModules />
<showLibraryContents />
<hideEmptyPackages />
<abbreviatePackageNames />
<autoscrollToSource />
<autoscrollFromSource />
<sortByType />
<manualOrder />
<foldersAlwaysOnTop value="true" />
</navigator>
<panes>
<pane id="ProjectPane">
<subPane>
<expand>
<path>
<item name="PushBlendPull" type="b2602c69:ProjectViewProjectNode" />
<item name="PushBlendPull" type="47feb1d3:ProjectViewModuleNode" />
</path>
<path>
<item name="PushBlendPull" type="b2602c69:ProjectViewProjectNode" />
<item name="PushBlendPull" type="47feb1d3:ProjectViewModuleNode" />
<item name="PushBlendPull" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="PushBlendPull" type="b2602c69:ProjectViewProjectNode" />
<item name="PushBlendPull" type="47feb1d3:ProjectViewModuleNode" />
<item name="PushBlendPull" type="462c0819:PsiDirectoryNode" />
<item name="tests" type="462c0819:PsiDirectoryNode" />
</path>
<path>
<item name="PushBlendPull" type="b2602c69:ProjectViewProjectNode" />
<item name="PushBlendPull" type="47feb1d3:ProjectViewModuleNode" />
<item name="scripts" type="462c0819:PsiDirectoryNode" />
</path>
</expand>
<select />
</subPane>
</pane>
<pane id="Scratches" />
<pane id="Scope" />
</panes>
</component>
<component name="PropertiesComponent">
<property name="last_opened_file_path" value="$PROJECT_DIR$/../scripts" />
<property name="settings.editor.selected.configurable" value="preferences.sourceCode.Python" />
</component>
<component name="PyConsoleOptionsProvider">
<option name="myPythonConsoleState">
<console-settings module-name="PushBlendPull" is-module-sdk="true">
<option name="myUseModuleSdk" value="true" />
<option name="myModuleName" value="PushBlendPull" />
</console-settings>
</option>
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="RunManager" selected="Python.dispatcher">
<configuration name="dispatcher" type="PythonConfigurationType" factoryName="Python">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/../../python/Envs/PushBlendPull/bin/python" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="PushBlendPull" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/dispatcher.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
</configuration>
<configuration name="test_dispatcher" type="PythonConfigurationType" factoryName="Python" temporary="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/tests" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="PushBlendPull" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/tests/test_dispatcher.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
</configuration>
<configuration name="worker" type="PythonConfigurationType" factoryName="Python" temporary="true">
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<module name="PushBlendPull" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/worker.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
</configuration>
<list size="3">
<item index="0" class="java.lang.String" itemvalue="Python.dispatcher" />
<item index="1" class="java.lang.String" itemvalue="Python.worker" />
<item index="2" class="java.lang.String" itemvalue="Python.test_dispatcher" />
</list>
<recent_temporary>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="Python.test_dispatcher" />
<item index="1" class="java.lang.String" itemvalue="Python.worker" />
</list>
</recent_temporary>
</component>
<component name="ShelveChangesManager" show_recycled="false">
<option name="remove_strategy" value="false" />
</component>
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="2dd0ebfd-c64a-47fb-842f-d234330d06b7" name="Default" comment="" />
<created>1514719771907</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1514719771907</updated>
</task>
<servers />
</component>
<component name="ToolWindowManager">
<frame x="1920" y="0" width="1920" height="1050" extended-state="6" />
<editor active="true" />
<layout>
<window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.104902476" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" />
<window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
<window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.3991323" sideWeight="0.44860306" order="7" side_tool="true" content_ui="tabs" />
<window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="false" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Python Console" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.3394794" sideWeight="0.85714287" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.3286334" sideWeight="0.4391144" order="2" side_tool="false" content_ui="tabs" />
<window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.32910052" sideWeight="0.49791667" order="7" side_tool="false" content_ui="tabs" />
<window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" />
<window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.3991323" sideWeight="0.55139697" order="3" side_tool="false" content_ui="tabs" />
<window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
<window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
<window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
<window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" />
<window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
<window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
</layout>
</component>
<component name="VcsContentAnnotationSettings">
<option name="myLimit" value="2678400000" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file:///usr/lib/python3.6/socketserver.py</url>
<line>318</line>
<option name="timeStamp" value="37" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/worker.py</url>
<line>311</line>
<option name="timeStamp" value="107" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/worker.py</url>
<line>133</line>
<option name="timeStamp" value="114" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/dispatcher.py</url>
<line>241</line>
<option name="timeStamp" value="169" />
</line-breakpoint>
</breakpoints>
<breakpoints-dialog>
<breakpoints-dialog />
</breakpoints-dialog>
<option name="time" value="171" />
</breakpoint-manager>
<watches-manager />
</component>
<component name="debuggerHistoryManager">
<expressions id="evaluateExpression">
<expression>
<expression-string>event_data</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>self._download_idle</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>reactor.getThreadPool()</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>reactor.getThreadPool().dumpStats()</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>reactor.getThreadPoll()</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>p.stdout.readline()</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>self.get_http_address(self.path_status)</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>'&gt;'.join(job)</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
<expression>
<expression-string>job.join('&gt;')</expression-string>
<language-id>Python</language-id>
<evaluation-mode>EXPRESSION</evaluation-mode>
</expression>
</expressions>
</component>
<component name="editorHistoryManager">
<entry file="file://$PROJECT_DIR$/test.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/Téléchargements/pydev-blender/doc/python_api/pypredef/aud.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/client.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="10" lean-forward="false" selection-start-line="0" selection-start-column="10" selection-end-line="0" selection-end-column="10" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/resource.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="4233">
<caret line="249" column="0" lean-forward="false" selection-start-line="249" selection-start-column="0" selection-end-line="249" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2771">
<caret line="163" column="41" lean-forward="false" selection-start-line="159" selection-start-column="8" selection-end-line="163" selection-end-column="41" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="4403">
<caret line="262" column="67" lean-forward="false" selection-start-line="262" selection-start-column="67" selection-end-line="262" selection-end-column="67" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/http/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2397">
<caret line="141" column="57" lean-forward="false" selection-start-line="141" selection-start-column="57" selection-end-line="141" selection-end-column="57" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="153">
<caret line="9" column="0" lean-forward="false" selection-start-line="9" selection-start-column="0" selection-end-line="9" selection-end-column="0" />
<folding />
</first_editor>
<second_editor>
<js_state />
</second_editor>
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/socketserver.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="13107">
<caret line="775" column="35" lean-forward="false" selection-start-line="775" selection-start-column="35" selection-end-line="775" selection-end-column="35" />
<folding />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/threading.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="14569">
<caret line="863" column="0" lean-forward="false" selection-start-line="863" selection-start-column="0" selection-end-line="863" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/test.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2907">
<caret line="188" column="36" lean-forward="false" selection-start-line="188" selection-start-column="36" selection-end-line="188" selection-end-column="36" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/http/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2397">
<caret line="141" column="57" lean-forward="false" selection-start-line="141" selection-start-column="57" selection-end-line="141" selection-end-column="57" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3604">
<caret line="224" column="0" lean-forward="false" selection-start-line="224" selection-start-column="0" selection-end-line="224" selection-end-column="0" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/socketserver.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8143">
<caret line="483" column="0" lean-forward="false" selection-start-line="483" selection-start-column="0" selection-end-line="483" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/threading.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="14671">
<caret line="863" column="0" lean-forward="false" selection-start-line="863" selection-start-column="0" selection-end-line="863" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
<folding />
</first_editor>
<second_editor>
<js_state />
</second_editor>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/test.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3077">
<caret line="198" column="35" lean-forward="false" selection-start-line="198" selection-start-column="35" selection-end-line="198" selection-end-column="35" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/socketserver.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="12716">
<caret line="752" column="0" lean-forward="false" selection-start-line="752" selection-start-column="0" selection-end-line="752" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/http/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2108">
<caret line="141" column="57" lean-forward="false" selection-start-line="141" selection-start-column="57" selection-end-line="141" selection-end-column="57" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/threading.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="14569">
<caret line="863" column="0" lean-forward="false" selection-start-line="863" selection-start-column="0" selection-end-line="863" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3009">
<caret line="189" column="50" lean-forward="false" selection-start-line="189" selection-start-column="49" selection-end-line="189" selection-end-column="50" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/socketserver.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="13107">
<caret line="775" column="35" lean-forward="false" selection-start-line="775" selection-start-column="35" selection-end-line="775" selection-end-column="35" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/Téléchargements/pydev-blender/doc/python_api/pypredef/aud.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/http/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="17">
<caret line="141" column="57" lean-forward="false" selection-start-line="141" selection-start-column="57" selection-end-line="141" selection-end-column="57" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/Téléchargements/pydev-blender/doc/python_api/pypredef/bpy.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts/modules/bpy/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts/modules/bpy_types.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts/modules/bpy_extras/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/bpy/2.79/scripts/modules/bpy/ops.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3842">
<caret line="226" column="0" lean-forward="false" selection-start-line="226" selection-start-column="0" selection-end-line="226" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/blendfile/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="179">
<caret line="740" column="0" lean-forward="false" selection-start-line="740" selection-start-column="0" selection-end-line="740" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/.PyCharmCE2017.3/system/python_stubs/-1694100972/builtins.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="94639">
<caret line="5567" column="6" lean-forward="false" selection-start-line="5567" selection-start-column="6" selection-end-line="5567" selection-end-column="6" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/subprocess.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="21726">
<caret line="1278" column="0" lean-forward="false" selection-start-line="1278" selection-start-column="0" selection-end-line="1278" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/internet/defer.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="179">
<caret line="652" column="0" lean-forward="false" selection-start-line="652" selection-start-column="0" selection-end-line="652" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/_responses.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="179">
<caret line="34" column="0" lean-forward="false" selection-start-line="34" selection-start-column="0" selection-end-line="34" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/sseclient/__init__.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-1258">
<caret line="25" column="8" lean-forward="false" selection-start-line="25" selection-start-column="8" selection-end-line="25" selection-end-column="8" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/requests/adapters.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1551">
<caret line="507" column="0" lean-forward="false" selection-start-line="507" selection-start-column="0" selection-end-line="507" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/cgi.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="68">
<caret line="4" column="26" lean-forward="false" selection-start-line="4" selection-start-column="26" selection-end-line="4" selection-end-column="26" />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/queue.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="64">
<caret line="203" column="0" lean-forward="true" selection-start-line="203" selection-start-column="0" selection-end-line="203" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$USER_HOME$/.PyCharmCE2017.3/system/python_stubs/-1694100972/_collections.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="71">
<caret line="169" column="0" lean-forward="true" selection-start-line="169" selection-start-column="0" selection-end-line="169" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/server.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3247">
<caret line="194" column="0" lean-forward="false" selection-start-line="194" selection-start-column="0" selection-end-line="194" selection-end-column="0" />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/http.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="14042">
<caret line="826" column="32" lean-forward="false" selection-start-line="826" selection-start-column="32" selection-end-line="826" selection-end-column="32" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$APPLICATION_HOME_DIR$/helpers/pydev/_pydev_imps/_pydev_execfile.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="289">
<caret line="17" column="0" lean-forward="false" selection-start-line="17" selection-start-column="0" selection-end-line="17" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/web/resource.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="6919">
<caret line="407" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="407" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/../../python/Envs/PushBlendPull/lib/python3.6/site-packages/twisted/internet/epollreactor.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3689">
<caret line="227" column="0" lean-forward="false" selection-start-line="227" selection-start-column="0" selection-end-line="227" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file:///usr/lib/python3.6/threading.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="8160">
<caret line="486" column="6" lean-forward="false" selection-start-line="486" selection-start-column="6" selection-end-line="486" selection-end-column="6" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/tests/test_dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="1938">
<caret line="114" column="17" lean-forward="false" selection-start-line="114" selection-start-column="17" selection-end-line="114" selection-end-column="17" />
<folding>
<element signature="e#0#9#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/README.md">
<provider selected="true" editor-type-id="split-provider[text-editor;MarkdownPreviewEditor]">
<state split_layout="SPLIT">
<first_editor relative-caret-position="238">
<caret line="14" column="27" lean-forward="false" selection-start-line="14" selection-start-column="27" selection-end-line="14" selection-end-column="27" />
<folding />
</first_editor>
<second_editor>
<js_state />
</second_editor>
</state>
</provider>
<provider editor-type-id="text-editor">
<state relative-caret-position="0">
<caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
<folding />
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/worker.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="3077">
<caret line="283" column="104" lean-forward="false" selection-start-line="283" selection-start-column="104" selection-end-line="283" selection-end-column="104" />
<folding>
<element signature="e#1#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/test.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="2910">
<caret line="228" column="8" lean-forward="false" selection-start-line="228" selection-start-column="8" selection-end-line="229" selection-end-column="62" />
<folding>
<element signature="e#0#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/client.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="187">
<caret line="50" column="72" lean-forward="false" selection-start-line="50" selection-start-column="72" selection-end-line="50" selection-end-column="72" />
<folding>
<element signature="e#0#10#0" expanded="true" />
</folding>
</state>
</provider>
</entry>
<entry file="file://$PROJECT_DIR$/dispatcher.py">
<provider selected="true" editor-type-id="text-editor">
<state relative-caret-position="-3186">
<caret line="262" column="29" lean-forward="false" selection-start-line="262" selection-start-column="29" selection-end-line="262" selection-end-column="29" />
<folding>
<element signature="e#482#718#0" expanded="false" />
<element signature="e#2807#2905#0" expanded="false" />
</folding>
</state>
</provider>
</entry>
</component>
</project>

View File

@@ -1,3 +1,26 @@
# PushBlendPull # PushBlendPull
Blender plugin that allows you to deport the actual blending of your project on another computer. Blender plugin that allows you to deport the actual blending of your project on another computer.
## Requirements:
1. Worker:
* twisted (requires python header)
## Bugs:
## Improvements:
1. Worker
* Clean after cancel
2. Dispatcher
* Accept workload parameter
* Cancel Job/Farm(redispactch)/All
* Only sync/ only blend
* Exchange only needed files
* P2P file exchange
* Slave mode
3. Plugin
* Push/Pull to remote dispatcher
* Allow farm configuration/file exchange
* Track rendered/canceld scenes/blendfiles

158
client.py Normal file
View File

@@ -0,0 +1,158 @@
import bpy
from bpy.types import Panel, Operator, PropertyGroup, UIList
from bpy.props import (StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
EnumProperty,
PointerProperty,
CollectionProperty
)
class PBPBlendFileProperty(PropertyGroup):
path = StringProperty(subtype="FILE_PATH")
filename = StringProperty(subtype="FILE_NAME")
blending_enabled = BoolProperty(
name="Enable or Disable",
description="Enable blend file for blending",
default=False)
status = StringProperty(
name="Status",
description="Status of current file",
default=""
)
class PBPWorkerProperty(PropertyGroup):
hostname = StringProperty()
port = StringProperty()
remote_project_path = StringProperty()
blending_enabled = BoolProperty(
name="Enable or Disable",
description="Enable farm for blending",
default=False)
status = StringProperty(
name="Status",
description="Status of current file",
default=""
)
class PBPProperties(PropertyGroup):
project_folder = StringProperty(
name="Project folder",
description="Path to project folder",
default="//",
subtype="DIR_PATH"
)
worker_list = CollectionProperty(type=PBPWorkerProperty)
worker_list_index = IntProperty(name="Index for my_list", default=0)
blend_file_list = CollectionProperty(type=PBPBlendFileProperty)
upload_status = StringProperty(
name="Upload status",
description="Status of current upload",
default=""
)
class RENDER_UL_workers(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.label(item.hostname)
class PBPProjectPanel(Panel):
bl_idname = "RENDER_PT_PBP_project_panel"
bl_label = "PBP Workers Settings"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "render"
def draw(self, context):
layout = self.layout
# layout.operator(ProjectManagerStartBlend.bl_idname)
configuration = context.window_manager.push_blend_pull
layout.label(text="Workers")
row = layout.row()
col = row.column()
col.template_list("RENDER_UL_workers", "", configuration, "worker_list", configuration, "worker_list_index", rows=2)
col = row.column()
sub = col.column(align=True)
sub.operator("render.pbp_add_worker", icon='ZOOMIN', text="")
sub.operator("render.pbp_del_worker", icon='ZOOMOUT', text="")
class PBPWorkersPanel(Panel):
bl_idname = "RENDER_PT_PBP_workers_panel"
bl_label = "PBP Workers Settings"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "render"
def draw(self, context):
layout = self.layout
# layout.operator(ProjectManagerStartBlend.bl_idname)
configuration = context.window_manager.push_blend_pull
layout.label(text="Workers")
row = layout.row()
col = row.column()
col.template_list("RENDER_UL_workers", "", configuration, "worker_list", configuration, "worker_list_index", rows=2)
col = row.column()
sub = col.column(align=True)
sub.operator("render.pbp_add_worker", icon='ZOOMIN', text="")
sub.operator("render.pbp_del_worker", icon='ZOOMOUT', text="")
class PBPAddWorkerOperator(Operator):
bl_idname = "render.pbp_add_worker"
bl_label = "Add a worker"
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self, width=600)
def draw(self, context):
configuration = context.window_manager.push_blend_pull
new_worker = configuration.worker_list.add()
layout = self.layout
row = layout.row(align=True)
row.prop(new_worker, "hostname")
row.prop(new_worker, "port")
row.prop(new_worker, "remote_project_path")
row.prop(new_worker, "blending_enabled")
class PBPDelWorkerOperator(Operator):
bl_idname = "render.pbp_del_worker"
bl_label = "Remove a worker"
def execute(self, context):
return {'FINISHED'}
def register():
bpy.utils.register_class(RENDER_UL_workers)
bpy.utils.register_module(__name__)
bpy.types.WindowManager.push_blend_pull = PointerProperty(type=PBPProperties)
def unregister():
bpy.utils.unregister_module(__name__)
del bpy.types.WindowManager.push_blend_pull
if __name__ == "__main__":
register()

574
dispatcher.py Normal file
View File

@@ -0,0 +1,574 @@
import os
import re
import math
import json
import logging
from threading import Thread, Event, RLock
from subprocess import Popen, PIPE, STDOUT
import requests
from requests.exceptions import ConnectionError
from twisted.internet import reactor
from twisted.web import server, resource, http
import sseclient
import blendfile as bf
logging.basicConfig(level=logging.INFO)
PROJECT_PATH = "/home/ewandor/projects/dev/blender/PushBlendPull"
class Index(resource.Resource):
isLeaf = False
def getChild(self, path, request):
if path == b'':
return self
return resource.Resource.getChild(self, path, request)
def render_GET(self, request):
return get_index_content()
class WorkpileResource(resource.Resource):
isLeaf = True
PARAM_WORKERS = b'workers'
PARAM_WORKPILE = b'workpile'
PARAM_PROJECT_PATH = b'project_path'
@classmethod
def get_param_workers(cls, request):
if cls.PARAM_WORKERS not in request.args:
raise ValueError
regex_worker = "^(?P<server>[^:]+(:\d+)?[^:]*):(?P<remote_path>.+)$"
workers = []
for worker in request.args[cls.PARAM_WORKERS]:
match = re.search(regex_worker, worker.decode("utf-8"))
if match is None:
raise ValueError
workers.append((match.group('server'), match.group('remote_path'),))
return workers
@classmethod
def get_param_workpile(cls, request):
if cls.PARAM_WORKPILE not in request.args:
raise ValueError
workpile = []
for work in request.args[cls.PARAM_WORKPILE]:
result = work.decode("utf-8").split('>')
if len(result) != 2:
raise ValueError
workpile.append(tuple(result))
return workpile
@classmethod
def get_param_project_path(cls, request):
if cls.PARAM_PROJECT_PATH not in request.args:
raise ValueError
project_path = request.args[cls.PARAM_PROJECT_PATH][0].decode("utf-8")
if not os.path.isdir(project_path):
raise FileNotFoundError
return project_path
def render_POST(self, request):
try:
logging.info('Request Handler: Received a new work pile')
stats = JobDispatcher.process_workpile(
self.get_param_workers(request),
self.get_param_project_path(request),
self.get_param_workpile(request)
)
return b'OK'
except ValueError:
return resource.ErrorPage(http.BAD_REQUEST, "Bad request", "Error in the request's format")
except FileNotFoundError:
return resource.NoResource("Blend file not found")
pass
class StatusResource(resource.Resource):
isLeaf = True
def render_GET(self, request):
return json.dumps(get_status()).encode()
class Job:
STATUS_QUEUED = 'queued'
STATUS_DONE = 'done'
STATUS_CANCELED = 'canceled'
def __init__(self, blend_file, scene, start_frame, end_frame, local_project_path, remote_project_path):
self.blend_file = blend_file
self.scene = scene
self.start_frame = start_frame
self.end_frame = end_frame
self.local_project_path = local_project_path
self.remote_project_path = remote_project_path
self.current_frame = 0
self.status = self.STATUS_QUEUED
def __str__(self):
return "{}>{} {}:{}".format(
self.blend_file,
self.scene if self.scene is not None else "default_scene",
self.start_frame if self.start_frame is not None else "start",
self.end_frame if self.end_frame is not None else "end"
)
def get_http_payload(self):
result = {'blend_file': self.blend_file}
if self.scene is not None:
result['scene'] = self.scene
if self.start_frame is not None:
result['start_frame'] = self.start_frame
if self.end_frame is not None:
result['end_frame'] = self.end_frame
return result
def __eq__(self, other):
if isinstance(other, str):
return other == str(self)
return other == self.blend_file and other.scene == self.scene \
and other.start_frame == self.start_frame \
and other.end_frame == self.end_frame
class Worker:
path_status = '/status'
path_job = '/job'
path_cancel = '/cancel'
worker_pool = []
finished_jobs = []
@classmethod
def get_worker(cls, authority):
try:
worker = cls.worker_pool[cls.worker_pool.index(authority)]
except ValueError:
worker = Worker(authority)
return worker
def __init__(self, authority):
self.authority = authority
self.job_queue = []
self.idle = True
self._upload_lock = RLock()
self._download_lock = RLock()
self.upload_status = None
self.download_status = None
self.worker_pool.append(self)
def get_http_address(self, path):
return 'http://' + self.authority + path
def check_online(self):
try:
requests.get(self.get_http_address(self.path_status))
except ConnectionError:
return False
return True
def process_joblist(self, project_path, remote_path, joblist):
reactor.callInThread(self._do_process_joblist, project_path, remote_path, joblist)
#Thread(target=self._do_process_joblist, args=(project_path, remote_path, joblist,)).start()
def _do_process_joblist(self, project_path, remote_path, joblist):
logging.info('Worker<{}>: Synchronizing worker'.format(self))
self.push_to(project_path, remote_path)
for job in joblist:
self.enqueue_job(job)
def push_to(self, project_path, remote_path):
self._upload_lock.acquire()
for upload_status in RemoteSynchronizer.push_to_worker(self, project_path, remote_path):
self.upload_status = upload_status
self.upload_status = None
self._upload_lock.release()
def pull_from(self, project_path, remote_path):
self._download_lock.acquire()
for download_status in RemoteSynchronizer.pull_from_worker(self, project_path, remote_path):
self.download_status = download_status
self.download_status = None
self._download_lock.release()
def enqueue_job(self, job):
self.job_queue.append(job)
if self.idle:
ready_event = Event()
Thread(target=self.start_listening, args=(ready_event,)).start()
ready_event.wait()
logging.info('Worker<{}>: Sending {}'.format(self, job))
requests.post(self.get_http_address(self.path_job), data=job.get_http_payload())
def start_listening(self, ready_event):
response = requests.get(
self.get_http_address(self.path_status),
stream=True,
headers={'accept': 'text/event-stream'}
)
status_output = sseclient.SSEClient(response)
stop = False
self.idle = False
ready_event.set()
for event in status_output.events():
event_data = json.loads(event.data)
if event_data[1] == Job.STATUS_CANCELED:
if event_data[0] in self.job_queue:
job = self.job_queue.pop(self.job_queue.index(event_data[0]))
job.status = Job.STATUS_CANCELED
self.finished_jobs.append(job)
elif event_data[0] == self.job_queue[0]:
if event_data[1] == Job.STATUS_DONE:
job = self.job_queue.pop(0)
job.status = Job.STATUS_DONE
self.finished_jobs.append(job)
logging.info('Worker<{}>: Job done {}, starting syncing local files'.format(self, job))
Thread(target=self.pull_from, args=(job.local_project_path, job.remote_project_path)).start()
if len(self.job_queue) == 0:
stop = True
elif event_data[1][:4] == 'Fra:':
self.job_queue[0].current_frame = int(event_data[1][4:])
if stop:
status_output.close()
self.idle = True
return
def __eq__(self, other):
if isinstance(other, str):
return other == self.authority
return other.authority == self.authority
def __str__(self):
return self.authority
def __hash__(self):
return self.authority.__hash__()
class RemoteSynchronizer:
@classmethod
def push_to_worker(cls, worker, local_path, remote_path):
address = worker.authority.split(':')[0]
destination = "{}:{}".format(address, remote_path)
return cls._do_sync(local_path, destination, ['*.blend1', 'README.md'])
@classmethod
def pull_from_worker(cls, worker, local_path, remote_path):
address = worker.authority.split(':')[0]
source = "{}:{}".format(address, remote_path)
return cls._do_sync(source, local_path, ['*.blend'])
@classmethod
def _do_sync(cls, source, destination, exclude_list):
args = ['rsync', '-azP', '--info=progress2']
for exclude_patern in exclude_list:
args.append('--exclude')
args.append(exclude_patern)
args.append(source)
args.append(destination)
p = Popen(args, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=1)
last_line = ''
while True:
line = p.stdout.readline()
if line == '':
if p.poll() is not None:
break
elif line != last_line:
logging.debug('Synch: stdout: {}'.format(line))
last_line = line
status = cls.handleLine(line)
if status is not None:
yield status
REGEX_PUSHPULL_STATUS = "\d+%"
@classmethod
def handleLine(cls, line, current_file=None):
if line == 'end':
return line
t = re.search(cls.REGEX_PUSHPULL_STATUS, line)
if t is not None:
return t[0]
def get_status():
workers_status = {}
subjobs_status = {}
for worker in Worker.worker_pool:
for subjob in worker.finished_jobs:
jobkey = (subjob.blend_file, subjob.scene)
if jobkey not in subjobs_status:
subjobs_status[jobkey] = []
subjobs_status[jobkey].append(subjob)
job_queue_status = []
for subjob in worker.job_queue:
job_queue_status.append(get_subjob_stat(subjob))
jobkey = (subjob.blend_file, subjob.scene)
if jobkey not in subjobs_status:
subjobs_status[jobkey] = []
subjobs_status[jobkey].append(subjob)
workers_status[worker.authority] = {
'upload_status': worker.upload_status,
'download_status':worker.download_status,
'job_queue': job_queue_status
}
jobs_status = {}
for job, subjobs in subjobs_status.items():
total_frame = 0
frame_done = 0
for subjob in subjobs:
total_frame += subjob.end_frame - subjob.start_frame + 1
frame_done += subjob.current_frame - subjob.start_frame + 1
jobs_status['>'.join(job)] = {'frame_done': frame_done, 'total_frame': total_frame}
return {
'project_path': PROJECT_PATH,
'workers': workers_status,
'jobs': jobs_status
}
def get_subjob_stat(subjob):
return {
'name': str(subjob),
'current_frame': subjob.current_frame,
'total_frame': subjob.end_frame - subjob.start_frame + 1
}
class JobDispatcher:
@staticmethod
def process_workpile(worker_list, project_path, workpile):
checked_worker_list = []
remote_paths = {}
for worker_param in worker_list:
worker = Worker.get_worker(worker_param[0])
if worker.check_online():
checked_worker_list.append((worker, worker_param[1],))
dispatched_jobs = {w: [] for w in checked_worker_list}
for blend_file, scene in workpile:
dispatch = JobDispatcher.dispatch_scene_to_workers(
checked_worker_list,
project_path, blend_file, scene)
for worker, job in dispatch.items():
dispatched_jobs[worker].append(job)
for (worker, joblist) in dispatched_jobs.items():
worker[0].process_joblist(project_path, worker[1], joblist)
return dispatched_jobs
@staticmethod
def dispatch_scene_to_workers(worker_list, project_path, blend_file, scene):
blend = bf.open_blend(blend_file)
scenes_info = retrieve_blenderfile_scenes_info(blend)
if scene not in scenes_info:
raise ValueError
start_frame, end_frame = scenes_info[scene]
undispatched_frames = end_frame - start_frame + 1
available_workers = len(worker_list)
next_start_frame = start_frame
round_batch = False
job_dispatch = {}
for worker, remote_project_path in worker_list:
if not round_batch and undispatched_frames % available_workers != 0:
batch = math.ceil(undispatched_frames / available_workers)
elif not round_batch:
batch = undispatched_frames // available_workers
round_batch = True
job = Job(
blend_file.replace(project_path, remote_project_path),
scene, next_start_frame, next_start_frame + batch - 1,
project_path, remote_project_path)
job_dispatch[(worker, remote_project_path,)] = job
next_start_frame += batch
undispatched_frames -= batch
available_workers -= 1
return job_dispatch
def main():
host = ''
port = 8026
root = Index()
root.putChild(b'status', StatusResource())
root.putChild(b'workpile', WorkpileResource())
# root.putChild(b'cancel', CancelResource)
site = server.Site(root)
reactor.listenTCP(port, site)
logging.info('Starting server {}:{}'.format(host, port))
reactor.run()
# worker_list = [('localhost:8025', '/home/ewandor/projects/dev/blender/PushBlendPull/',)]
# project_path = '/home/ewandor/projects/dev/blender/PushBlendPull/'
# dispatch_scene_to_workers(
# worker_list,
# project_path,
# '/home/ewandor/projects/dev/blender/PushBlendPull/tests/pitou.blend',
# 'thom'
# )
def retrieve_blendfile_assets(blendfile):
assets_dna_type = ['Image', 'bSound', 'MovieClip']
assets = []
for block in blendfile.blocks:
if block.dna_type_name in assets_dna_type and block.get(b'name') != '':
assets.append(block.get(b'name'))
return assets
def retrieve_blenderfile_scenes_info(blendfile):
scenes_info = {}
scenes = [block for block in blendfile.blocks if block.code == b'SC']
for scene in scenes:
scenes_info[scene.get((b'id', b'name'))[2:]] = (
scene.get((b'r', b'sfra')),
scene.get((b'r', b'efra')),
)
return scenes_info
# blend1 = bf.open_blend('tests/test_blendfile.blend1')
# blend = bf.open_blend('tests/pitou.blend')
#
# seqs = [block for block in blend.blocks if block.dna_type_name == 'Sequence']
#
# for block in blend.blocks:
# print(block.code, '-', block.dna_type_name)
# print('coucou')
# for block in blend1.blocks:
# print(block.code, '-', block.dna_type_name)
# # assets = retrieve_blendfile_assets(blend)
# scenes_info = retrieve_blenderfile_scenes_info(blend)
#
#
# print(scenes_info[2][0])
def get_index_content():
return '''<html>
<head>
</head>
<body>
<input type="submit" value="track_status" onclick="return get_status();" />
<ul id="events"></ul>
<form action="/workpile" method="post" target="_blank" onsubmit="return submit_workpile()" id="form_workpile">
farms<br/>
<input type="checkbox" name="workers" value="hawat:8025:/home/ggentile/projects/dev/blender/PushBlendPull/" checked>hawat:8025<br/>
scenes<br/>
<input type="checkbox" name="workpile" value="/home/ewandor/projects/dev/blender/PushBlendPull/tests/pitou.blend>pitou"/>//tests/pitou.blend>pitou<br/>
<input type="checkbox" name="workpile" value="/home/ewandor/projects/dev/blender/PushBlendPull/tests/pitou.blend>thom"/>//tests/pitou.blend>thom<br/>
<input type="checkbox" name="workpile" value="/home/ewandor/projects/dev/blender/PushBlendPull/tests/thom.blend>Scene"/>//tests/thom.blend>Scene<br/>
project_path<input type="text" name="project_path" value="/home/ewandor/projects/dev/blender/PushBlendPull/"><br/>
<input type="submit" value="send_job"/>
</form>
<script type="text/javascript">
var listening = false;
function submit_workpile() {
submit_ajax(
document.getElementById("form_workpile"),
function() {get_status();}
);
return false;
}
function submit_ajax(form, callback) {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
callback();
}
};
request.open("POST", form.action);
request.send(new FormData(form));
update_queue()
}
function get_status() {
if (!listening) {
var eventList = document.getElementById("events");
while (eventList.firstChild) {
eventList.removeChild(eventList.firstChild);
}
var evtSource = new EventSource("http://localhost:8025/status");
evtSource.onmessage = function(e) {
var newElement = document.createElement("li");
newElement.innerHTML = "message: " + e.data;
eventList.appendChild(newElement);
if (eventList.childNodes.length > 5) {
eventList.removeChild(eventList.firstChild);
}
}
listening = true;
}
}
function update_queue() {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
var response = JSON.parse(request.responseText);
var select = document.getElementById("jobs");
while (select.firstChild) {
select.removeChild(select.firstChild);
}
response.queue.forEach(function(job) {
var newElement = document.createElement("option");
newElement.text = job;
newElement.value = job;
select.appendChild(newElement);
});
}
};
request.open('GET', "http://localhost:8025/job", true);
request.send(null);
}
document.addEventListener("DOMContentLoaded", function(event) {
document.getElementById("queued_jobs").addEventListener("click", update_queue, false);
}, false);
</script>
</body>
</html>'''.encode()
if __name__ == "__main__":
main()

BIN
test.blend Normal file

Binary file not shown.

BIN
test.blend1 Normal file

Binary file not shown.

262
test.py Normal file
View File

@@ -0,0 +1,262 @@
import bpy
import os
import re
import time
from queue import Queue
from threading import Thread, Event
from subprocess import Popen, PIPE, STDOUT
from bpy.types import Panel, Operator, PropertyGroup
from bpy.props import (StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
EnumProperty,
PointerProperty,
CollectionProperty
)
class BlendFileProperty(PropertyGroup):
path = StringProperty(subtype="FILE_PATH")
filename = StringProperty(subtype="FILE_NAME")
blending_enabled = BoolProperty(
name="Enable or Disable",
description="Enable blend file for blending",
default=False)
status = StringProperty(
name="Status",
description="Status of current file",
default=""
)
class PushBlendPullProperties(PropertyGroup):
project_folder = StringProperty(
name="Project folder",
description="Path to project folder",
default="//",
subtype="DIR_PATH"
)
remote_host = StringProperty(
name="Remote host",
description="Address or host of remote",
defaul =""
)
remote_folder = StringProperty(
name="Remote project folder",
description="Path to remote project folder",
default=""
)
blend_file_list = CollectionProperty(type=BlendFileProperty)
upload_status = StringProperty(
name="Upload status",
description="Status of current upload",
default=""
)
class ProjectManagerRefreshBlenFileOperator(Operator):
bl_idname = "wm.pm_refresh_blend"
bl_label = "Refresh Blender Files"
def execute(self, context):
blendFileList = context.window_manager.project_manager.blend_file_list
enabled = []
for blendFile in blendFileList:
if blendFile.blending_enabled:
enabled.append(blendFile.path)
blendFileList.clear()
path = bpy.path.abspath(context.window_manager.project_manager.project_folder)
for root, dirs, files in os.walk(path):
for file in files:
if file.endswith(".blend"):
blendFile = blendFileList.add()
blendFile.filename = os.path.splitext(file)[0]
blendFile.path = os.path.join(root, file)
blendFile.name = bpy.path.relpath(blendFile.path)
blendFile.blending_enabled = blendFile.path in enabled
return {'FINISHED'}
class ProjectManagerStartBlend(Operator):
bl_idname = "wm.project_manager"
bl_label = "Start blend"
_timer = None
_queue = None
_worker = None
def execute(self, context):
wm = context.window_manager
self._queue = Queue()
self._worker = RemoteThreadHandler(
self._queue,
wm.project_manager.blend_file_list,
bpy.path.abspath(wm.project_manager.project_folder),
wm.project_manager.remote_host,
wm.project_manager.remote_folder
)
self._worker.start()
self._timer = wm.event_timer_add(1, context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
if event.type in {'ESC'}:
self.cancel(context)
print("canceled")
return {'CANCELLED'}
if event.type == 'TIMER':
while not self._queue.empty():
self.handle_message(self._queue.get(), context.window_manager.project_manager)
self._queue.task_done()
if not self._worker.is_alive():
self.cancel(context)
print("finished")
return {'FINISHED'}
return {'PASS_THROUGH'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self._worker.stop()
def handle_message(self, msg, pm):
if msg[0] == "push":
print(msg)
pm.upload_status = "push "+msg[1]
elif msg[0] == "pull":
msg[2].status = "pull "+msg[1]
elif msg[0] == "blend":
msg[2].status = "blend "+msg[1]
class RemoteThreadHandler(Thread):
def __init__(self, q, blendFileList, localFolder, remoteHost, remoteFolder):
super(RemoteThreadHandler, self).__init__()
self._queue = q
self.blendFileList = blendFileList
self.remotePath = "{}:{}".format(remoteHost,remoteFolder)
self.localFolder = localFolder
self.remoteHost = remoteHost
self.remoteFolder = remoteFolder
self._stop_event = Event()
def stop(self):
self._stop_event.set()
def stopped(self):
return self._stop_event.is_set()
ARGS_PUSH = "rsync -azP --info=progress2 --exclude '*.blend1' --exclude 'README.md' {} {}"
ARGS_PULL = "rsync -azP --info=progress2 --exclude '*.blend' {} {}"
ARGS_BLEND = "ssh -t hawat 'blender -b {} -a'"
REGEX_PUSHPULL_STATUS = "\d+%"
def handleLinePush(self, line, current_file=None):
if line == 'end':
self._queue.put(("push", "end",))
return
t = re.search(self.REGEX_PUSHPULL_STATUS, line)
if t is not None:
self._queue.put(("push", t[0],))
def handleLinePull(self, line, current_file):
if line == 'end':
self._queue.put(("pull", "end", current_file,))
return
t = re.search(self.REGEX_PUSHPULL_STATUS, line)
if t is not None:
self._queue.put(("pull", t[0], current_file,))
def run(self):
self.start_process_handle_queue(self.ARGS_PUSH.format(self.localFolder, self.remotePath), self.handleLinePush)
for blendFile in self.blendFileList:
if blendFile.blending_enabled:
remoteBlendFilePath = blendFile.path.replace(self.localFolder, self.remoteFolder)
self.start_process_handle_queue(self.ARGS_BLEND.format(remoteBlendFilePath), self.handleLineBlend, blendFile)
pullWorker = Thread(
target=self.start_process_handle_queue,
args=(self.ARGS_PULL.format(self.remotePath, self.localFolder) , self.handleLinePull, blendFile)
)
pullWorker.start()
def start_process_handle_queue(self, args, lineHandler, current_file = None):
if self.stopped():
return
print(args)
p = Popen(args, shell=True, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=1)
last_line = ''
while not self.stopped():
line = p.stdout.readline()
if line == '':
if p.poll() is not None:
break
elif line != last_line:
lineHandler(line, current_file)
last_line = line
if self.stopped():
p.kill()
lineHandler('end', current_file)
class ProjectManagerPanel(Panel):
bl_idname = "OBJECT_PT_project_manager"
bl_label = "Project Management"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = ""
def draw(self, context):
layout = self.layout
layout.operator(ProjectManagerStartBlend.bl_idname)
configuration = context.window_manager.project_manager
layout.label(text="Configuration")
layout = self.layout
row = layout.row(align=True)
row.prop(configuration, "project_folder")
row = layout.row(align=True)
row.prop(configuration, "remote_host")
row = layout.row(align=True)
row.prop(configuration, "remote_folder")
row = layout.row(align=True)
row.prop(configuration, "upload_status")
layout.operator(ProjectManagerRefreshBlenFileOperator.bl_idname)
fileList = configuration.blend_file_list
for blendFile in sorted(fileList.values(), key=lambda file: file.path):
row = layout.row()
row.prop(blendFile, "blending_enabled", text=bpy.path.relpath(blendFile.path))
row.prop(blendFile, "status", text="")
def register():
bpy.utils.register_module(__name__)
bpy.types.WindowManager.project_manager = PointerProperty(type=ProjectManagerProperties)
def unregister():
bpy.utils.unregister_module(__name__)
del bpy.types.WindowManager.project_manage
if __name__ == "__main__":
register()

BIN
tests/pitou.blend Normal file

Binary file not shown.

BIN
tests/pitou.blend1 Normal file

Binary file not shown.

13
tests/test.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
echo $1 "start"
sleep 1
echo $1 "1/4"
sleep 1
echo $1 "2/4"
sleep 1
echo $1 "3/4"
sleep 1
echo $1 "4/4"
sleep 1
echo $1 "fin"

BIN
tests/test_blendfile.blend Normal file

Binary file not shown.

BIN
tests/test_blendfile.blend1 Normal file

Binary file not shown.

135
tests/test_dispatcher.py Normal file
View File

@@ -0,0 +1,135 @@
import os
import re
import json
import logging
from queue import Queue, Empty
from threading import Thread, Event
from subprocess import Popen, PIPE, STDOUT
import requests
from twisted.internet import reactor
from twisted.web import server, resource, http
logging.basicConfig(level=logging.INFO)
class Index(resource.Resource):
isLeaf = False
def getChild(self, path, request):
if path == b'':
return self
return resource.Resource.getChild(self, path, request)
def render_GET(self, request):
return ''
class StatusResource(resource.Resource):
isLeaf = True
def __init__(self):
self.subscriptions = []
self.last_status = ('idle',)
def response_callback(self, err, request):
request.finish
logging.info('Request Handler: connection either resulted in error or client closed')
self.subscriptions.remove(request)
def add_event_stream_headers(self, request):
request.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
request.setHeader("Access-Control-Allow-Origin", "*")
return request
def add_json_headers(self, request):
request.setHeader('Content-Type', 'application/json; charset=utf-8')
return request
def publish(self, status):
self.last_status = status
for req in self.subscriptions:
self.push_sse_mesage(req=req, msg=status)
def push_sse_mesage(self, req, msg):
event_line = "data: {}\r\n".format(json.dumps(msg)) + '\r\n'
req.write(event_line.encode())
def render_GET(self, request):
accept_header = request.requestHeaders.getRawHeaders(b'accept')
if b'text/event-stream' in accept_header:
request = self.add_event_stream_headers(request) # 2. Format Headers
request.write("")
request.notifyFinish().addBoth(self.response_callback, request)
self.subscriptions.append(request)
logging.info('Request Handler: new subscriber ({})'.format(len(self.subscriptions)))
return server.NOT_DONE_YET
else:
return json.dumps(self.last_status).encode()
class JobResource(resource.Resource):
isLeaf = True
def __init__(self, fake_worker):
self.fake_worker = fake_worker
def render_POST(self, request):
self.fake_worker.start_fake_working()
return b'OK'
def render_GET(self, request):
return json.dumps({
"current_job": '',
"queue": []
}).encode()
def main():
host = ''
port = 8025
for x in range(1, 255):
interface = '127.0.0.{}'.format(x)
root = Index()
status_output = StatusResource()
worker = FakeWorker(status_output)
root.putChild(b'status', status_output)
root.putChild(b'job', JobResource(worker))
site = server.Site(root)
reactor.listenTCP(port, site, interface=interface)
reactor.addSystemEventTrigger("after", "startup", worker.stop_worker)
logging.info('Starting server {}:{}'.format(host, port))
reactor.run()
def send_request_to_dispatcher():
request_param = {
'workers': '',
'workpile': '',
'project_path': ''
}
requests.post('http://localhost:8026/workpile', data=request_param)
class FakeWorker:
def __init__(self, status_output):
self._stop_working = Event()
self._cancel_job = Event()
self._status_output = status_output
self._last_output_status = ''
self._current_job = None
def start_fake_working(self):
worker_thread = Thread(target=self._do_fake_work)
worker_thread.start()
def _do_fake_work(self):
for x in range(1,1000):
self._status_output.publish(('fakeblendfile>fakescene', "Fra:{}".format(x),))
self._status_output.publish(('fakeblendfile>fakescene', "done",))
if __name__ == "__main__":
main()

BIN
tests/thom.blend Normal file

Binary file not shown.

BIN
tests/thom.blend1 Normal file

Binary file not shown.

480
worker.py Normal file
View File

@@ -0,0 +1,480 @@
import os
import re
import time
import json
import logging
from queue import Queue, Empty
from threading import Thread, Event
from subprocess import Popen, PIPE, STDOUT
from twisted.internet import reactor
from twisted.web import server, resource, http
logging.basicConfig(level=logging.INFO)
class Index(resource.Resource):
isLeaf = False
def getChild(self, path, request):
if path == b'':
return self
return resource.Resource.getChild(self, path, request)
def render_GET(self, request):
return get_index_content()
class StatusResource(resource.Resource):
isLeaf = True
def __init__(self):
self.subscriptions = []
self.last_status = ('idle',)
def response_callback(self, err, request):
request.finish
logging.info('Request Handler: connection either resulted in error or client closed')
self.subscriptions.remove(request)
def add_event_stream_headers(self, request):
request.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
request.setHeader("Access-Control-Allow-Origin", "*")
return request
def add_json_headers(self, request):
request.setHeader('Content-Type', 'application/json; charset=utf-8')
return request
def publish(self, status):
self.last_status = status
for req in self.subscriptions:
self.push_sse_mesage(req=req, msg=status)
def push_sse_mesage(self, req, msg):
event_line = "data: {}\r\n".format(json.dumps(msg)) + '\r\n'
req.write(event_line.encode())
def render_GET(self, request):
accept_header = request.requestHeaders.getRawHeaders(b'accept')
if b'text/event-stream' in accept_header:
request = self.add_event_stream_headers(request) # 2. Format Headers
request.write("")
request.notifyFinish().addBoth(self.response_callback, request)
self.subscriptions.append(request)
logging.info('Request Handler: new subscriber ({})'.format(len(self.subscriptions)))
return server.NOT_DONE_YET
else:
return json.dumps(self.last_status).encode()
class JobResource(resource.Resource):
isLeaf = True
PARAM_BLEND_FILE = b'blend_file'
PARAM_SCENE = b'scene'
PARAM_START_FRAME = b'start_frame'
PARAM_END_FRAME = b'end_frame'
def __init__(self, worker):
self.worker = worker
@classmethod
def get_param_blend_file(cls, request):
if cls.PARAM_BLEND_FILE not in request.args or not request.args[cls.PARAM_BLEND_FILE][0].endswith(b'.blend'):
raise ValueError
blend_file = request.args[cls.PARAM_BLEND_FILE][0].decode("utf-8")
if not os.path.isfile(blend_file):
raise FileNotFoundError
return blend_file
@classmethod
def get_param_scene(cls, request):
if cls.PARAM_SCENE not in request.args or request.args[cls.PARAM_SCENE][0] == b'':
return None
return request.args[cls.PARAM_SCENE][0].decode("utf-8")
@classmethod
def get_param_start_frame(cls, request):
if cls.PARAM_START_FRAME not in request.args or request.args[cls.PARAM_START_FRAME][0] == b'':
return None
start_frame = request.args[cls.PARAM_START_FRAME][0]
if not start_frame.isdigit():
raise ValueError
return int(start_frame)
@classmethod
def get_param_end_frame(cls, request):
if cls.PARAM_END_FRAME not in request.args or request.args[cls.PARAM_END_FRAME][0] == b'':
return None
end_frame = request.args[cls.PARAM_END_FRAME][0]
if not end_frame.isdigit():
raise ValueError
return int(end_frame)
def prepare_job(self, request):
return Job(
self.get_param_blend_file(request),
scene=self.get_param_scene(request),
start_frame=self.get_param_start_frame(request),
end_frame=self.get_param_end_frame(request)
)
def render_POST(self, request):
try:
job = self.prepare_job(request)
logging.info('Request Handler: received a job request: {}'.format(job))
self.worker.enqueue_job(job)
return b'OK'
except ValueError:
return resource.ErrorPage(http.BAD_REQUEST, "Bad request", "Error in the request's format")
except FileNotFoundError:
return resource.NoResource("Blend file not found")
def render_GET(self, request):
return json.dumps({
"current_job": str(self.worker.get_current_job()),
"queue": [str(job) for job in self.worker.get_job_queue()]
}).encode()
class CancelResource(resource.Resource):
isLeaf = True
PARAM_TYPE = b'type'
PARAM_JOBS = b'jobs'
CANCEL_TYPE_QUEUED_JOBS = b'queued_jobs'
CANCEL_TYPE_QUEUE = b'queue'
CANCEL_TYPE_CURRENT_JOB = b'current_job'
CANCEL_TYPE_ALL = b'all'
def __init__(self, worker):
self.worker = worker
@classmethod
def get_param_type(cls, request):
if cls.PARAM_TYPE not in request.args:
raise ValueError
param_type = request.args[cls.PARAM_TYPE][0]
available_type = [cls.CANCEL_TYPE_QUEUED_JOBS, cls.CANCEL_TYPE_QUEUE,
cls.CANCEL_TYPE_CURRENT_JOB, cls.CANCEL_TYPE_ALL]
if param_type not in available_type:
raise ValueError
return param_type
@classmethod
def get_param_jobs(cls, request):
if cls.PARAM_JOBS not in request.args:
raise ValueError
jobs = request.args[cls.PARAM_JOBS]
return [job.decode("utf-8") for job in jobs]
def render_POST(self, request):
logging.info('Request Handler: received cancel request')
try:
type_param = self.get_param_type(request)
if type_param == self.CANCEL_TYPE_QUEUED_JOBS:
self.worker.remove_job_list_from_queue(self.get_param_jobs(request))
elif type_param == self.CANCEL_TYPE_QUEUE:
self.worker.clear_job_queue()
elif type_param == self.CANCEL_TYPE_CURRENT_JOB:
self.worker.cancel_current_job()
elif type_param == self.CANCEL_TYPE_ALL:
self.worker.clear_job_queue()
self.worker.cancel_current_job()
except ValueError:
return resource.ErrorPage(http.BAD_REQUEST, "Bad request", "Error in the request's format")
return b'OK'
def main():
host = ''
port = 8025
root = Index()
status_output = StatusResource()
worker = QueueConsumer(status_output)
root.putChild(b'status', status_output)
root.putChild(b'job', JobResource(worker))
root.putChild(b'cancel', CancelResource(worker))
site = server.Site(root)
reactor.listenTCP(port, site)
worker.start_worker()
reactor.addSystemEventTrigger("before", "shutdown", worker.stop_worker)
logging.info('Starting server {}:{}'.format(host, port))
reactor.run()
class QueueConsumer:
def __init__(self, status_output):
self._job_queue = EditableQueue()
self._stop_working = Event()
self._cancel_job = Event()
self._status_output = status_output
self._last_output_status = ''
self._current_job = None
def start_worker(self):
self._stop_working.clear()
worker_thread = Thread(target=self._consume_queue)
worker_thread.start()
def stop_worker(self):
self._stop_working.set()
def enqueue_job(self, job):
self._job_queue.put(job)
def get_job_queue(self):
return self._job_queue.as_list()
def get_current_job(self):
return self._current_job
def cancel_current_job(self):
self._cancel_job.set()
def remove_job_list_from_queue(self, job_list):
removed_jobs = self._job_queue.remove_sublist(job_list)
for job in removed_jobs:
self._status_output.publish((str(job), Job.STATUS_CANCELED,))
def clear_job_queue(self):
removed_jobs = self._job_queue.clear()
for job in removed_jobs:
self._status_output.publish((str(job), Job.STATUS_CANCELED,))
def _consume_queue(self):
while not self._stop_working.is_set():
try:
job = self._job_queue.get(timeout=1)
except Empty:
continue
self._current_job = job
self._execute_job()
self._current_job = None
def _execute_job(self):
logging.info('Worker: starting a new job: {}'.format(self._current_job))
start_time = time.time()
p = Popen(["/usr/bin/blender"] + self._current_job.get_blender_args(),
stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=1)
while not self._cancel_job.is_set():
line = p.stdout.readline()
if line == '':
if p.poll() is not None:
logging.info('Worker: job finished: {}'.format(self._current_job))
self._status_output.publish((str(self._current_job), Job.STATUS_DONE, time.time() - start_time,))
break
else:
status = self._parse_stdout(line)
if status is not None:
logging.info('Worker: job status is: {}'.format(status))
self._status_output.publish((str(self._current_job), status,))
if self._cancel_job.is_set():
logging.info('Worker: canceling: {}'.format(self._current_job))
p.kill()
self._cancel_job.clear()
self._status_output.publish((str(self._current_job), Job.STATUS_CANCELED,))
def _parse_stdout(self, line):
logging.debug('Worker: getting output: {}'.format(line))
regex_blend_status = "Fra:\d+"
t = re.search(regex_blend_status, line)
if t is not None and t[0] != self._last_output_status:
self._last_output_status = t[0]
return t[0]
else:
return None
class EditableQueue(Queue):
def remove_sublist(self, item_list):
with self.not_empty:
removed_items = []
for item in item_list:
try:
self.queue.remove(item)
except ValueError:
pass
else:
removed_items.append(item)
return removed_items
def clear(self):
with self.not_empty:
removed_items = list(self.queue)
self.queue.clear()
return removed_items
def as_list(self):
with self.not_empty:
return list(self.queue)
class Job:
STATUS_DONE = 'done'
STATUS_CANCELED = 'canceled'
def __init__(self, blend_file, **kwargs):
self.blend_file = blend_file
self.scene = kwargs["scene"] if "scene" in kwargs else None
self.start_frame = kwargs["start_frame"] if "start_frame" in kwargs else None
self.end_frame = kwargs["end_frame"] if "end_frame" in kwargs else None
def __str__(self):
return "{}>{} {}:{}".format(
self.blend_file,
self.scene if self.scene is not None else "default_scene",
self.start_frame if self.start_frame is not None else "start",
self.end_frame if self.end_frame is not None else "end"
)
def get_blender_args(self):
args = ['--background', self.blend_file]
if self.scene is not None:
args.append('--scene')
args.append(self.scene)
if self.start_frame is not None:
args.append('--frame-start')
args.append(str(self.start_frame))
if self.end_frame is not None:
args.append('--frame-end')
args.append(str(self.end_frame))
args.append('--render-anim')
return args
def __eq__(self, other):
if isinstance(other, str):
return other == str(self)
return other == self.blend_file and other.scene == self.scene \
and other.start_frame == self.start_frame \
and other.end_frame == self.end_frame
def get_index_content():
return '''<html>
<head>
</head>
<body>
<input type="submit" value="track_status" onclick="return start_event();" />
<ul id="events"></ul>
<form action="/job" method="post" target="_blank" onsubmit="return submit_job()" id="form_job">
<input type="radio" name="blend_file" value="/home/ewandor/projects/dev/blender/PushBlendPull/tests/pitou.blend" checked>pitou<br/>
<input type="radio" name="blend_file" value="/home/ewandor/projects/dev/blender/PushBlendPull/tests/thom.blend">thom<br/>
scene<input type="text" name="scene" value="pitou"><br/>
start_frame<input type="text" name="start_frame" value="1"><br/>
end_frame<input type="text" name="end_frame" value="50"><br/>
<input type="submit" value="send_job"/>
</form>
<form action="/cancel" method="post" target="_blank" onsubmit="return submit_cancel()" id="form_cancel">
<input type="text" name="toto[tata]
<input type="radio" name="type" value="queued_jobs" id="queued_jobs">queued_jobs<br/>
<input type="radio" name="type" value="queue">queue<br/>
<input type="radio" name="type" value="current_job" checked>current_job<br/>
<input type="radio" name="type" value="all">all<br/>
<select multiple name="jobs" id="jobs"></select>
<input type="submit" value="cancel"/>
</form>
<script type="text/javascript">
var listening = false;
function submit_job() {
submit_ajax(
document.getElementById("form_job"),
function() {start_event();}
);
return false;
}
function submit_cancel() {
submit_ajax(
document.getElementById("form_cancel"),
function() {update_queue();}
);
return false;
}
function submit_ajax(form, callback) {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
callback();
}
};
request.open("POST", form.action);
request.send(new FormData(form));
update_queue()
}
function start_event() {
if (!listening) {
var eventList = document.getElementById("events");
while (eventList.firstChild) {
eventList.removeChild(eventList.firstChild);
}
var evtSource = new EventSource("/status");
evtSource.onmessage = function(e) {
var newElement = document.createElement("li");
newElement.innerHTML = "message: " + e.data;
eventList.appendChild(newElement);
if (eventList.childNodes.length > 5) {
eventList.removeChild(eventList.firstChild);
}
}
listening = true;
}
}
function update_queue() {
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
var response = JSON.parse(request.responseText);
var select = document.getElementById("jobs");
while (select.firstChild) {
select.removeChild(select.firstChild);
}
response.queue.forEach(function(job) {
var newElement = document.createElement("option");
newElement.text = job;
newElement.value = job;
select.appendChild(newElement);
});
}
};
request.open('GET', "/job", true);
request.send(null);
}
document.addEventListener("DOMContentLoaded", function(event) {
document.getElementById("queued_jobs").addEventListener("click", update_queue, false);
}, false);
</script>
</body>
</html>'''.encode()
if __name__ == "__main__":
main()