├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.rst ├── codecov.yml ├── docs └── worker_management.rst ├── example-average-color ├── .dockerignore ├── Dockerfile ├── README.rst ├── average_color │ ├── average_color.py │ └── average_color.xml ├── cli_list.json └── cli_list.py ├── example-girder-requests ├── Dockerfile ├── README.rst ├── cli_list.json ├── cli_list.py └── girder_requests │ ├── girder_requests.py │ └── girder_requests.xml ├── setup.cfg ├── setup.py ├── slicer_cli_web ├── __init__.py ├── cli_list_entrypoint.py ├── cli_utils.py ├── config.py ├── ctk_cli_adjustment.py ├── docker_resource.py ├── girder_plugin.py ├── girder_worker_plugin │ ├── __init__.py │ ├── cli_progress.py │ └── direct_docker_run.py ├── image_job.py ├── models │ ├── __init__.py │ ├── docker_image.py │ ├── exceptions.py │ ├── json_to_xml.py │ ├── parser.py │ └── schema.json ├── prepare_task.py ├── rest_slicer_cli.py ├── web_client │ ├── JobStatus.js │ ├── collections │ │ ├── WidgetCollection.js │ │ └── index.js │ ├── events.js │ ├── index.js │ ├── main.js │ ├── models │ │ ├── WidgetModel.js │ │ └── index.js │ ├── package.json │ ├── parser │ │ ├── constraints.js │ │ ├── convert.js │ │ ├── defaultValue.js │ │ ├── group.js │ │ ├── index.js │ │ ├── panel.js │ │ ├── param.js │ │ ├── parse.js │ │ └── widget.js │ ├── routes.js │ ├── stylesheets │ │ ├── configView.styl │ │ ├── controlWidget.styl │ │ ├── controlsPanel.styl │ │ ├── outputParameterDialog.styl │ │ ├── panel.styl │ │ ├── panelGroup.styl │ │ └── slicerUI.styl │ ├── templates │ │ ├── booleanWidget.pug │ │ ├── colorWidget.pug │ │ ├── configView.pug │ │ ├── controlsPanel.pug │ │ ├── enumerationWidget.pug │ │ ├── fileWidget.pug │ │ ├── jobsListWidget.pug │ │ ├── outputParameterDialog.pug │ │ ├── panel.pug │ │ ├── panelGroup.pug │ │ ├── rangeWidget.pug │ │ ├── regionWidget.pug │ │ ├── slicerUI.pug │ │ ├── uploadImageDialog.pug │ │ └── widget.pug │ ├── utils.js │ └── views │ │ ├── CollectionView.js │ │ ├── ConfigView.js │ │ ├── ControlWidget.js │ │ ├── ControlsPanel.js │ │ ├── ItemSelectorWidget.js │ │ ├── ItemView.js │ │ ├── JobsListWidget.js │ │ ├── JobsPanel.js │ │ ├── OutputParameterDialog.js │ │ ├── Panel.js │ │ ├── PanelGroup.js │ │ ├── index.js │ │ └── utils.js └── worker_tools.py ├── small-docker ├── .dockerignore ├── Dockerfile ├── Example1 │ ├── Example1.json │ ├── Example1.py │ ├── Example1.xml │ └── Example1.yaml ├── Example2 │ ├── Example2.json │ ├── Example2.py │ ├── Example2.xml │ └── Example2.yaml ├── Example3 │ ├── Example3.json │ ├── Example3.py │ ├── Example3.xml │ └── Example3.yaml ├── ExampleProgress │ ├── ExampleProgress.json │ ├── ExampleProgress.py │ └── progress_helper.py ├── ExampleSubJob │ ├── ExampleSubJob.json │ └── ExampleSubJob.py ├── cli_list.json └── cli_list.py ├── tests ├── __init__.py ├── conftest.py ├── data │ ├── ExampleSpec.xml │ ├── girder_worker.cfg │ ├── parser_params_advanced.json │ ├── parser_params_advanced.xml │ ├── parser_params_advanced.yaml │ ├── parser_params_simple.json │ ├── parser_params_simple.xml │ ├── parser_params_simple.yaml │ ├── parser_simple.json │ ├── parser_simple.xml │ └── parser_simple.yaml ├── girder_worker_plugin │ ├── test_cli_progress.py │ └── test_direct_docker_run.py ├── test_batch.py ├── test_config.py ├── test_docker.py ├── test_load.py ├── test_parser.py ├── test_rest_slicer_cli.py ├── test_web_client.py └── web_client_specs │ ├── .eslintrc │ ├── configViewSpec.js │ ├── containerViewSpec.js │ ├── dockerTaskSpec.js │ ├── itemSelectorWidgetSpec.js │ ├── panelGroupSpec.js │ ├── parseSpec.js │ └── widgetSpec.js ├── tox.ini └── utils └── xmltojson.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Vim template 4 | # swap 5 | [._]*.s[a-w][a-z] 6 | [._]s[a-w][a-z] 7 | # session 8 | Session.vim 9 | # temporary 10 | .netrwhist 11 | *~ 12 | 13 | # auto-generated tag files 14 | tags 15 | 16 | ### Cpp template 17 | # Compiled Object files 18 | *.slo 19 | *.lo 20 | *.o 21 | *.obj 22 | 23 | # Precompiled Headers 24 | *.gch 25 | *.pch 26 | 27 | # Compiled Dynamic libraries 28 | *.so 29 | *.dylib 30 | *.dll 31 | 32 | # Fortran module files 33 | *.mod 34 | *.smod 35 | 36 | # Compiled Static libraries 37 | *.lai 38 | *.la 39 | *.a 40 | *.lib 41 | 42 | # Executables 43 | *.exe 44 | *.out 45 | *.app 46 | 47 | ### CMake template 48 | CMakeCache.txt 49 | CMakeFiles 50 | CMakeScripts 51 | Makefile 52 | cmake_install.cmake 53 | install_manifest.txt 54 | CTestTestfile.cmake 55 | 56 | ### Emacs template 57 | # -*- mode: gitignore; -*- 58 | *~ 59 | \#*\# 60 | /.emacs.desktop 61 | /.emacs.desktop.lock 62 | *.elc 63 | auto-save-list 64 | tramp 65 | .\#* 66 | 67 | # Org-mode 68 | .org-id-locations 69 | *_archive 70 | 71 | # flymake-mode 72 | *_flymake.* 73 | 74 | # eshell files 75 | /eshell/history 76 | /eshell/lastdir 77 | 78 | # elpa packages 79 | /elpa/ 80 | 81 | # reftex files 82 | *.rel 83 | 84 | # AUCTeX auto folder 85 | /auto/ 86 | 87 | # cask packages 88 | .cask/ 89 | dist/ 90 | 91 | # Flycheck 92 | flycheck_*.el 93 | 94 | # server auth directory 95 | /server/ 96 | 97 | # projectiles files 98 | .projectile### VirtualEnv template 99 | # Virtualenv 100 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 101 | .Python 102 | [Bb]in 103 | [Ii]nclude 104 | [Ll]ib 105 | [Ll]ib64 106 | [Ll]ocal 107 | [Ss]cripts 108 | pyvenv.cfg 109 | .venv 110 | pip-selfcheck.json 111 | 112 | ### Linux template 113 | *~ 114 | 115 | # temporary files which can be created if a process still has a handle open of a deleted file 116 | .fuse_hidden* 117 | 118 | # KDE directory preferences 119 | .directory 120 | 121 | # Linux trash folder which might appear on any partition or disk 122 | .Trash-* 123 | 124 | ### C template 125 | # Object files 126 | *.o 127 | *.ko 128 | *.obj 129 | *.elf 130 | 131 | # Precompiled Headers 132 | *.gch 133 | *.pch 134 | 135 | # Libraries 136 | *.lib 137 | *.a 138 | *.la 139 | *.lo 140 | 141 | # Shared objects (inc. Windows DLLs) 142 | *.dll 143 | *.so 144 | *.so.* 145 | *.dylib 146 | 147 | # Executables 148 | *.exe 149 | *.out 150 | *.app 151 | *.i*86 152 | *.x86_64 153 | *.hex 154 | 155 | # Debug files 156 | *.dSYM/ 157 | *.su 158 | 159 | ### Windows template 160 | # Windows image file caches 161 | Thumbs.db 162 | ehthumbs.db 163 | 164 | # Folder config file 165 | Desktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk 178 | 179 | ### KDevelop4 template 180 | *.kdev4 181 | .kdev4/ 182 | 183 | ### Python template 184 | # Byte-compiled / optimized / DLL files 185 | __pycache__/ 186 | *.py[cod] 187 | *$py.class 188 | 189 | # C extensions 190 | *.so 191 | 192 | # Distribution / packaging 193 | .Python 194 | env/ 195 | build/ 196 | develop-eggs/ 197 | dist/ 198 | downloads/ 199 | eggs/ 200 | .eggs/ 201 | lib/ 202 | lib64/ 203 | parts/ 204 | sdist/ 205 | var/ 206 | *.egg-info/ 207 | .installed.cfg 208 | *.egg 209 | 210 | # PyInstaller 211 | # Usually these files are written by a python script from a template 212 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 213 | *.manifest 214 | *.spec 215 | 216 | # Installer logs 217 | pip-log.txt 218 | pip-delete-this-directory.txt 219 | 220 | # Unit test / coverage reports 221 | htmlcov/ 222 | .tox/ 223 | .coverage 224 | .coverage.* 225 | .cache 226 | nosetests.xml 227 | coverage.xml 228 | *,cover 229 | .hypothesis/ 230 | 231 | # Translations 232 | *.mo 233 | *.pot 234 | 235 | # Django stuff: 236 | *.log 237 | local_settings.py 238 | 239 | # Flask stuff: 240 | instance/ 241 | .webassets-cache 242 | 243 | # Scrapy stuff: 244 | .scrapy 245 | 246 | # Sphinx documentation 247 | docs/_build/ 248 | 249 | # PyBuilder 250 | target/ 251 | 252 | # IPython Notebook 253 | .ipynb_checkpoints 254 | 255 | # pyenv 256 | .python-version 257 | 258 | # celery beat schedule file 259 | celerybeat-schedule 260 | 261 | # dotenv 262 | .env 263 | 264 | # virtualenv 265 | venv/ 266 | ENV/ 267 | 268 | # Spyder project settings 269 | .spyderproject 270 | 271 | # Rope project settings 272 | .ropeproject 273 | 274 | ### Xcode template 275 | # Xcode 276 | # 277 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 278 | 279 | ## Build generated 280 | build/ 281 | DerivedData/ 282 | 283 | ## Various settings 284 | *.pbxuser 285 | !default.pbxuser 286 | *.mode1v3 287 | !default.mode1v3 288 | *.mode2v3 289 | !default.mode2v3 290 | *.perspectivev3 291 | !default.perspectivev3 292 | xcuserdata/ 293 | 294 | ## Other 295 | *.moved-aside 296 | *.xccheckout 297 | *.xcscmblueprint 298 | 299 | ### NodeJS template 300 | # Logs 301 | logs 302 | *.log 303 | npm-debug.log* 304 | 305 | # Runtime data 306 | pids 307 | *.pid 308 | *.seed 309 | 310 | # Directory for instrumented libs generated by jscoverage/JSCover 311 | lib-cov 312 | 313 | # Coverage directory used by tools like istanbul 314 | coverage 315 | 316 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 317 | .grunt 318 | 319 | # node-waf configuration 320 | .lock-wscript 321 | 322 | # Compiled binary addons (http://nodejs.org/api/addons.html) 323 | build/Release 324 | 325 | # Dependency directory 326 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 327 | node_modules 328 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | 10 | [*.js] 11 | indent_size = 4 12 | 13 | [{*.json}] 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.pug] 20 | indent_size = 2 21 | 22 | [*.py] 23 | indent_size = 4 24 | max_line_length = 100 25 | 26 | [{*.rb,Vagrantfile}] 27 | indent_size = 2 28 | 29 | [{*.sh,*.bat}] 30 | indent_size = 4 31 | 32 | [*.styl] 33 | indent_size = 2 34 | 35 | [*.yml] 36 | indent_size = 2 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Created by https://www.gitignore.io/api/node 92 | 93 | ### Node ### 94 | # Logs 95 | logs 96 | *.log 97 | npm-debug.log* 98 | 99 | # Runtime data 100 | pids 101 | *.pid 102 | *.seed 103 | *.pid.lock 104 | 105 | # Directory for instrumented libs generated by jscoverage/JSCover 106 | lib-cov 107 | 108 | # Coverage directory used by tools like istanbul 109 | coverage 110 | 111 | # nyc test coverage 112 | .nyc_output 113 | 114 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 115 | .grunt 116 | 117 | # node-waf configuration 118 | .lock-wscript 119 | 120 | # Compiled binary addons (http://nodejs.org/api/addons.html) 121 | build/Release 122 | 123 | # Dependency directories 124 | node_modules 125 | jspm_packages 126 | 127 | # Optional npm cache directory 128 | .npm 129 | 130 | # Optional eslint cache 131 | .eslintcache 132 | 133 | # Optional REPL history 134 | .node_repl_history 135 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.0.1 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-ast 9 | - id: check-builtin-literals 10 | - id: check-case-conflict 11 | - id: check-docstring-first 12 | - id: check-executables-have-shebangs 13 | - id: check-json 14 | - id: check-merge-conflict 15 | - id: check-shebang-scripts-are-executable 16 | # - id: check-symlinks 17 | # - id: check-toml 18 | # - id: check-xml 19 | - id: check-yaml 20 | - id: debug-statements 21 | - id: destroyed-symlinks 22 | - id: detect-private-key 23 | - id: double-quote-string-fixer 24 | - id: end-of-file-fixer 25 | - id: fix-byte-order-marker 26 | - id: forbid-new-submodules 27 | - id: mixed-line-ending 28 | - id: no-commit-to-branch 29 | - id: trailing-whitespace 30 | - repo: https://github.com/pre-commit/pygrep-hooks 31 | rev: v1.9.0 32 | hooks: 33 | - id: python-no-eval 34 | - id: python-no-log-warn 35 | - id: rst-backticks 36 | - id: rst-directive-colons 37 | - id: rst-inline-touching-normal 38 | - id: text-unicode-replacement-char 39 | - repo: https://github.com/Lucas-C/pre-commit-hooks-markup 40 | rev: v1.0.1 41 | hooks: 42 | - id: rst-linter 43 | files: README.rst 44 | name: rst-linter of README.rst 45 | - repo: https://github.com/codespell-project/codespell 46 | rev: v2.1.0 47 | hooks: 48 | - id: codespell 49 | args: 50 | - --ignore-words-list 51 | - "hist,indext,pixelx,thex,te,xdescribe" 52 | - repo: https://github.com/syntaqx/git-hooks 53 | rev: v0.0.17 54 | hooks: 55 | - id: circleci-config-validate 56 | - repo: https://github.com/asottile/pyupgrade 57 | rev: v3.15.0 58 | hooks: 59 | - id: pyupgrade 60 | args: 61 | - --py38-plus 62 | - --keep-percent-format 63 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | # We have some variation in tests due to variations in the test runs. We 5 | # want to ignore these changes, but not let code coverage slip too much. 6 | project: 7 | default: 8 | threshold: 1 9 | # This applies to the changed code. We allow it to be much less covered 10 | # than the main code, since we use the project threshold for that. 11 | patch: 12 | default: 13 | threshold: 25 14 | only_pulls: true 15 | -------------------------------------------------------------------------------- /docs/worker_management.rst: -------------------------------------------------------------------------------- 1 | Worker Management 2 | ----------------- 3 | 4 | The slicer_cli_web plugin has a simple worker management capability. This uses a configuration file that lists workers than can be started and stopped via shell commands. If more jobs are in the queue than started workers, then additional workers will be started. When ALL jobs have been stopped for some duration, all workers are stopped. 5 | 6 | The config file is a yaml file stored anywhere in the system. The specific item holding the file can be configured in the plugin's settings. 7 | 8 | An example file illustrates the options:: 9 | 10 | --- 11 | idle-time: 12 | # This is the duration in seconds that will elapse from when all jobs are 13 | # finished to when the workers will be stopped. The default is 300 14 | # seconds. 15 | all: 300 16 | workers: 17 | # This is a list of workers 18 | - 19 | # name is optional 20 | name: 'Example Worker' 21 | # start is required. It is a shell command that is run as the same 22 | # user as started girder 23 | start: start_worker.sh 24 | # stop is required. It is a shell command that is run as the same 25 | # user as started girder 26 | stop: stop_worker.sh 27 | # concurrency is the number of jobs that the worker can handle. It is 28 | # optional, but must be a positive integer. The default is 1. 29 | concurrency: 2 30 | # If initial-stop is not False, then all workers are stopped when the main 31 | # application is first started. This ensure workers are not abandoned, but 32 | # may prevent workers from finishing during a benign server restart. 33 | initial-stop: True 34 | 35 | This could be used, for instance, to start and stop EC2 instances with the appropriate commands. 36 | -------------------------------------------------------------------------------- /example-average-color/.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | ### Vim template 4 | # swap 5 | [._]*.s[a-w][a-z] 6 | [._]s[a-w][a-z] 7 | # session 8 | Session.vim 9 | # temporary 10 | .netrwhist 11 | *~ 12 | 13 | # auto-generated tag files 14 | tags 15 | 16 | ### Cpp template 17 | # Compiled Object files 18 | *.slo 19 | *.lo 20 | *.o 21 | *.obj 22 | 23 | # Precompiled Headers 24 | *.gch 25 | *.pch 26 | 27 | # Compiled Dynamic libraries 28 | *.so 29 | *.dylib 30 | *.dll 31 | 32 | # Fortran module files 33 | *.mod 34 | *.smod 35 | 36 | # Compiled Static libraries 37 | *.lai 38 | *.la 39 | *.a 40 | *.lib 41 | 42 | # Executables 43 | *.exe 44 | *.out 45 | *.app 46 | 47 | ### CMake template 48 | CMakeCache.txt 49 | CMakeFiles 50 | CMakeScripts 51 | Makefile 52 | cmake_install.cmake 53 | install_manifest.txt 54 | CTestTestfile.cmake 55 | 56 | ### Emacs template 57 | # -*- mode: gitignore; -*- 58 | *~ 59 | \#*\# 60 | /.emacs.desktop 61 | /.emacs.desktop.lock 62 | *.elc 63 | auto-save-list 64 | tramp 65 | .\#* 66 | 67 | # Org-mode 68 | .org-id-locations 69 | *_archive 70 | 71 | # flymake-mode 72 | *_flymake.* 73 | 74 | # eshell files 75 | /eshell/history 76 | /eshell/lastdir 77 | 78 | # elpa packages 79 | /elpa/ 80 | 81 | # reftex files 82 | *.rel 83 | 84 | # AUCTeX auto folder 85 | /auto/ 86 | 87 | # cask packages 88 | .cask/ 89 | dist/ 90 | 91 | # Flycheck 92 | flycheck_*.el 93 | 94 | # server auth directory 95 | /server/ 96 | 97 | # projectiles files 98 | .projectile### VirtualEnv template 99 | # Virtualenv 100 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 101 | .Python 102 | [Bb]in 103 | [Ii]nclude 104 | [Ll]ib 105 | [Ll]ib64 106 | [Ll]ocal 107 | [Ss]cripts 108 | pyvenv.cfg 109 | .venv 110 | pip-selfcheck.json 111 | 112 | ### Linux template 113 | *~ 114 | 115 | # temporary files which can be created if a process still has a handle open of a deleted file 116 | .fuse_hidden* 117 | 118 | # KDE directory preferences 119 | .directory 120 | 121 | # Linux trash folder which might appear on any partition or disk 122 | .Trash-* 123 | 124 | ### C template 125 | # Object files 126 | *.o 127 | *.ko 128 | *.obj 129 | *.elf 130 | 131 | # Precompiled Headers 132 | *.gch 133 | *.pch 134 | 135 | # Libraries 136 | *.lib 137 | *.a 138 | *.la 139 | *.lo 140 | 141 | # Shared objects (inc. Windows DLLs) 142 | *.dll 143 | *.so 144 | *.so.* 145 | *.dylib 146 | 147 | # Executables 148 | *.exe 149 | *.out 150 | *.app 151 | *.i*86 152 | *.x86_64 153 | *.hex 154 | 155 | # Debug files 156 | *.dSYM/ 157 | *.su 158 | 159 | ### Windows template 160 | # Windows image file caches 161 | Thumbs.db 162 | ehthumbs.db 163 | 164 | # Folder config file 165 | Desktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk 178 | 179 | ### KDevelop4 template 180 | *.kdev4 181 | .kdev4/ 182 | 183 | ### Python template 184 | # Byte-compiled / optimized / DLL files 185 | __pycache__/ 186 | *.py[cod] 187 | *$py.class 188 | 189 | # C extensions 190 | *.so 191 | 192 | # Distribution / packaging 193 | .Python 194 | env/ 195 | build/ 196 | develop-eggs/ 197 | dist/ 198 | downloads/ 199 | eggs/ 200 | .eggs/ 201 | lib/ 202 | lib64/ 203 | parts/ 204 | sdist/ 205 | var/ 206 | *.egg-info/ 207 | .installed.cfg 208 | *.egg 209 | 210 | # PyInstaller 211 | # Usually these files are written by a python script from a template 212 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 213 | *.manifest 214 | *.spec 215 | 216 | # Installer logs 217 | pip-log.txt 218 | pip-delete-this-directory.txt 219 | 220 | # Unit test / coverage reports 221 | htmlcov/ 222 | .tox/ 223 | .coverage 224 | .coverage.* 225 | .cache 226 | nosetests.xml 227 | coverage.xml 228 | *,cover 229 | .hypothesis/ 230 | 231 | # Translations 232 | *.mo 233 | *.pot 234 | 235 | # Django stuff: 236 | *.log 237 | local_settings.py 238 | 239 | # Flask stuff: 240 | instance/ 241 | .webassets-cache 242 | 243 | # Scrapy stuff: 244 | .scrapy 245 | 246 | # Sphinx documentation 247 | docs/_build/ 248 | 249 | # PyBuilder 250 | target/ 251 | 252 | # IPython Notebook 253 | .ipynb_checkpoints 254 | 255 | # pyenv 256 | .python-version 257 | 258 | # celery beat schedule file 259 | celerybeat-schedule 260 | 261 | # dotenv 262 | .env 263 | 264 | # virtualenv 265 | venv/ 266 | ENV/ 267 | 268 | # Spyder project settings 269 | .spyderproject 270 | 271 | # Rope project settings 272 | .ropeproject 273 | 274 | ### Xcode template 275 | # Xcode 276 | # 277 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 278 | 279 | ## Build generated 280 | build/ 281 | DerivedData/ 282 | 283 | ## Various settings 284 | *.pbxuser 285 | !default.pbxuser 286 | *.mode1v3 287 | !default.mode1v3 288 | *.mode2v3 289 | !default.mode2v3 290 | *.perspectivev3 291 | !default.perspectivev3 292 | xcuserdata/ 293 | 294 | ## Other 295 | *.moved-aside 296 | *.xccheckout 297 | *.xcscmblueprint 298 | 299 | ### NodeJS template 300 | # Logs 301 | logs 302 | *.log 303 | npm-debug.log* 304 | 305 | # Runtime data 306 | pids 307 | *.pid 308 | *.seed 309 | 310 | # Directory for instrumented libs generated by jscoverage/JSCover 311 | lib-cov 312 | 313 | # Coverage directory used by tools like istanbul 314 | coverage 315 | 316 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 317 | .grunt 318 | 319 | # node-waf configuration 320 | .lock-wscript 321 | 322 | # Compiled binary addons (http://nodejs.org/api/addons.html) 323 | build/Release 324 | 325 | # Dependency directory 326 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 327 | node_modules 328 | -------------------------------------------------------------------------------- /example-average-color/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | LABEL maintainer="Kitware, Inc. " 3 | 4 | RUN pip install --no-cache-dir --find-links https://girder.github.io/large_image_wheels large_image[sources] 5 | 6 | RUN pip install --no-cache-dir girder-slicer-cli-web 7 | 8 | COPY . / 9 | ENTRYPOINT ["python", "./cli_list.py"] 10 | -------------------------------------------------------------------------------- /example-average-color/README.rst: -------------------------------------------------------------------------------- 1 | This is an example docker with a few inputs and outputs that computes the 2 | average color of a large image. 3 | 4 | To build, use docker in this directory:: 5 | 6 | docker build --force-rm -t girder/slicer_cli_web:example-average-color . 7 | 8 | To run, you'll need an image that can be read by the large_image library. 9 | You can run this as a command line, mounting a directory for input and output, 10 | like so:: 11 | 12 | docker run --rm -t -v /local/image/path:/input:ro -v /local/output/path:/output --rm -it girder/slicer_cli_web:example-average-color average_color /input/sample_image.svs /output/metadata.json --returnparameterfile /output/results.txt --channel=red 13 | 14 | You can also enumerate available algorithms:: 15 | 16 | docker run --rm -t girder/slicer_cli_web:example-average-color --list_cli 17 | 18 | And get help for the one available algorithm:: 19 | 20 | docker run --rm -t girder/slicer_cli_web:example-average-color average_color --help 21 | -------------------------------------------------------------------------------- /example-average-color/average_color/average_color.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | 4 | import large_image 5 | import numpy 6 | from ctk_cli import CLIArgumentParser # noqa I004 7 | 8 | # imported for side effects - prevents spurious warnings 9 | from slicer_cli_web import ctk_cli_adjustment # noqa 10 | 11 | 12 | def main(args): 13 | print('>> parsed arguments') 14 | pprint.pprint(vars(args)) 15 | 16 | ts = large_image.open(args.imageFile) 17 | 18 | tileMeans = [] 19 | tileWeights = [] 20 | # iterate through the tiles at a particular magnification: 21 | for tile in ts.tileIterator( 22 | format=large_image.tilesource.TILE_FORMAT_NUMPY, 23 | scale=dict(magnification=5), 24 | tile_size=dict(width=2048)): 25 | # The tile image data is in tile['tile'] and is a numpy 26 | # multi-dimensional array 27 | mean = numpy.mean(tile['tile'], axis=(0, 1)) 28 | tileMeans.append(mean) 29 | tileWeights.append(tile['width'] * tile['height']) 30 | print('x: %d y: %d w: %d h: %d mag: %g color: %s' % ( 31 | tile['x'], tile['y'], tile['width'], tile['height'], 32 | tile['magnification'], ' '.join(f'{val:g}' for val in mean))) 33 | mean = numpy.average(tileMeans, axis=0, weights=tileWeights) 34 | 35 | channels = ['red', 'green', 'blue'] 36 | if args.channel in channels: 37 | average = mean[channels.index(args.channel)] 38 | else: 39 | average = float(numpy.average(mean)) 40 | 41 | print('Average: %g' % average) 42 | sampleMetadata = { 43 | 'Average Color': average, 44 | 'Average Color By Band': [float(val) for val in mean], 45 | } 46 | open(args.outputItemMetadata, 'w').write(json.dumps(sampleMetadata)) 47 | 48 | if args.returnParameterFile: 49 | with open(args.returnParameterFile, 'w') as f: 50 | f.write('average = %s\n' % average) 51 | 52 | 53 | if __name__ == '__main__': 54 | main(CLIArgumentParser().parse_args()) 55 | -------------------------------------------------------------------------------- /example-average-color/average_color/average_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample 4 | Sample Algorithm - Image Processing 5 | Calculate the average color of a whole slide image. 6 | 0.1.0 7 | Apache 2.0 8 | David Manthey (Kitware) 9 | 10 | General parameters 11 | 12 | 13 | imageFile 14 | 0 15 | Input image 16 | 17 | input 18 | 19 | 20 | channel 21 | channel 22 | Which channel to compute 23 | 24 | all 25 | all 26 | red 27 | green 28 | blue 29 | 30 | 31 | average 32 | 33 | The average color 34 | output 35 | 0 36 | 37 | 38 | outputItemMetadata 39 | 40 | Output metadata file (*.json) 41 | output 42 | 1 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example-average-color/cli_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "average_color": { 3 | "type": "python" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-average-color/cli_list.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def processCLI(filename): 8 | try: 9 | with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 10 | filename)) as f: 11 | list_spec = json.load(f) 12 | except Exception: 13 | print('Failed to parse %s' % filename) 14 | return 15 | if len(sys.argv) >= 2 and sys.argv[1] == '--list_cli': 16 | print(json.dumps(list_spec, sort_keys=True, indent=2, separators=(',', ': '))) 17 | return 18 | if len(sys.argv) < 2 or sys.argv[1][:1] == '-': 19 | print('%s --list_cli to get a list of available interfaces.' % __file__) 20 | print('%s --help for more details.' % __file__) 21 | return 22 | 23 | cli = os.path.normpath(sys.argv[1]) 24 | 25 | cli = list_spec[cli].get('alias', cli) 26 | 27 | if list_spec[cli]['type'] == 'python': 28 | script_file = os.path.join(cli, os.path.basename(cli) + '.py') 29 | # python /.py [] 30 | subprocess.call([sys.executable, script_file] + sys.argv[2:]) 31 | elif list_spec[cli]['type'] == 'cxx': 32 | script_file = os.path.join('.', cli, os.path.basename(cli)) 33 | # .// [] 34 | subprocess.call([script_file] + sys.argv[2:]) 35 | else: 36 | raise Exception('CLIs of type %s are not supported' % list_spec[cli]['type']) 37 | 38 | 39 | if __name__ == '__main__': 40 | processCLI('cli_list.json') 41 | -------------------------------------------------------------------------------- /example-girder-requests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | MAINTAINER David Manthey 3 | 4 | RUN pip install --no-cache--dir --find-links https://girder.github.io/large_image_wheels large_image[sources] 5 | RUN pip install --no-cache--dir girder-slicer-cli-web 6 | RUN pip install --no-cache--dir girder-client 7 | 8 | COPY . $PWD 9 | ENTRYPOINT ["python", "./cli_list.py"] 10 | -------------------------------------------------------------------------------- /example-girder-requests/README.rst: -------------------------------------------------------------------------------- 1 | This is an example docker with a few inputs that uses the girder_client to fetch annotations associated with a large_image item. It expects that you are using the girder-large-image-annotations plugin and have such annotations. 2 | 3 | To build, use docker in this directory:: 4 | 5 | docker build --force-rm -t girder/slicer_cli_web:girder_requests . 6 | 7 | Altthough this can be run as a command line, it is more likely to be run from the Girder interface so that the girder-client api url and token are auto-populated. 8 | -------------------------------------------------------------------------------- /example-girder-requests/cli_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "girder_requests": { 3 | "type": "python" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example-girder-requests/cli_list.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def processCLI(filename): 8 | try: 9 | with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 10 | filename)) as f: 11 | list_spec = json.load(f) 12 | except Exception: 13 | print('Failed to parse %s' % filename) 14 | return 15 | if len(sys.argv) >= 2 and sys.argv[1] == '--list_cli': 16 | print(json.dumps(list_spec, sort_keys=True, indent=2, separators=(',', ': '))) 17 | return 18 | if len(sys.argv) < 2 or sys.argv[1][:1] == '-': 19 | print('%s --list_cli to get a list of available interfaces.' % __file__) 20 | print('%s --help for more details.' % __file__) 21 | return 22 | 23 | cli = os.path.normpath(sys.argv[1]) 24 | 25 | cli = list_spec[cli].get('alias', cli) 26 | 27 | if list_spec[cli]['type'] == 'python': 28 | script_file = os.path.join(cli, os.path.basename(cli) + '.py') 29 | # python /.py [] 30 | subprocess.call([sys.executable, script_file] + sys.argv[2:]) 31 | elif list_spec[cli]['type'] == 'cxx': 32 | script_file = os.path.join('.', cli, os.path.basename(cli)) 33 | # .// [] 34 | subprocess.call([script_file] + sys.argv[2:]) 35 | else: 36 | raise Exception('CLIs of type %s are not supported' % list_spec[cli]['type']) 37 | 38 | 39 | if __name__ == '__main__': 40 | processCLI('cli_list.json') 41 | -------------------------------------------------------------------------------- /example-girder-requests/girder_requests/girder_requests.py: -------------------------------------------------------------------------------- 1 | # import json 2 | import pprint 3 | 4 | import girder_client 5 | # import large_image 6 | # import numpy 7 | from ctk_cli import CLIArgumentParser 8 | 9 | 10 | def main(args): 11 | print('>> parsed arguments') 12 | pprint.pprint(vars(args)) 13 | gc = girder_client.GirderClient(apiUrl=args.girderApiUrl) 14 | gc.setToken(args.girderToken) 15 | 16 | annotations = gc.get('annotation', parameters=dict(limit=100, offset=0, itemId=args.imageId)) 17 | pprint.pprint(annotations) 18 | 19 | 20 | if __name__ == '__main__': 21 | main(CLIArgumentParser().parse_args()) 22 | -------------------------------------------------------------------------------- /example-girder-requests/girder_requests/girder_requests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample 4 | Girder Client - Fetch Annotation 5 | Summarize annotations associated with a whole slide image. 6 | 0.1.0 7 | Apache 2.0 8 | David Manthey (Kitware) 9 | 10 | General parameters 11 | 12 | 13 | imageId 14 | 0 15 | 16 | An image ID 17 | 18 | 19 | 20 | 21 | A Girder API URL and token for Girder client 22 | 23 | girderApiUrl 24 | api-url 25 | 26 | A Girder API URL (e.g., https://girder.example.com:443/api/v1) 27 | 28 | 29 | 30 | girderToken 31 | girder-token 32 | 33 | A Girder token 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def prerelease_local_scheme(version): 7 | """Return local scheme version unless building on master in CircleCI. 8 | This function returns the local scheme version number 9 | (e.g. 0.0.0.dev+g) unless building on CircleCI for a 10 | pre-release in which case it ignores the hash and produces a 11 | PEP440 compliant pre-release version number (e.g. 0.0.0.dev). 12 | """ 13 | from setuptools_scm.version import get_local_node_and_date 14 | 15 | if os.getenv('CIRCLE_BRANCH') == 'master': 16 | return '' 17 | else: 18 | return get_local_node_and_date(version) 19 | 20 | 21 | with open('README.rst') as f: 22 | readme = f.read() 23 | 24 | # perform the install 25 | setup( 26 | name='girder-slicer-cli-web', 27 | use_scm_version={ 28 | 'local_scheme': prerelease_local_scheme, 29 | 'fallback_version': '0.0.0'}, 30 | setup_requires=[ 31 | 'setuptools-scm', 32 | ], 33 | description='A girder plugin for exposing slicer CLIs over the web', 34 | long_description=readme, 35 | long_description_content_type='text/x-rst', 36 | url='https://github.com/girder/slicer_cli_web', 37 | keywords='girder-plugin, slicer_cli_web', 38 | author='Kitware, Inc.', 39 | author_email='kitware@kitware.com', 40 | license='Apache Software License 2.0', 41 | classifiers=[ 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Environment :: Web Environment', 44 | 'License :: OSI Approved :: Apache Software License', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.8', 49 | 'Programming Language :: Python :: 3.9', 50 | 'Programming Language :: Python :: 3.10', 51 | 'Programming Language :: Python :: 3.11', 52 | 'Programming Language :: Python :: 3.12', 53 | 'Programming Language :: Python :: 3.13', 54 | ], 55 | include_package_data=True, 56 | package_dir={'girder_slicer_cli_web': 'slicer_cli_web'}, 57 | packages=find_packages(exclude=['tests', 'test.*']), 58 | zip_safe=False, 59 | install_requires=[ 60 | 'ctk_cli', 61 | 'jinja2', 62 | 'jsonschema', 63 | 'pyyaml', 64 | ], 65 | extras_require={ 66 | 'girder': [ 67 | 'docker>=2.6.0', 68 | 'girder>=3.0.4', 69 | 'girder-jobs>=3.0.3', 70 | 'girder-worker[girder]>=0.6.0', 71 | ], 72 | 'worker': [ 73 | 'docker>=2.6.0', 74 | 'girder-worker[worker]>=0.6.0', 75 | ] 76 | }, 77 | entry_points={ 78 | 'girder.plugin': [ 79 | 'slicer_cli_web = slicer_cli_web.girder_plugin:SlicerCLIWebPlugin' 80 | ], 81 | 'girder_worker_plugins': [ 82 | 'slicer_cli_web = slicer_cli_web.girder_worker_plugin:SlicerCLIWebWorkerPlugin' 83 | ] 84 | }, 85 | python_requires='>=3.8', 86 | ) 87 | -------------------------------------------------------------------------------- /slicer_cli_web/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Copyright Kitware Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 ( the "License" ); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################# 16 | 17 | # These two imports must be in this order for appropriate side effects 18 | 19 | # isort: off 20 | 21 | from . import ctk_cli_adjustment # noqa 22 | 23 | from ctk_cli import CLIArgumentParser # noqa 24 | 25 | # isort: on 26 | 27 | 28 | from importlib.metadata import PackageNotFoundError 29 | from importlib.metadata import version as _importlib_version 30 | 31 | try: 32 | __version__ = _importlib_version(__name__) 33 | except PackageNotFoundError: 34 | # package is not installed 35 | pass 36 | 37 | 38 | __license__ = 'Apache 2.0' 39 | -------------------------------------------------------------------------------- /slicer_cli_web/cli_list_entrypoint.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import subprocess 5 | import sys 6 | import textwrap as _textwrap 7 | 8 | try: 9 | from girder import logger 10 | except ImportError: 11 | import logging as logger 12 | 13 | 14 | class _MultilineHelpFormatter(argparse.HelpFormatter): 15 | def _fill_text(self, text, width, indent): 16 | text = self._whitespace_matcher.sub(' ', text).strip() 17 | paragraphs = text.split('|n') 18 | multiline_text = '' 19 | for paragraph in paragraphs: 20 | formatted_paragraph = '\n' + _textwrap.fill( 21 | paragraph, width, 22 | initial_indent=indent, 23 | subsequent_indent=indent) + '\n' 24 | multiline_text += formatted_paragraph 25 | return multiline_text 26 | 27 | 28 | def _make_print_cli_list_spec_action(cli_list_spec_file): 29 | 30 | with open(cli_list_spec_file) as f: 31 | str_cli_list_spec = f.read() 32 | 33 | class _PrintCLIListSpecAction(argparse.Action): 34 | 35 | def __init__(self, 36 | option_strings, 37 | dest=argparse.SUPPRESS, 38 | default=argparse.SUPPRESS, 39 | help=None): 40 | super().__init__( 41 | option_strings=option_strings, 42 | dest=dest, 43 | default=default, 44 | nargs=0, 45 | help=help) 46 | 47 | def __call__(self, parser, namespace, values, option_string=None): 48 | print(str_cli_list_spec) 49 | parser.exit() 50 | 51 | return _PrintCLIListSpecAction 52 | 53 | 54 | def CLIListEntrypoint(cli_list_spec_file=None, cwd=None): 55 | 56 | if cli_list_spec_file is None: 57 | cli_list_spec_file = os.path.join(cwd or os.getcwd(), 'slicer_cli_list.json') 58 | 59 | # Parse CLI List spec 60 | with open(cli_list_spec_file) as f: 61 | cli_list_spec = json.load(f) 62 | 63 | # create command-line argument parser 64 | cmdparser = argparse.ArgumentParser( 65 | formatter_class=_MultilineHelpFormatter 66 | ) 67 | 68 | # add --list_cli 69 | cmdparser.add_argument( 70 | '--list_cli', 71 | action=_make_print_cli_list_spec_action(cli_list_spec_file), 72 | help='Prints the json file containing the list of CLIs present' 73 | ) 74 | 75 | # add cl-rel-path argument 76 | cmdparser.add_argument('cli', 77 | help='CLI to run', 78 | metavar='', 79 | choices=cli_list_spec.keys()) 80 | 81 | args = cmdparser.parse_args(sys.argv[1:2]) 82 | 83 | args.cli = os.path.normpath(args.cli) 84 | 85 | if cli_list_spec[args.cli]['type'] == 'python': 86 | 87 | script_file = os.path.join(cwd or os.getcwd(), args.cli, 88 | os.path.basename(args.cli) + '.py') 89 | 90 | # python /.py [] 91 | output_code = subprocess.call([sys.executable, script_file] + sys.argv[2:]) 92 | 93 | elif cli_list_spec[args.cli]['type'] == 'cxx': 94 | 95 | script_file = os.path.join(cwd or os.getcwd(), args.cli, os.path.basename(args.cli)) 96 | 97 | if os.path.isfile(script_file): 98 | 99 | # .// [] 100 | output_code = subprocess.call([script_file] + sys.argv[2:]) 101 | 102 | else: 103 | 104 | # assumes parent dir of CLI executable is in ${PATH} 105 | output_code = subprocess.call([os.path.basename(args.cli)] + sys.argv[2:]) 106 | 107 | else: 108 | logger.exception('CLIs of type %s are not supported', 109 | cli_list_spec[args.cli]['type']) 110 | raise Exception( 111 | 'CLIs of type %s are not supported', 112 | cli_list_spec[args.cli]['type'] 113 | ) 114 | 115 | return output_code 116 | 117 | 118 | if __name__ == '__main__': 119 | sys.exit(CLIListEntrypoint()) 120 | -------------------------------------------------------------------------------- /slicer_cli_web/cli_utils.py: -------------------------------------------------------------------------------- 1 | """utils for CLI spec handling.""" 2 | import io 3 | 4 | from .ctk_cli_adjustment import CLIModule 5 | 6 | return_parameter_file_name = 'returnparameterfile' 7 | 8 | SLICER_TYPE_TO_GIRDER_MODEL_MAP = { 9 | 'image': 'file', 10 | 'file': 'file', 11 | 'item': 'item', 12 | 'directory': 'folder' 13 | } 14 | 15 | SLICER_SUPPORTED_TYPES = set(['boolean', 'integer', 'float', 'double', 'string', 16 | 'integer-vector', 'float-vector', 'double-vector', 'string-vector', 17 | 'integer-enumeration', 'float-enumeration', 'double-enumeration', 18 | 'string-enumeration', 19 | 'region'] + list(SLICER_TYPE_TO_GIRDER_MODEL_MAP.keys())) 20 | 21 | 22 | def generate_description(clim): 23 | """Create CLI description string.""" 24 | str_description = ['Description:

