├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py └── index.rst ├── requirements.txt ├── rorolite ├── __init__.py ├── config.py ├── deploy.py ├── fabfile.py ├── files │ └── etc │ │ └── profile.d │ │ └── rorolite.sh ├── main.py ├── project.py ├── runtime.py ├── runtimes │ ├── python3-keras │ │ └── runtime.yml │ ├── python3 │ │ └── runtime.yml │ └── raspberrypi-python3-keras │ │ └── runtime.yml └── utils.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | build/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include rorolite/runtimes * 2 | recursive-include rorolite/files * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rorolite 2 | 3 | rorolite is a command-line tool to deploy ML applications written in Python to your own server with a single command. It is an open-source tool licensed under Apache license. 4 | 5 | The interface of `rorolite` is based on the interface of [rorodata platform][rorodata]. While `rorolite` is limited to running programs on already created server, the [rorodata platform][rorodata] allows allocating compute resources on demand and provides more tools to streamline data science. 6 | 7 | Currently `rorolite` is limited to deploying one project per server. 8 | 9 | It only supports Ubuntu/Debian distributions of Linux, preferably Ubuntu 16.04. 10 | 11 | [rorodata]: http://rorodata.com/ 12 | 13 | ## Install 14 | 15 | Install `rorolite` using `pip`: 16 | 17 | $ pip install rorolite 18 | 19 | ## How To Use 20 | 21 | Write a `rorolite.yml` specifying the host ipaddress and the services. 22 | 23 | runtime: python3-keras 24 | host: 1.2.3.4 25 | 26 | services: 27 | - name: api 28 | function: credit_risk_service.predict 29 | port: 8000 30 | 31 | - name: webapp 32 | command: gunicorn webapp:app 33 | port: 8080 34 | 35 | Either a function or a command can be specified as a service. When a function is specified as a service, `rorolite` used the [firefly][] to deploy it as a service. 36 | 37 | [firefly]: http://firefly-python.readthedocs.io/ 38 | 39 | The server needs to provisioned once to install all the necessary system software and base dependencies specified by the runtime mentioned in the `rorolite.yml` file. All the application dependencies are installed on every deploy. 40 | 41 | The currently available runtimes are: 42 | 43 | * python3 44 | * python3-keras 45 | 46 | To provision the server, run: 47 | 48 | $ rorolite provision 49 | ... 50 | 51 | To deploy your project, run: 52 | 53 | $ rorolite deploy 54 | Deploying project version 7... 55 | ... 56 | Services are live at: 57 | api -- http://1.2.3.4:8000/ 58 | webapp -- http://1.2.3.4:8080/ 59 | 60 | The `deploy` command pushes your code to the server, sets up a virtual env, installs all the dependencies from your `requirements.txt` file and starts the specified services. 61 | 62 | Inspect the running services using the `ps` command. 63 | 64 | $ rorolite ps 65 | ... 66 | api RUNNING pid 23796, uptime 0:02:07 67 | 68 | The `logs` command allows inspecting logs of any service. 69 | 70 | $ rorolite logs api 71 | 2017-10-25 04:13:12 firefly [INFO] Starting Firefly... 72 | 2017-10-25 04:15:12 predict function called 73 | 74 | The `run` command allows running any command on the remote server. 75 | 76 | $ rorolite run python train.py 77 | starting the training... 78 | reading the input files... 79 | building the model... 80 | saving the model... 81 | done. 82 | 83 | Or you can even start a jupyter notebook server. 84 | 85 | $ rorolite run:notebook 86 | ... 87 | Copy/paste this URL into your browser when you connect for the first time, 88 | to login with a token: 89 | http://1.2.3.4:8888/?token=7f53b445100a5edc0d035fb7ce53061ff7dae351a107ebd4 90 | 91 | Copying files to/from remote server can be done using ``put``/``get`` commands. A directory ``/volumes/data`` is created during provisioning for storing data files, models etc. 92 | 93 | $ rorolite put data/loans.csv /volumes/data/ 94 | ... 95 | [1.2.3.4] put: data/loans.csv -> /volumes/data/loans.csv 96 | 97 | $ rorolite get /volumes/data/model.pkl models/model.pkl 98 | ... 99 | [1.2.3.4] download: models/model.pkl <- /volumes/data/model.pkl 100 | 101 | ## Sample Applications 102 | 103 | Checkout the following sample applications written for rorolite: 104 | 105 | * [iris-demo](https://github.com/rorodata/iris-demo) 106 | * [rorolite-demo](https://github.com/rorodata/rorolite-demo) 107 | 108 | ## LICENSE 109 | 110 | rorolite is licensed under Apache 2 license. Please see LICENSE file for more details. 111 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = rorolite 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # rorolite documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Feb 6 21:54:27 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.githubpages', 'sphinxtogithub'] 35 | 36 | sphinx_to_github = True 37 | sphinx_to_github_verbose = True 38 | sphinx_to_github_encoding = "utf-8" 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'rorolite' 54 | copyright = '2018, rorodata' 55 | author = 'rorodata' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = '0.2.2' 63 | # The full version, including alpha/beta/rc tags. 64 | release = '0.2.2' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = False 83 | 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | html_theme = 'alabaster' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | 104 | # -- Options for HTMLHelp output ------------------------------------------ 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'rorolitedoc' 108 | 109 | 110 | # -- Options for LaTeX output --------------------------------------------- 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'rorolite.tex', 'rorolite Documentation', 135 | 'rorodata', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output --------------------------------------- 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'rorolite', 'rorolite Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'rorolite', 'rorolite Documentation', 156 | author, 'rorolite', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. rorolite documentation master file, created by 2 | sphinx-quickstart on Tue Feb 6 21:54:27 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | rorolite 7 | ======== 8 | 9 | rorolite is an open-source command-line tool to deploy Machine Learning 10 | applications to your own server. It provides simple interface to provision the server to install all the required dependencies and deploy the ML application as an API. 11 | 12 | This is a lite version of the `rorodata platform `_. 13 | 14 | Installation 15 | ------------ 16 | 17 | Install ``rorolite`` using ``pip`` :: 18 | 19 | $ pip install rorolite 20 | 21 | System Requirements 22 | ------------------- 23 | 24 | The target server should be running Ubuntu 16.04. 25 | 26 | How to use 27 | ---------- 28 | 29 | Write a ``rorolite.yml`` specifying the host ipaddress and the services. 30 | :: 31 | 32 | runtime: python3 33 | 34 | # IP address/hostname of the target server 35 | host: 1.2.3.4 36 | 37 | # username on the target server 38 | user: alice 39 | 40 | services: 41 | # run the predict function in credit_risk_service module as an API on port 8000 42 | - name: api 43 | function: credit_risk_service.predict 44 | port: 8000 45 | 46 | # run gunicorn process port 8080 47 | - name: webapp 48 | command: gunicorn webapp:app -b 0.0.0.0:8080 49 | port: 8080 50 | 51 | Either a function or a command can be specified as a service. When a function is specified as a service, rorolite used the `firefly `_ to deploy it as a service. 52 | 53 | The server needs to provisioned once to install all the necessary system software and base dependencies specified by the runtime mentioned in the ``rorolite.yml`` file. All the application dependencies are installed on every deploy. 54 | 55 | The currently available runtimes are: 56 | 57 | * python3 58 | * python3-keras 59 | 60 | To provision the server, run:: 61 | 62 | $ rorolite provision 63 | ... 64 | 65 | To deploy your project, run:: 66 | 67 | $ rorolite deploy 68 | Deploying project version 7... 69 | ... 70 | Services are live at: 71 | api -- http://1.2.3.4:8000/ 72 | webapp -- http://1.2.3.4:8080/ 73 | 74 | The ``deploy`` command pushes your code to the server, sets up a virtual env, installs all the dependencies from your ``requirements.txt`` file and starts the specified services. 75 | 76 | Inspect the running services using the ``ps`` command. 77 | :: 78 | 79 | $ rorolite ps 80 | ... 81 | api RUNNING pid 23796, uptime 0:02:07 82 | 83 | The ``logs`` command allows inspecting logs of any service. 84 | :: 85 | 86 | $ rorolite logs api 87 | 2017-10-25 04:13:12 firefly [INFO] Starting Firefly... 88 | 2017-10-25 04:15:12 predict function called 89 | 90 | The ``run`` command allows running any command on the remote server. 91 | :: 92 | 93 | $ rorolite run python train.py 94 | starting the training... 95 | reading the input files... 96 | building the model... 97 | saving the model... 98 | done. 99 | 100 | Or you can even start a jupyter notebook server. 101 | 102 | :: 103 | 104 | $ rorolite run:notebook 105 | ... 106 | Copy/paste this URL into your browser when you connect for the first time, 107 | to login with a token: 108 | http://1.2.3.4:8888/?token=7f53b445100a5edc0d035fb7ce53061ff7dae351a107ebd4 109 | 110 | Copying files to/from remote server can be done using ``put``/``get`` commands. A directory ``/volumes/data`` is created during provisioning for storing data files, models etc. 111 | 112 | :: 113 | 114 | $ rorolite put data/loans.csv /volumes/data/ 115 | ... 116 | [1.2.3.4] put: data/loans.csv -> /volumes/data/loans.csv 117 | 118 | $ rorolite get /volumes/data/model.pkl models/model.pkl 119 | ... 120 | [1.2.3.4] download: models/model.pkl <- /volumes/data/model.pkl 121 | 122 | License 123 | ------- 124 | 125 | rorolite is licensed under Apache 2 license. Please see `LICENSE `_ file for more details. 126 | 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | Fabric3>=1.13.1.post1 3 | firefly-python>=0.1.9 -------------------------------------------------------------------------------- /rorolite/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.2.3" 3 | -------------------------------------------------------------------------------- /rorolite/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import sys 4 | import yaml 5 | 6 | class Config: 7 | def __init__(self, config): 8 | self.config = config 9 | 10 | if "host" not in self.config: 11 | raise Exception("Missing required field in the config file: host") 12 | 13 | self.host = config['host'] 14 | self.user = config.get('user') or os.getlogin() 15 | 16 | @staticmethod 17 | def load(filename): 18 | config = yaml.safe_load(open(filename)) 19 | return Config(config) 20 | 21 | def load_config(directory): 22 | path = os.path.join(directory, "rorolite.yml") 23 | if not os.path.exists(path): 24 | print("Unable to find rorolite.yml file", file=sys.stderr) 25 | sys.exit(1) 26 | return Config.load(path) 27 | -------------------------------------------------------------------------------- /rorolite/deploy.py: -------------------------------------------------------------------------------- 1 | """Deployment flow. 2 | """ 3 | import os 4 | import pathlib 5 | import yaml 6 | import tempfile 7 | import shutil 8 | from fabric.api import env, sudo, lcd 9 | import fabric.api as remote 10 | from .project import Project 11 | 12 | SUPERVISOR_CONFIG = """ 13 | [program:{name}] 14 | command = {command} 15 | directory = {directory} 16 | redirect_stderr = true 17 | stdout_logfile = /var/log/supervisor/%(program_name)s.log 18 | environment = 19 | PATH="/opt/rorolite/project/.rorolite/env/bin:%(ENV_PATH)s" 20 | """ 21 | 22 | class Deployment: 23 | def __init__(self, directory="."): 24 | self.project = Project(directory) 25 | self.directory = directory 26 | self.config = None 27 | self.version = 0 28 | self.deploy_root = None 29 | 30 | def read_config(self, root): 31 | path = os.path.join(root, "rorolite.yml") 32 | return yaml.safe_load(open(path).read()) 33 | 34 | def deploy(self): 35 | self.config = self.read_config(self.directory) 36 | if "host" not in self.config: 37 | raise Exception("Missing required field in rorolite.yml: host") 38 | 39 | self.version = self.find_current_version() + 1 40 | self.deploy_root = "/opt/rorolite/deploys/{}".format(self.version) 41 | print("Deploying project version {}...".format(self.version)) 42 | 43 | remote.sudo("mkdir -p " + self.deploy_root) 44 | 45 | self.push_directory() 46 | self.setup_virtualenv() 47 | 48 | remote.sudo("ln -sfT {} /opt/rorolite/project".format(self.deploy_root)) 49 | 50 | self.restart_services() 51 | 52 | def find_current_version(self): 53 | output = remote.run("ls /opt/rorolite/deploys 2>/dev/null || echo", quiet=True) 54 | versions = [int(v) for v in output.strip().split() if v.isnumeric()] 55 | return versions and max(versions) or 0 56 | 57 | def push_directory(self): 58 | with tempfile.TemporaryDirectory() as tmpdir: 59 | archive = self.archive(rootdir=".", output_dir=tmpdir) 60 | remote.put(archive, "/tmp/rorolite-project.tgz") 61 | 62 | with lcd(tmpdir): 63 | self.generate_supervisor_config(rootdir=tmpdir) 64 | 65 | supervisor_archive = self.archive(tmpdir, base_dir=".rorolite", filename="rorolite-supervisor") 66 | remote.put(supervisor_archive, "/tmp/rorolite-supervisor.tgz") 67 | 68 | with remote.cd(self.deploy_root): 69 | remote.sudo("chown {} .".format(env.user)) 70 | remote.run("tar xzf /tmp/rorolite-project.tgz") 71 | remote.run("tar xzf /tmp/rorolite-supervisor.tgz") 72 | 73 | def setup_virtualenv(self): 74 | print("setting up virtualenv...") 75 | with remote.cd(self.deploy_root): 76 | python_binary = self.project.runtime.python_binary 77 | remote.run("{python} -m virtualenv --system-site-packages -p {python} .rorolite/env".format(python=python_binary)) 78 | # The system wide installation of firefly and jupyter is creating 79 | # some trouble with import paths. 80 | # Installing them in the virtualenv as a work-around 81 | remote.run(".rorolite/env/bin/pip install -q firefly-python jupyter jupyterlab") 82 | if os.path.exists("requirements.txt"): 83 | # install all the application dependencies 84 | remote.run(".rorolite/env/bin/pip install -q -r requirements.txt") 85 | 86 | def archive(self, rootdir, output_dir=None, format='gztar', base_dir=".", filename='rorolite-project'): 87 | output_dir = output_dir or rootdir 88 | base_name = os.path.join(output_dir, filename) 89 | return shutil.make_archive(base_name, format, root_dir=rootdir, base_dir=base_dir) 90 | 91 | def restart_services(self): 92 | services = self.config.get('services', []) 93 | # TODO: validate services 94 | sudo("rm -rf /etc/supervisor/conf.d && ln -sfT /opt/rorolite/project/.rorolite/supervisor /etc/supervisor/conf.d") 95 | sudo("supervisorctl update") 96 | 97 | if not services: 98 | print("Deploy successful. No services found.") 99 | return 100 | 101 | for s in services: 102 | sudo("supervisorctl restart {}".format(s['name'])) 103 | 104 | host = self.config['host'] 105 | print("Services are live at:") 106 | for s in services: 107 | print(" {} -- http://{}:{}/".format(s['name'], host, s['port'])) 108 | 109 | def generate_supervisor_config(self, rootdir): 110 | # Create the supervisor directory 111 | path = pathlib.Path(rootdir).joinpath(".rorolite", "supervisor") 112 | 113 | # XXX-Anand: Jan 2018 114 | # Passing exist_ok to mkdir is failing mysteriously. 115 | # This very function is working fine when called indepenently. 116 | # May be there is some monkey-patching going on in Fabric. 117 | # The following work-around takes care of it. 118 | if not path.exists(): 119 | path.mkdir(parents=True) 120 | 121 | services = self.config.get('services', []) 122 | for s in services: 123 | self._generate_config(s, rootdir=rootdir) 124 | 125 | def _generate_config(self, service, rootdir): 126 | print("generating supervisor config for " + service['name']) 127 | name = service['name'] 128 | function = service.get('function') 129 | command = service.get('command') 130 | port = service['port'] 131 | directory = "/opt/rorolite/project/" + service.get("directory", "") 132 | 133 | if function: 134 | command = '/opt/rorolite/project/.rorolite/env/bin/firefly -b 0.0.0.0:{port} {function}'.format(port=port, function=function) 135 | 136 | if command is None: 137 | raise Exception("command is not specified for service {!r}".format(name)) 138 | 139 | path = pathlib.Path(rootdir).joinpath(".rorolite", "supervisor", name + ".conf") 140 | if not path.parent.exists(): 141 | path.parent.mkdir(parents=True) 142 | 143 | text = SUPERVISOR_CONFIG.format(name=name, directory=directory, command=command) 144 | with path.open("w") as f: 145 | f.write(text) 146 | -------------------------------------------------------------------------------- /rorolite/fabfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from fabric.api import task, run, env, cd, sudo, put, get 3 | from fabric.tasks import execute, Task 4 | from .utils import hijack_output_loop 5 | from .deploy import Deployment 6 | from .project import Project 7 | 8 | # Fabric prints all the messages with a '[hostname] out:' prefix. 9 | # Hijacking it to remove the prefix 10 | hijack_output_loop() 11 | 12 | @task 13 | def hello(name="world"): 14 | with cd("."): 15 | run("echo hello " + name) 16 | 17 | @task 18 | def run_command(command, workdir=None): 19 | workdir = workdir or "/opt/rorolite/project" 20 | command_str = " ".join(command) 21 | with cd(workdir): 22 | run(command_str) 23 | 24 | @task 25 | def run_notebook(workdir=None, args=None, kwargs=None): 26 | args = args or [] 27 | kwargs = kwargs or {} 28 | command = "jupyter notebook --ip {host} --allow-root".format(host=env.host).split() + list(args) 29 | return run_command(command, workdir=workdir) 30 | 31 | @task 32 | def run_jupyterlab(workdir=None, args=None, kwargs=None): 33 | args = args or [] 34 | kwargs = kwargs or {} 35 | command = "jupyter lab --ip {host} --allow-root".format(host=env.host).split() + list(args) 36 | return run_command(command, workdir=workdir) 37 | 38 | @task 39 | def restart(service): 40 | sudo("supervisorctl restart " + service) 41 | 42 | @task 43 | def logs(service, n=10, follow=False): 44 | follow_flag = "-f" if follow else "" 45 | cmd = "tail -n {} {} /var/log/supervisor/{}.log".format(n, follow_flag, service) 46 | sudo(cmd) 47 | 48 | @task 49 | def deploy(): 50 | d = Deployment() 51 | d.deploy() 52 | 53 | @task 54 | def provision(): 55 | project = Project() 56 | project.runtime.install() 57 | setup_volumes() 58 | 59 | @task 60 | def putfile(src, dest): 61 | put(src, dest) 62 | 63 | @task 64 | def getfile(src, dest): 65 | get(src, dest) 66 | 67 | @task 68 | def supervisorctl(*args): 69 | sudo("supervisorctl " + " ".join(args)) 70 | 71 | def setup_volumes(): 72 | sudo("mkdir -p /volumes/data") 73 | sudo("chown {} /volumes".format(env.user)) 74 | sudo("chown {} /volumes/data".format(env.user)) 75 | 76 | def run_task(taskname, *args, **kwargs): 77 | task = globals().get(taskname) 78 | if isinstance(task, Task): 79 | execute(task, *args, **kwargs) 80 | else: 81 | raise Exception("Invalid task: " + repr(taskname)) 82 | -------------------------------------------------------------------------------- /rorolite/files/etc/profile.d/rorolite.sh: -------------------------------------------------------------------------------- 1 | export PATH=/opt/rorolite/project/.rorolite/env/bin:$PATH -------------------------------------------------------------------------------- /rorolite/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import click 3 | from fabric.api import env as fabric_env 4 | from . import __version__ 5 | from . import fabfile 6 | from . import config 7 | 8 | @click.group() 9 | @click.version_option(version=__version__) 10 | def cli(verbose=False): 11 | """rorolite is a tool to deploy ML applications to your server. 12 | """ 13 | conf = config.load_config(".") 14 | fabric_env.hosts = [conf.host] 15 | fabric_env.user = conf.user 16 | 17 | @cli.command(context_settings={"allow_interspersed_args": False}) 18 | @click.argument("command", nargs=-1) 19 | @click.option("-w", "--workdir") 20 | def run(command, workdir=None): 21 | fabfile.run_task("run_command", command=command, workdir=workdir) 22 | 23 | @cli.command(name="run:notebook", context_settings={ 24 | "allow_interspersed_args": False, 25 | "allow_extra_args": True, 26 | "ignore_unknown_options": True,}) 27 | @click.argument("args", nargs=-1) 28 | @click.option("-w", "--workdir") 29 | def run_notebook(workdir=None, args=[], **kwargs): 30 | fabfile.run_task("run_notebook", workdir=workdir, args=args, kwargs=kwargs) 31 | 32 | @cli.command(name="run:lab", context_settings={ 33 | "allow_interspersed_args": False, 34 | "allow_extra_args": True, 35 | "ignore_unknown_options": True,}) 36 | @click.argument("args", nargs=-1) 37 | @click.option("-w", "--workdir") 38 | def run_jupyterlab(workdir=None, args=[], **kwargs): 39 | fabfile.run_task("run_jupyterlab", workdir=workdir, args=args, kwargs=kwargs) 40 | 41 | @cli.command() 42 | def provision(): 43 | fabfile.run_task("provision") 44 | 45 | @cli.command() 46 | def deploy(): 47 | fabfile.run_task("deploy") 48 | 49 | @cli.command() 50 | @click.argument("name") 51 | @click.option("-n", default=10, type=int) 52 | @click.option("-f", "--follow", is_flag=True, default=False) 53 | def logs(name, n=10, follow=False): 54 | fabfile.run_task("logs", service=name, n=n, follow=follow) 55 | 56 | @cli.command() 57 | def ps(): 58 | fabfile.run_task("supervisorctl", "status") 59 | 60 | @cli.command() 61 | @click.argument("name") 62 | def stop(name): 63 | fabfile.run_task("supervisorctl", "stop", name) 64 | 65 | @cli.command() 66 | @click.argument("name") 67 | def start(name): 68 | fabfile.run_task("supervisorctl", "start", name) 69 | 70 | @cli.command() 71 | @click.argument("name") 72 | def restart(name): 73 | fabfile.run_task("supervisorctl", "restart", name) 74 | 75 | @cli.command() 76 | @click.argument("name") 77 | def hello(name="world"): 78 | """Prints a hello world message on the remote server. 79 | """ 80 | fabfile.run_task("hello", name=name) 81 | 82 | @cli.command() 83 | @click.argument("src") 84 | @click.argument("dest") 85 | def put(src, dest): 86 | """Copies a local file to remote server. 87 | """ 88 | fabfile.run_task("putfile", src=src, dest=dest) 89 | 90 | @cli.command() 91 | @click.argument("src") 92 | @click.argument("dest") 93 | def get(src, dest): 94 | """Copies a file from remote server to local machine. 95 | """ 96 | fabfile.run_task("getfile", src=src, dest=dest) 97 | 98 | @cli.command() 99 | def help(): 100 | """Show this help message.""" 101 | cli.main(args=[]) 102 | 103 | def main(): 104 | cli() 105 | -------------------------------------------------------------------------------- /rorolite/project.py: -------------------------------------------------------------------------------- 1 | """ 2 | rorolite.project 3 | ~~~~~~~~~~~~~~~~ 4 | 5 | This module implements the project object. 6 | """ 7 | import os 8 | import yaml 9 | from .runtime import Runtime, DEFAULT_RUNTIME 10 | 11 | class Project: 12 | def __init__(self, root="."): 13 | self.root = root 14 | self.metadata = self.read_metadata() 15 | 16 | @property 17 | def runtime(self): 18 | runtime = self.metadata.get('runtime', DEFAULT_RUNTIME) 19 | return Runtime.load(runtime) 20 | 21 | def __getitem__(self, key): 22 | return self.metadata[key] 23 | 24 | def read_metadata(self): 25 | path = os.path.join(self.root, "rorolite.yml") 26 | return yaml.safe_load(open(path)) 27 | -------------------------------------------------------------------------------- /rorolite/runtime.py: -------------------------------------------------------------------------------- 1 | """ 2 | rorolite.runtime 3 | ~~~~~~~~~~~~~~~~ 4 | 5 | This module implements support for multiple rorolite runtimes. 6 | """ 7 | from fabric.api import sudo, put, cd 8 | from pkg_resources import resource_listdir, resource_exists, resource_filename, resource_stream 9 | import yaml 10 | import pathlib 11 | 12 | DEFAULT_RUNTIME = "python3" 13 | 14 | class Runtime(object): 15 | """Runtime represents a software setup. 16 | 17 | Some examples of runtimes are python3, python3-keras, raspberrypi-python3-keras 18 | etc. 19 | 20 | The runtime object contains all the metadata required to install 21 | all the required software on a computer. 22 | 23 | It provides an `install` method, which is supposed to be called 24 | from a fabric task. It'll perform some actions on the remote 25 | machine to complete the setup. 26 | """ 27 | def __init__(self, name, version, data): 28 | self.root = resource_filename(__name__, "runtimes/" + name) 29 | self.name = name 30 | self.version = version 31 | self.data = data 32 | self.init() 33 | 34 | def __repr__(self): 35 | return "".format(self.name) 36 | 37 | def init(self): 38 | self.apt_packages = self.data.get("apt_packages", []) 39 | self.pip_packages = self.data.get("pip_packages", []) 40 | self.python_binary = self.data.get("python_binary", "python") 41 | self.before_scripts = self.data.get("before_scripts", []) 42 | self.after_scripts = self.data.get("after_scripts", []) 43 | 44 | def install(self): 45 | """Installs the runtime on a remote machine. 46 | 47 | This method must be called only from a fabric task. 48 | """ 49 | target = "/tmp/rorolite-runtime/" + self.name 50 | sudo("mkdir -p " + target) 51 | 52 | put(self.root, "/tmp/rorolite-runtime", use_sudo=True) 53 | if self.before_scripts: 54 | with cd(target): 55 | for s in self.before_scripts: 56 | print("executing", s) 57 | sudo(s) 58 | 59 | if self.apt_packages: 60 | sudo("apt-get -q update") 61 | sudo("apt-get -q -y install " + " ".join(self.apt_packages)) 62 | 63 | if self.pip_packages: 64 | sudo("{} -m pip -q install {}".format( 65 | self.python_binary, " ".join(self.pip_packages))) 66 | 67 | if self.after_scripts: 68 | with cd(target): 69 | for s in self.after_scripts: 70 | print("executing", s) 71 | sudo(s) 72 | 73 | self.setup_system_path() 74 | 75 | def setup_system_path(self): 76 | path = pathlib.Path(__file__).parent / "files" / "etc" / "profile.d" / "rorolite.sh" 77 | put(str(path), "/etc/profile.d/Z99-rorolite.sh", use_sudo=True) 78 | 79 | @classmethod 80 | def all(cls): 81 | def is_runtime(name): 82 | return resource_exists("runtimes/{}/runtime.yml".format(name)) 83 | 84 | names = resource_listdir(__name__, "runtimes") 85 | names = [is_runtime(name) for name in names] 86 | return [cls.load(name) for name in names] 87 | 88 | @classmethod 89 | def load(cls, name): 90 | """Loads the runtime of the specified name. 91 | """ 92 | f = resource_stream(__name__, "runtimes/{}/runtime.yml".format(name)) 93 | data = yaml.safe_load(f) 94 | return Runtime(data['name'], data['version'], data) 95 | -------------------------------------------------------------------------------- /rorolite/runtimes/python3-keras/runtime.yml: -------------------------------------------------------------------------------- 1 | name: python3-keras 2 | version: 18.01 3 | 4 | python_binary: /usr/bin/python3.5 5 | 6 | apt_packages: 7 | - build-essential 8 | - cmake 9 | - pkg-config 10 | - libblas-dev 11 | - liblapack-dev 12 | - libatlas-base-dev 13 | - gfortran 14 | - git 15 | 16 | # image libraries for opencv 17 | - libopenjp2-7 18 | - libjpeg-dev 19 | - libjasper-dev 20 | - libtiff5 21 | - libtiff5-dev 22 | - libpng12-dev 23 | 24 | # video libraries for opencv 25 | - libavcodec-dev 26 | - libavformat-dev 27 | - libswscale-dev 28 | - libv4l-dev 29 | - libxvidcore-dev 30 | - libx264-dev 31 | 32 | # other packages required for opencv 33 | - libilmbase12 34 | - openexr 35 | - libgstreamer1.0-0 36 | - libgtk-3-0 37 | 38 | - libhdf5-10 39 | - python3.5-dev 40 | - python3-pip 41 | - python3-virtualenv 42 | - supervisor 43 | 44 | pip_packages: 45 | - numpy 46 | - scipy 47 | - pandas 48 | - matplotlib 49 | - scikit-learn 50 | - scikit-image 51 | - opencv-python 52 | - nltk 53 | - sympy 54 | - joblib 55 | - h5py 56 | - pillow 57 | - SQLAlchemy 58 | - tensorflow 59 | - keras 60 | - Pillow 61 | - h5py 62 | -------------------------------------------------------------------------------- /rorolite/runtimes/python3/runtime.yml: -------------------------------------------------------------------------------- 1 | name: python3 2 | version: 18.01 3 | 4 | python_binary: /usr/bin/python3.5 5 | 6 | apt_packages: 7 | - build-essential 8 | - cmake 9 | - pkg-config 10 | - libblas-dev 11 | - liblapack-dev 12 | - libatlas-base-dev 13 | - gfortran 14 | - git 15 | - python3.5-dev 16 | - python3-pip 17 | - python3-virtualenv 18 | - supervisor 19 | 20 | pip_packages: 21 | - numpy 22 | - scipy 23 | - pandas 24 | - matplotlib 25 | - scikit-learn 26 | - scikit-image 27 | - Pillow 28 | - SQLAlchemy 29 | -------------------------------------------------------------------------------- /rorolite/runtimes/raspberrypi-python3-keras/runtime.yml: -------------------------------------------------------------------------------- 1 | name: raspberrypi-python3-keras 2 | version: 18.01 3 | 4 | python_binary: /usr/bin/python3.5 5 | 6 | apt_packages: 7 | - build-essential 8 | - cmake 9 | - pkg-config 10 | - libblas-dev 11 | - liblapack-dev 12 | - libatlas-base-dev 13 | - gfortran 14 | - git 15 | 16 | # image libraries for opencv 17 | - libopenjp2-7 18 | - libjpeg-dev 19 | - libjasper-dev 20 | - libtiff5 21 | - libtiff5-dev 22 | - libpng12-dev 23 | 24 | # video libraries for opencv 25 | - libavcodec-dev 26 | - libavformat-dev 27 | - libswscale-dev 28 | - libv4l-dev 29 | - libxvidcore-dev 30 | - libx264-dev 31 | 32 | # other packages required for opencv 33 | - libilmbase12 34 | - openexr 35 | - libgstreamer1.0-0 36 | - libgtk-3-0 37 | 38 | - libhdf5-100 39 | - python3.5-dev 40 | - python3-pip 41 | - python3-virtualenv 42 | - supervisor 43 | 44 | pip_packages: 45 | - numpy 46 | - scipy 47 | - pandas 48 | - matplotlib 49 | - scikit-learn 50 | - scikit-image 51 | - opencv-python 52 | - nltk 53 | - sympy 54 | - joblib 55 | - h5py 56 | - pillow 57 | - SQLAlchemy 58 | - https://s3.amazonaws.com/rorodata-raspberrypi/tensorflow-1.5.0rc1-cp35-none-any.whl 59 | - keras 60 | - Pillow 61 | - h5py -------------------------------------------------------------------------------- /rorolite/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from fabric import io 3 | 4 | OutputLooper = io.OutputLooper 5 | 6 | class RoroliteOutputLooper(OutputLooper): 7 | """Replacement to OutputLooper of Fabric that doesn't print prefix 8 | in the output. 9 | """ 10 | def __init__(self, *args, **kwargs): 11 | OutputLooper.__init__(self, *args, **kwargs) 12 | self.prefix = "" 13 | 14 | def hijack_output_loop(): 15 | """Hijacks the fabric's output loop to supress the '[hostname] out:' 16 | prefix from output. 17 | """ 18 | io.OutputLooper = RoroliteOutputLooper 19 | 20 | def setup_logger(verbose=False): 21 | if verbose: 22 | level = logging.DEBUG 23 | else: 24 | level = logging.INFO 25 | logging.basicConfig(format='[%(name)s] %(message)s', level=level) 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | rorolite 3 | ======== 4 | 5 | rorolite is a command-line tool to deploy ML applications written in Python to your own server with a single command. It is an open-source tool licensed under Apache license. 6 | 7 | The interface of ``rorolite`` is based on the interface of `rorodata platform `_. While ``rorolite`` is limited to running programs on already created server, the [rorodata platform][rorodata] allows allocating compute resources on demand and provides more tools to streamline data science. 8 | 9 | Currently ``rorolite`` is limited to deploying one project per server. 10 | 11 | See ``_ for more details. 12 | 13 | """ 14 | from setuptools import setup, find_packages 15 | 16 | __version__ = '0.2.2' 17 | 18 | setup( 19 | name='rorolite', 20 | version=__version__, 21 | author='rorodata', 22 | author_email='rorodata.team@gmail.com', 23 | description='Command-line tool to deploy ML applications to your own server with a single command', 24 | long_description=__doc__, 25 | packages=find_packages(), 26 | include_package_data=True, 27 | zip_safe=False, 28 | install_requires=[ 29 | 'click==6.7', 30 | 'Fabric3>=1.13.1.post1', 31 | 'firefly-python>=0.1.9' 32 | ], 33 | entry_points=''' 34 | [console_scripts] 35 | rorolite=rorolite.main:main 36 | ''', 37 | ) 38 | --------------------------------------------------------------------------------