Adding project files to project template
This commit is contained in:
14
.idea/PushBlendPull.iml
generated
Normal file
14
.idea/PushBlendPull.iml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
70
.idea/markdown-navigator.xml
generated
Normal 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>
|
||||||
3
.idea/markdown-navigator/profiles_settings.xml
generated
Normal file
3
.idea/markdown-navigator/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<component name="MarkdownNavigator.ProfileManager">
|
||||||
|
<settings default="" pdf-export="" />
|
||||||
|
</component>
|
||||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
940
.idea/workspace.xml
generated
Normal 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>'>'.join(job)</expression-string>
|
||||||
|
<language-id>Python</language-id>
|
||||||
|
<evaluation-mode>EXPRESSION</evaluation-mode>
|
||||||
|
</expression>
|
||||||
|
<expression>
|
||||||
|
<expression-string>job.join('>')</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>
|
||||||
25
README.md
25
README.md
@@ -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
158
client.py
Normal 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
574
dispatcher.py
Normal 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
BIN
test.blend
Normal file
Binary file not shown.
BIN
test.blend1
Normal file
BIN
test.blend1
Normal file
Binary file not shown.
262
test.py
Normal file
262
test.py
Normal 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
BIN
tests/pitou.blend
Normal file
Binary file not shown.
BIN
tests/pitou.blend1
Normal file
BIN
tests/pitou.blend1
Normal file
Binary file not shown.
13
tests/test.sh
Executable file
13
tests/test.sh
Executable 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
BIN
tests/test_blendfile.blend
Normal file
Binary file not shown.
BIN
tests/test_blendfile.blend1
Normal file
BIN
tests/test_blendfile.blend1
Normal file
Binary file not shown.
135
tests/test_dispatcher.py
Normal file
135
tests/test_dispatcher.py
Normal 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
BIN
tests/thom.blend
Normal file
Binary file not shown.
BIN
tests/thom.blend1
Normal file
BIN
tests/thom.blend1
Normal file
Binary file not shown.
480
worker.py
Normal file
480
worker.py
Normal 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()
|
||||||
Reference in New Issue
Block a user