' + clim.description] 25 | 26 | if clim.version: 27 | str_description.append('Version: ' + clim.version) 28 | 29 | if clim.license: 30 | str_description.append('License: ' + clim.license) 31 | 32 | if clim.contributor: 33 | str_description.append('Author(s): ' + clim.contributor) 34 | 35 | if clim.acknowledgements: 36 | str_description.append('Acknowledgements: ' + clim.acknowledgements) 37 | 38 | return '

'.join(str_description) 39 | 40 | 41 | def as_model(cliXML): 42 | """Parses cli xml spec.""" 43 | stream = io.BytesIO(cliXML if isinstance(cliXML, bytes) else cliXML.encode('utf8')) 44 | return CLIModule(stream=stream) 45 | 46 | 47 | def get_cli_parameters(clim): 48 | 49 | # get parameters 50 | index_params, opt_params, simple_out_params = clim.classifyParameters() 51 | 52 | # perform sanity checks 53 | for param in index_params + opt_params: 54 | if param.typ not in SLICER_SUPPORTED_TYPES: 55 | raise Exception( 56 | 'Parameter type %s is currently not supported' % param.typ 57 | ) 58 | 59 | # sort indexed parameters in increasing order of index 60 | index_params.sort(key=lambda p: p.index) 61 | 62 | # sort opt parameters in increasing order of name for easy lookup 63 | def get_flag(p): 64 | if p.flag is not None: 65 | return p.flag.strip('-') 66 | elif p.longflag is not None: 67 | return p.longflag.strip('-') 68 | else: 69 | return None 70 | 71 | opt_params.sort(key=lambda p: get_flag(p)) 72 | 73 | return index_params, opt_params, simple_out_params 74 | 75 | 76 | def is_on_girder(param): 77 | if param.reference == '_girder_id_': 78 | return False 79 | return param.typ in SLICER_TYPE_TO_GIRDER_MODEL_MAP 80 | 81 | 82 | def is_girder_api(param): 83 | return param.name in {'girderApiUrl', 'girderToken'} 84 | -------------------------------------------------------------------------------- /slicer_cli_web/config.py: -------------------------------------------------------------------------------- 1 | ############################################################################# 2 | # Copyright Kitware Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 ( the "License" ); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################# 16 | 17 | from girder.api.rest import getCurrentUser 18 | from girder.constants import AccessType 19 | from girder.exceptions import ValidationException 20 | from girder.models.folder import Folder 21 | from girder.models.item import Item 22 | from girder.models.setting import Setting 23 | from girder.settings import SettingDefault 24 | from girder.utility import setting_utilities 25 | 26 | 27 | # Constants representing the setting keys for this plugin 28 | class PluginSettings: 29 | SLICER_CLI_WEB_TASK_FOLDER = 'slicer_cli_web.task_folder' 30 | SLICER_CLI_WEB_WORKER_CONFIG_ITEM = 'slicer_cli_web.worker_config_item' 31 | 32 | @staticmethod 33 | def has_task_folder(): 34 | return Setting().get(PluginSettings.SLICER_CLI_WEB_TASK_FOLDER) is not None 35 | 36 | @staticmethod 37 | def get_task_folder(): 38 | folder = Setting().get(PluginSettings.SLICER_CLI_WEB_TASK_FOLDER) 39 | if not folder: 40 | return None 41 | return Folder().load(folder, level=AccessType.READ, user=getCurrentUser()) 42 | 43 | @staticmethod 44 | def has_worker_config_item(): 45 | return Setting().get(PluginSettings.SLICER_CLI_WEB_WORKER_CONFIG_ITEM) is not None 46 | 47 | @staticmethod 48 | def get_worker_config_item(): 49 | item = Setting().get(PluginSettings.SLICER_CLI_WEB_WORKER_CONFIG_ITEM) 50 | if not item: 51 | return None 52 | return Item().load(item, force=True, exc=False) 53 | 54 | 55 | @setting_utilities.validator({ 56 | PluginSettings.SLICER_CLI_WEB_TASK_FOLDER 57 | }) 58 | def validateFolder(doc): 59 | if not doc['value']: 60 | return 61 | # We should only be able to change settings with admin privilege, so we 62 | # don't need to check folder access here. 63 | folder = Folder().load(doc['value'], force=True) 64 | if not folder: 65 | raise ValidationException('invalid folder selected') 66 | 67 | 68 | @setting_utilities.validator({ 69 | PluginSettings.SLICER_CLI_WEB_WORKER_CONFIG_ITEM 70 | }) 71 | def validateItem(doc): 72 | if not doc['value']: 73 | return 74 | # We should only be able to change settings with admin privilege, so we 75 | # don't need to check folder access here. 76 | item = Item().load(doc['value'], force=True) 77 | if not item: 78 | raise ValidationException('invalid folder selected') 79 | 80 | 81 | # Defaults 82 | 83 | # Defaults that have fixed values can just be added to the system defaults 84 | # dictionary. 85 | SettingDefault.defaults.update({ 86 | PluginSettings.SLICER_CLI_WEB_TASK_FOLDER: None, 87 | PluginSettings.SLICER_CLI_WEB_WORKER_CONFIG_ITEM: None, 88 | }) 89 | -------------------------------------------------------------------------------- /slicer_cli_web/girder_plugin.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Kitware Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 ( the "License" ); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | import datetime 18 | import json 19 | 20 | from girder import events, logger 21 | from girder.constants import AccessType 22 | from girder.plugin import GirderPlugin, getPlugin 23 | from girder_jobs.constants import JobStatus 24 | from girder_jobs.models.job import Job 25 | 26 | from . import worker_tools 27 | from .docker_resource import DockerResource 28 | from .models import DockerImageItem 29 | 30 | 31 | def _onUpload(event): 32 | try: 33 | ref = json.loads(event.info.get('reference')) 34 | except (ValueError, TypeError): 35 | return 36 | 37 | if isinstance(ref, dict) and ref.get('type') == 'slicer_cli.parameteroutput': 38 | job = Job().load(ref['jobId'], force=True, exc=True) 39 | 40 | file = event.info['file'] 41 | 42 | # Add link to job model to the output item 43 | Job().updateJob(job, otherFields={ 44 | 'slicerCLIBindings.outputs.parameters': file['_id'] 45 | }) 46 | 47 | 48 | class SlicerCLIWebPlugin(GirderPlugin): 49 | DISPLAY_NAME = 'Slicer CLI Web' 50 | CLIENT_SOURCE_PATH = 'web_client' 51 | 52 | def load(self, info): 53 | try: 54 | getPlugin('worker').load(info) 55 | except Exception: 56 | logger.info('Girder working is unavailable') 57 | 58 | DockerImageItem.prepare() 59 | 60 | # resource name must match the attribute added to info[apiroot] 61 | resource = DockerResource('slicer_cli_web') 62 | info['apiRoot'].slicer_cli_web = resource 63 | 64 | Job().exposeFields(level=AccessType.READ, fields={'slicerCLIBindings'}) 65 | 66 | events.bind('jobs.job.update.after', resource.resourceName, 67 | resource.addRestEndpoints) 68 | events.bind('data.process', 'slicer_cli_web', _onUpload) 69 | 70 | count = 0 71 | for job in Job().find({ 72 | 'status': {'$in': [ 73 | JobStatus.INACTIVE, JobStatus.QUEUED, JobStatus.RUNNING, 74 | # from girder_worker, but we don't strictly require its 75 | # existence 76 | 820, 821, 822, 823, 824, 77 | ]}, 78 | 'updated': {'$lt': datetime.datetime.utcnow() - datetime.timedelta(days=7)} 79 | }, force=True): 80 | try: 81 | Job().updateJob(job, log='Canceled stale job.', status=JobStatus.CANCELED) 82 | count += 1 83 | except Exception: 84 | pass 85 | if count: 86 | logger.info('Marking %d old job(s) as cancelled' % count) 87 | worker_tools.start() 88 | -------------------------------------------------------------------------------- /slicer_cli_web/girder_worker_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Kitware Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 ( the "License" ); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | from girder_worker import GirderWorkerPluginABC 18 | 19 | 20 | class SlicerCLIWebWorkerPlugin(GirderWorkerPluginABC): 21 | def __init__(self, app, *args, **kwargs): 22 | self.app = app 23 | 24 | def task_imports(self): 25 | return ['slicer_cli_web.girder_worker_plugin.direct_docker_run'] 26 | -------------------------------------------------------------------------------- /slicer_cli_web/girder_worker_plugin/cli_progress.py: -------------------------------------------------------------------------------- 1 | import re 2 | from xml.sax.saxutils import unescape 3 | 4 | from girder_worker.docker.io import StreamWriter 5 | 6 | 7 | class CLIProgressCLIWriter(StreamWriter): 8 | def __init__(self, job_manager): 9 | super().__init__() 10 | 11 | self._job_manager = job_manager 12 | self._buf = b'' 13 | self._state = None 14 | re_start = re.compile('.*[\\s]*', re.MULTILINE | re.DOTALL) 15 | re_end = re.compile('.*[\\s]*', re.MULTILINE | re.DOTALL) 16 | re_time = re.compile('[^<]*[\\s]*', re.MULTILINE) 17 | re_stage_progress = re.compile( 18 | '[^<]*[\\s]*', re.MULTILINE) 19 | 20 | self._re_progress = re.compile('([^<]*)[\\s]*', 21 | re.MULTILINE) 22 | self._re_name = re.compile('([^<]*)[\\s]*', re.MULTILINE) 23 | self._re_comment = re.compile('([^<]*)[\\s]*', 24 | re.MULTILINE) 25 | 26 | self._re_clean = [re_start, re_end, re_time, re_stage_progress, self._re_progress, 27 | self._re_name, self._re_comment] 28 | 29 | self._last_name = 'Unknown' 30 | self._last_comment = '' 31 | 32 | def forward(self, buf): 33 | self._job_manager.write(buf) 34 | 35 | def write(self, buf): 36 | act = self._buf + buf 37 | self._buf = b'' 38 | 39 | if b' value 11 | */ 12 | values() { 13 | const params = {}; 14 | this.each((m) => { 15 | // apply special handling for certain parameter types 16 | // https://github.com/DigitalSlideArchive/slicer/blob/9e5112ab3444ad8c699d70452a5fe4a74ebbc778/server/__init__.py#L44-L46 17 | switch (m.get('type')) { 18 | case 'file': 19 | case 'item': 20 | case 'image': 21 | case 'directory': 22 | params[m.id] = m.value().id; 23 | break; 24 | case 'multi': 25 | case 'new-file': 26 | if (m && m.value && m.value()) { 27 | params[m.id + '_folder'] = m.value().get('folderId'); 28 | params[m.id] = m.value().get('name'); 29 | } 30 | break; 31 | case 'string': 32 | case 'boolean': 33 | case 'integer': 34 | case 'float': 35 | case 'double': 36 | case 'string-enumeration': 37 | params[m.id] = m.value(); 38 | break; 39 | default: 40 | params[m.id] = JSON.stringify(m.value()); 41 | } 42 | }); 43 | return params; 44 | } 45 | }); 46 | 47 | export default WidgetCollection; 48 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/collections/index.js: -------------------------------------------------------------------------------- 1 | export { default as WidgetCollection } from './WidgetCollection'; 2 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/events.js: -------------------------------------------------------------------------------- 1 | import girderEvents from '@girder/core/events'; 2 | 3 | export default girderEvents; 4 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/index.js: -------------------------------------------------------------------------------- 1 | import * as models from './models'; 2 | import * as collections from './collections'; 3 | import * as views from './views'; 4 | import * as parser from './parser'; 5 | import events from './events'; 6 | import utils from './utils'; 7 | 8 | export { 9 | models, 10 | collections, 11 | views, 12 | events, 13 | parser, 14 | utils 15 | }; 16 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/main.js: -------------------------------------------------------------------------------- 1 | import { registerPluginNamespace } from '@girder/core/pluginUtils'; 2 | 3 | // import modules for side effects 4 | import './routes'; 5 | import './views/ItemView'; 6 | import './views/CollectionView'; 7 | import './JobStatus'; 8 | 9 | // expose symbols under girder.plugins 10 | import * as slicerCLIWeb from './index'; 11 | registerPluginNamespace('slicer_cli_web', slicerCLIWeb); 12 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/models/index.js: -------------------------------------------------------------------------------- 1 | export { default as WidgetModel } from './WidgetModel'; 2 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@girder/slicer_cli_web", 3 | "version": "1.0.0", 4 | "description": "A girder plugin for exposing slicer CLIs over the web", 5 | "bugs": { 6 | "url": "https://github.com/girder/slicer_cli_web/issues" 7 | }, 8 | "license": "Apache-2.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/girder/slicer_cli_web.git" 12 | }, 13 | "main": "./index.js", 14 | "dependencies": { 15 | "bootstrap-colorpicker": "^2.5.3", 16 | "bootstrap-slider": "^10.6.2", 17 | "tinycolor2": "~1.4.1" 18 | }, 19 | "peerDependencies": { 20 | "@girder/core": "*", 21 | "@girder/jobs": "*" 22 | }, 23 | "girderPlugin": { 24 | "name": "slicer_cli_web", 25 | "main": "./main.js", 26 | "dependencies": [ 27 | "jobs" 28 | ] 29 | }, 30 | "eslintConfig": { 31 | "extends": "@girder", 32 | "rules": { 33 | "for-direction": "error", 34 | "getter-return": "error", 35 | "multiline-ternary": [ 36 | "error", 37 | "always-multiline" 38 | ], 39 | "no-alert": "error", 40 | "switch-colon-spacing": "error", 41 | "object-curly-spacing": "off", 42 | "import/exports-last": "error", 43 | "promise/no-native": "error", 44 | "promise/no-return-in-finally": "error", 45 | "promise/no-return-wrap": "error" 46 | }, 47 | "root": true 48 | }, 49 | "eslintIgnore": [ 50 | "**/node_modules/" 51 | ], 52 | "pugLintConfig": { 53 | "extends": "@girder/pug-lint-config", 54 | "excludeFiles": [ 55 | "**/node_modules/" 56 | ] 57 | }, 58 | "stylintrc": { 59 | "blocks": false, 60 | "brackets": { 61 | "expect": "never", 62 | "error": true 63 | }, 64 | "colons": { 65 | "expect": "never", 66 | "error": true 67 | }, 68 | "colors": false, 69 | "commaSpace": { 70 | "expect": "always", 71 | "error": true 72 | }, 73 | "commentSpace": { 74 | "expect": "always", 75 | "error": true 76 | }, 77 | "cssLiteral": { 78 | "expect": "never", 79 | "error": true 80 | }, 81 | "depthLimit": false, 82 | "efficient": { 83 | "expect": "always", 84 | "error": true 85 | }, 86 | "exclude": [ 87 | "**/node_modules/**" 88 | ], 89 | "extendPref": "@extend", 90 | "globalDupe": false, 91 | "groupOutputByFile": { 92 | "expect": true, 93 | "error": true 94 | }, 95 | "indentPref": { 96 | "expect": 2, 97 | "error": true 98 | }, 99 | "leadingZero": { 100 | "expect": "always", 101 | "error": true 102 | }, 103 | "maxErrors": false, 104 | "maxWarnings": false, 105 | "mixed": false, 106 | "mixins": [], 107 | "namingConvention": false, 108 | "namingConventionStrict": false, 109 | "none": { 110 | "expect": "always", 111 | "error": true 112 | }, 113 | "noImportant": false, 114 | "parenSpace": { 115 | "expect": "never", 116 | "error": true 117 | }, 118 | "placeholders": false, 119 | "prefixVarsWithDollar": { 120 | "expect": "always", 121 | "error": true 122 | }, 123 | "quotePref": { 124 | "expect": "double", 125 | "error": true 126 | }, 127 | "reporterOptions": { 128 | "columns": [ 129 | "lineData", 130 | "severity", 131 | "description", 132 | "rule" 133 | ], 134 | "columnSplitter": " ", 135 | "showHeaders": false, 136 | "truncate": true 137 | }, 138 | "semicolons": { 139 | "expect": "never", 140 | "error": true 141 | }, 142 | "sortOrder": false, 143 | "stackedProperties": { 144 | "expect": "never", 145 | "error": true 146 | }, 147 | "trailingWhitespace": { 148 | "expect": "never", 149 | "error": true 150 | }, 151 | "universal": { 152 | "expect": "never", 153 | "error": true 154 | }, 155 | "valid": { 156 | "expect": true, 157 | "error": true 158 | }, 159 | "zeroUnits": { 160 | "expect": "never", 161 | "error": true 162 | }, 163 | "zIndexNormalize": { 164 | "expect": 5, 165 | "error": true 166 | } 167 | }, 168 | "devDependencies": { 169 | "@girder/eslint-config": "^3.0.2", 170 | "@girder/pug-lint-config": "^3.0.2", 171 | "eslint": "^6.5.1", 172 | "eslint-config-semistandard": "^15.0.0", 173 | "eslint-config-standard": "^14.1.0", 174 | "eslint-plugin-backbone": "^2.1.1", 175 | "eslint-plugin-import": "^2.18.2", 176 | "eslint-plugin-node": "^10.0.0", 177 | "eslint-plugin-promise": "^4.2.1", 178 | "eslint-plugin-standard": "^4.0.1", 179 | "pug-lint": "^2.6.0", 180 | "stylint": "^2.0.0" 181 | }, 182 | "scripts": { 183 | "lint": "eslint --cache --fix . && pug-lint . && stylint" 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/parser/constraints.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | import convert from './convert'; 4 | 5 | /** 6 | * Parse a `contraints` tag. 7 | */ 8 | export default function constraints(type, constraintsTag) { 9 | const $c = $(constraintsTag); 10 | const spec = {}; 11 | const min = $c.find('minimum').text(); 12 | const max = $c.find('maximum').text(); 13 | const step = $c.find('step').text(); 14 | if (min) { 15 | spec.min = convert(type, min); 16 | } 17 | if (max) { 18 | spec.max = convert(type, max); 19 | } 20 | if (step) { 21 | spec.step = convert(type, step); 22 | } 23 | return spec; 24 | } 25 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/parser/convert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert from a string to the given value type. 3 | * @param {string} type A widget type 4 | * @param {string} value The value to be converted 5 | * @returns {*} The converted value 6 | */ 7 | export default function convert(type, value) { 8 | if (type === 'number' || type === 'number-enumeration') { 9 | value = parseFloat(value); 10 | } else if (type === 'boolean') { 11 | value = (value.toLowerCase() === 'true'); 12 | } else if (type === 'number-vector') { 13 | value = value.split(',').map(parseFloat); 14 | } else if (type === 'string-vector') { 15 | value = value.split(','); 16 | } 17 | return value; 18 | } 19 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/parser/defaultValue.js: -------------------------------------------------------------------------------- 1 | import convert from './convert'; 2 | 3 | /** 4 | * Parse a `default` tag returning an empty object when no default is given. 5 | * If the default value appears to be a templated string, also return an 6 | * empty object. 7 | */ 8 | export default function defaultValue(type, value) { 9 | if (value.length > 0) { 10 | if (value.text().substr(0, 2) !== '{{' || value.text().substr(Math.max(0, value.text().length - 2)) !== '}}') { 11 | return {value: convert(type, value.text())}; 12 | } 13 | const defstr = '__default__'; 14 | const converted = convert(type, defstr); 15 | if (converted === defstr) { 16 | return {value: converted}; 17 | } 18 | } 19 | return {}; 20 | } 21 | -------------------------------------------------------------------------------- /slicer_cli_web/web_client/parser/group.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import _ from 'underscore'; 3 | 4 | import param from './param'; 5 | 6 | /** 7 | * Parse a parameter group (deliminated by