├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── mls ├── __init__.py ├── client │ ├── __init__.py │ ├── client.py │ └── utils.py ├── server │ ├── __init__.py │ ├── base.py │ ├── preparator.py │ ├── server.py │ ├── state_machine.py │ └── utils.py └── tests │ ├── __init__.py │ └── test.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | *.DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS,tests,experiments 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=2 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # Allow loading of arbitrary C extensions. Extensions are imported into the 34 | # active Python interpreter and may run arbitrary code. 35 | unsafe-load-any-extension=no 36 | 37 | 38 | [MESSAGES CONTROL] 39 | 40 | # Only show warnings with the listed confidence levels. Leave empty to show 41 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 42 | confidence= 43 | 44 | # Disable the message, report, category or checker with the given id(s). You 45 | # can either give multiple identifiers separated by comma (,) or put this 46 | # option multiple times (only on the command line, not in the configuration 47 | # file where it should appear only once).You can also use "--disable=all" to 48 | # disable everything first and then reenable specific checks. For example, if 49 | # you want to run only the similarities checker, you can use "--disable=all 50 | # --enable=similarities". If you want to run only the classes checker, but have 51 | # no Warning level messages displayed, use"--disable=all --enable=classes 52 | # --disable=W" 53 | disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,R0903,W0702,W0401,W0703 54 | 55 | # Enable the message, report, category or checker with the given id(s). You can 56 | # either give multiple identifier separated by comma (,) or put this option 57 | # multiple time (only on the command line, not in the configuration file where 58 | # it should appear only once). See also the "--disable" option for examples. 59 | enable= 60 | 61 | 62 | [REPORTS] 63 | 64 | # Python expression which should return a note less than 10 (10 is the highest 65 | # note). You have access to the variables errors warning, statement which 66 | # respectively contain the number of errors / warnings messages and the total 67 | # number of statements analyzed. This is used by the global evaluation report 68 | # (RP0004). 69 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 70 | 71 | # Template used to display messages. This is a python new-style format string 72 | # used to format the message information. See doc for all details 73 | #msg-template= 74 | 75 | # Set the output format. Available formats are text, parseable, colorized, json 76 | # and msvs (visual studio).You can also give a reporter class, eg 77 | # mypackage.mymodule.MyReporterClass. 78 | output-format=colorized 79 | 80 | # Tells whether to display a full report or only the messages 81 | reports=no 82 | 83 | # Activate the evaluation score. 84 | score=yes 85 | 86 | 87 | [REFACTORING] 88 | 89 | # Maximum number of nested blocks for function / method body 90 | max-nested-blocks=5 91 | 92 | 93 | [BASIC] 94 | 95 | # Naming hint for argument names 96 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 97 | 98 | # Regular expression matching correct argument names 99 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 100 | 101 | # Naming hint for attribute names 102 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 103 | 104 | # Regular expression matching correct attribute names 105 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 106 | 107 | # Bad variable names which should always be refused, separated by a comma 108 | bad-names=foo,bar,baz,toto,tutu,tata 109 | 110 | # Naming hint for class attribute names 111 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 112 | 113 | # Regular expression matching correct class attribute names 114 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 115 | 116 | # Naming hint for class names 117 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 118 | 119 | # Regular expression matching correct class names 120 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 121 | 122 | # Naming hint for constant names 123 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 124 | 125 | # Regular expression matching correct constant names 126 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 127 | 128 | # Minimum line length for functions/classes that require docstrings, shorter 129 | # ones are exempt. 130 | docstring-min-length=-1 131 | 132 | # Naming hint for function names 133 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 134 | 135 | # Regular expression matching correct function names 136 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 137 | 138 | # Good variable names which should always be accepted, separated by a comma 139 | good-names=i,j,k,ex,Run,_,x,y,z,e,ml 140 | 141 | # Include a hint for the correct naming format with invalid-name 142 | include-naming-hint=no 143 | 144 | # Naming hint for inline iteration names 145 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 146 | 147 | # Regular expression matching correct inline iteration names 148 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 149 | 150 | # Naming hint for method names 151 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 152 | 153 | # Regular expression matching correct method names 154 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 155 | 156 | # Naming hint for module names 157 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 158 | 159 | # Regular expression matching correct module names 160 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 161 | 162 | # Colon-delimited sets of names that determine each other's naming style when 163 | # the name regexes allow several styles. 164 | name-group= 165 | 166 | # Regular expression which should only match function or class names that do 167 | # not require a docstring. 168 | no-docstring-rgx=^_ 169 | 170 | # List of decorators that produce properties, such as abc.abstractproperty. Add 171 | # to this list to register other decorators that produce valid properties. 172 | property-classes=abc.abstractproperty 173 | 174 | # Naming hint for variable names 175 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 176 | 177 | # Regular expression matching correct variable names 178 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 179 | 180 | 181 | [FORMAT] 182 | 183 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 184 | expected-line-ending-format= 185 | 186 | # Regexp for a line that is allowed to be longer than the limit. 187 | ignore-long-lines=^\s*(# )??$ 188 | 189 | # Number of spaces of indent required inside a hanging or continued line. 190 | indent-after-paren=4 191 | 192 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 193 | # tab). 194 | indent-string=' ' 195 | 196 | # Maximum number of characters on a single line. 197 | max-line-length=100 198 | 199 | # Maximum number of lines in a module 200 | max-module-lines=1000 201 | 202 | # List of optional constructs for which whitespace checking is disabled. `dict- 203 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 204 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 205 | # `empty-line` allows space-only lines. 206 | no-space-check=trailing-comma,dict-separator 207 | 208 | # Allow the body of a class to be on the same line as the declaration if body 209 | # contains single statement. 210 | single-line-class-stmt=no 211 | 212 | # Allow the body of an if to be on the same line as the test if there is no 213 | # else. 214 | single-line-if-stmt=no 215 | 216 | 217 | [LOGGING] 218 | 219 | # Logging modules to check that the string format arguments are in logging 220 | # function parameter format 221 | logging-modules=logging 222 | 223 | 224 | [MISCELLANEOUS] 225 | 226 | # List of note tags to take in consideration, separated by a comma. 227 | notes=FIXME,XXX,TODO 228 | 229 | 230 | [SIMILARITIES] 231 | 232 | # Ignore comments when computing similarities. 233 | ignore-comments=yes 234 | 235 | # Ignore docstrings when computing similarities. 236 | ignore-docstrings=yes 237 | 238 | # Ignore imports when computing similarities. 239 | ignore-imports=no 240 | 241 | # Minimum lines number of a similarity. 242 | min-similarity-lines=4 243 | 244 | 245 | [SPELLING] 246 | 247 | # Spelling dictionary name. Available dictionaries: none. To make it working 248 | # install python-enchant package. 249 | spelling-dict= 250 | 251 | # List of comma separated words that should not be checked. 252 | spelling-ignore-words= 253 | 254 | # A path to a file that contains private dictionary; one word per line. 255 | spelling-private-dict-file= 256 | 257 | # Tells whether to store unknown words to indicated private dictionary in 258 | # --spelling-private-dict-file option instead of raising a message. 259 | spelling-store-unknown-words=no 260 | 261 | 262 | [TYPECHECK] 263 | 264 | # List of decorators that produce context managers, such as 265 | # contextlib.contextmanager. Add to this list to register other decorators that 266 | # produce valid context managers. 267 | contextmanager-decorators=contextlib.contextmanager 268 | 269 | # List of members which are set dynamically and missed by pylint inference 270 | # system, and so shouldn't trigger E1101 when accessed. Python regular 271 | # expressions are accepted. 272 | generated-members= 273 | 274 | # Tells whether missing members accessed in mixin class should be ignored. A 275 | # mixin class is detected if its name ends with "mixin" (case insensitive). 276 | ignore-mixin-members=yes 277 | 278 | # This flag controls whether pylint should warn about no-member and similar 279 | # checks whenever an opaque object is returned when inferring. The inference 280 | # can return multiple potential results while evaluating a Python object, but 281 | # some branches might not be evaluated, which results in partial inference. In 282 | # that case, it might be useful to still emit no-member and other checks for 283 | # the rest of the inferred objects. 284 | ignore-on-opaque-inference=yes 285 | 286 | # List of class names for which member attributes should not be checked (useful 287 | # for classes with dynamically set attributes). This supports the use of 288 | # qualified names. 289 | ignored-classes=optparse.Values,thread._local,_thread._local 290 | 291 | # List of module names for which member attributes should not be checked 292 | # (useful for modules/projects where namespaces are manipulated during runtime 293 | # and thus existing member attributes cannot be deduced by static analysis. It 294 | # supports qualified module names, as well as Unix pattern matching. 295 | ignored-modules=cv2 296 | 297 | # Show a hint with possible names when a member name was not found. The aspect 298 | # of finding the hint is based on edit distance. 299 | missing-member-hint=yes 300 | 301 | # The minimum edit distance a name should have in order to be considered a 302 | # similar match for a missing member name. 303 | missing-member-hint-distance=1 304 | 305 | # The total number of similar names that should be taken in consideration when 306 | # showing a hint for a missing member. 307 | missing-member-max-choices=1 308 | 309 | 310 | [VARIABLES] 311 | 312 | # List of additional names supposed to be defined in builtins. Remember that 313 | # you should avoid to define new builtins when possible. 314 | additional-builtins= 315 | 316 | # Tells whether unused global variables should be treated as a violation. 317 | allow-global-unused-variables=yes 318 | 319 | # List of strings which can identify a callback function by name. A callback 320 | # name must start or end with one of those strings. 321 | callbacks=cb_,_cb 322 | 323 | # A regular expression matching the name of dummy variables (i.e. expectedly 324 | # not used). 325 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 326 | 327 | # Argument names that match this expression will be ignored. Default to name 328 | # with leading underscore 329 | ignored-argument-names=_.*|^ignored_|^unused_ 330 | 331 | # Tells whether we should check for unused import in __init__ files. 332 | init-import=no 333 | 334 | # List of qualified module names which can have objects that can redefine 335 | # builtins. 336 | redefining-builtins-modules=six.moves,future.builtins 337 | 338 | 339 | [CLASSES] 340 | 341 | # List of method names used to declare (i.e. assign) instance attributes. 342 | defining-attr-methods=__init__,__new__,setUp 343 | 344 | # List of member names, which should be excluded from the protected access 345 | # warning. 346 | exclude-protected=_asdict,_fields,_replace,_source,_make 347 | 348 | # List of valid names for the first argument in a class method. 349 | valid-classmethod-first-arg=cls 350 | 351 | # List of valid names for the first argument in a metaclass class method. 352 | valid-metaclass-classmethod-first-arg=mcs 353 | 354 | 355 | [DESIGN] 356 | 357 | # Maximum number of arguments for function / method 358 | max-args=20 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=20 362 | 363 | # Maximum number of boolean expressions in a if statement 364 | max-bool-expr=5 365 | 366 | # Maximum number of branch for function / method body 367 | max-branches=12 368 | 369 | # Maximum number of locals for function / method body 370 | max-locals=15 371 | 372 | # Maximum number of parents for a class (see R0901). 373 | max-parents=7 374 | 375 | # Maximum number of public methods for a class (see R0904). 376 | max-public-methods=20 377 | 378 | # Maximum number of return / yield for function / method body 379 | max-returns=6 380 | 381 | # Maximum number of statements in function / method body 382 | max-statements=50 383 | 384 | # Minimum number of public methods for a class (see R0903). 385 | min-public-methods=2 386 | 387 | 388 | [IMPORTS] 389 | 390 | # Allow wildcard imports from modules that define __all__. 391 | allow-wildcard-with-all=no 392 | 393 | # Analyse import fallback blocks. This can be used to support both Python 2 and 394 | # 3 compatible code, which means that the block might have code that exists 395 | # only in one or another interpreter, leading to false positives when analysed. 396 | analyse-fallback-blocks=no 397 | 398 | # Deprecated modules which should not be used, separated by a comma 399 | deprecated-modules=optparse,tkinter.tix 400 | 401 | # Create a graph of external dependencies in the given file (report RP0402 must 402 | # not be disabled) 403 | ext-import-graph= 404 | 405 | # Create a graph of every (i.e. internal and external) dependencies in the 406 | # given file (report RP0402 must not be disabled) 407 | import-graph= 408 | 409 | # Create a graph of internal dependencies in the given file (report RP0402 must 410 | # not be disabled) 411 | int-import-graph= 412 | 413 | # Force import order to recognize a module as part of the standard 414 | # compatibility libraries. 415 | known-standard-library= 416 | 417 | # Force import order to recognize a module as part of a third party library. 418 | known-third-party=enchant 419 | 420 | 421 | [EXCEPTIONS] 422 | 423 | # Exceptions that will emit a warning when being caught. Defaults to 424 | # "Exception" 425 | overgeneral-exceptions=Exception 426 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 connectome.ai 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server and client for hosting ML based solutions 2 | 3 | ## Installation 4 | 5 | `pip3 install myLittleServer` 6 | 7 | ## Server creation 8 | 9 | Server can implement simple interface with just predict method. Use it, when you don't need to save any information which can be obtained in progress. 10 | 11 | Example of class which fits expected interface: 12 | 13 | ```python 14 | class Interface: 15 | 16 | def __init__(self, **kwargs): 17 | pass 18 | 19 | # data can be any object, which you expect to receive 20 | def predict(self, data): 21 | return data 22 | ``` 23 | 24 | To run server with such interface, type this code: 25 | ```python 26 | from mls import Server 27 | # Create instance of Interface class 28 | interface = Interface() 29 | # Server initialization 30 | server = Server(port=3001, ml=interface) 31 | ``` 32 | 33 | If predict is not a pure function (results of it depends on the state), than StateMachine is your way to go! 34 | Example of class which fits expected interface: 35 | 36 | ```python 37 | class StateMachineInterface: 38 | 39 | # data can be any object, which you expect to receive 40 | # method has to return events in predefined format 41 | def update(self, data): 42 | events = self._process(data) 43 | return events 44 | 45 | # data can be any object, which you expect to receive 46 | def set_state(self, state): 47 | self._update_my_state(state) 48 | return None 49 | ``` 50 | 51 | To run server with such interface, type this code: 52 | 53 | ```python 54 | interface = StateMachineInterface() 55 | 56 | from mls import StateMachine 57 | 58 | # Server initialization 59 | server = StateMachine(port=3001, ml=interface) 60 | ``` 61 | 62 | Following syntax is the same for both `Server` and `StateMachine`. 63 | 64 | You can start ml module initialization process inside server. It can be helpful if you want to check when server is ready. In that case pass Interface constructor and it's kwargs as separate params. 65 | 66 | ```python 67 | from mls import Server 68 | # Your init kwargs 69 | config = {'foo': 'bar'} 70 | # Server initialization 71 | # You need to provide class itself, not object of that class 72 | # It's the same as if you do Interface(**ml_config) 73 | server = Server(port=3001, ml=Interface, ml_config=config) 74 | ``` 75 | 76 | You can turn off server logs by providing `log=False`: 77 | ```python 78 | # You can turn off logging 79 | server = Server(port=3001, ml=interface, log=False) 80 | ``` 81 | 82 | ## Client creation 83 | 84 | Client initialization: 85 | ```python 86 | from mls import Client 87 | 88 | # Client initialization 89 | client = Client(address='http://localhost:3001') 90 | ``` 91 | 92 | Check server status: 93 | ```python 94 | # Check that server launched 95 | is_started = client.started() 96 | # Check that ml module is set up and ready for work 97 | is_ready = client.ready() 98 | # You can call only ready() as it has inner check for case, when server hasn't started 99 | ``` 100 | 101 | Result of client call is [future](https://docs.python.org/3/library/concurrent.futures.html). So all it's methods work fine. 102 | 103 | ```python 104 | res = client.predict(data_to_process) 105 | ``` 106 | 107 | Get the body of the result. It's a blocking operation: 108 | 109 | ```python 110 | data = res.result() 111 | ``` 112 | 113 | Best practice is to check res for exception before running `result()` method as it will throw exception if one occurred on the server side. 114 | 115 | ```python 116 | # Otherwise res.result() will raise an exception. 117 | if res.exception() is None: 118 | data = res.result() 119 | ``` 120 | 121 | You can check if call to server is finished: 122 | 123 | ```python 124 | while True: 125 | if not res.done(): 126 | sleep(1) 127 | else: 128 | do_stuff(res.result()) 129 | ``` 130 | 131 | You can also add a callback function: 132 | 133 | ```python 134 | res.add_done_callback(lambda res: do_stuff(res.result())) 135 | ``` 136 | -------------------------------------------------------------------------------- /mls/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | from .client import Client 4 | from .server import Server, StateMachine 5 | 6 | __all__ = ['Client', 'Server', 'StateMachine'] 7 | -------------------------------------------------------------------------------- /mls/client/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | from .client import Client 4 | 5 | __all__ = ['Client'] 6 | -------------------------------------------------------------------------------- /mls/client/client.py: -------------------------------------------------------------------------------- 1 | """Client implementation.""" 2 | 3 | 4 | import os 5 | from concurrent.futures import ThreadPoolExecutor 6 | 7 | import requests 8 | 9 | from .utils import _prepare_input, _prepare_output 10 | 11 | 12 | class Client: 13 | """ 14 | Client is a representation of a client. 15 | 16 | :param address: address of a server like http://localhost:3000 17 | """ 18 | 19 | def __init__(self, address): 20 | self._address = os.path.join(address, 'jsonrpc') 21 | self._id = 0 22 | self._pool = ThreadPoolExecutor(1) 23 | 24 | def _increment_id(self): 25 | self._id += 1 26 | if self._id > 10 ** 8: 27 | self._id = 0 28 | 29 | def _send_request(self, method, data): 30 | 31 | response = requests.post( 32 | self._address, 33 | data=_prepare_input(data), 34 | headers={'method': method} 35 | ) 36 | 37 | error = response.headers.get('error') 38 | if error != '': 39 | raise ValueError(error) 40 | 41 | return _prepare_output(response.content) 42 | 43 | def _ping(self): 44 | return self._send_request('ready', None) 45 | 46 | def ready(self): 47 | """ 48 | ready tells if server is ready. 49 | 50 | :returns: true or false 51 | """ 52 | try: 53 | return self._ping() 54 | except requests.exceptions.ConnectionError: 55 | return False 56 | 57 | def started(self): 58 | """ 59 | sstarted tells if server started. 60 | 61 | :returns: true or false 62 | """ 63 | try: 64 | _ = self._ping() 65 | return True 66 | except requests.exceptions.ConnectionError: 67 | return False 68 | 69 | def train(self, data): 70 | """ 71 | train launches training ml algorithm on server on provided data. 72 | 73 | :param data: data to process 74 | """ 75 | return self._pool.submit(self._send_request, 'train', data) 76 | 77 | def update(self, data): 78 | """ 79 | update sends new data to state machine. 80 | 81 | :param data: data to process 82 | """ 83 | 84 | return self._pool.submit(self._send_request, 'update', data) 85 | 86 | def set_state(self, data): 87 | """ 88 | set_state sends new state data to state machine. 89 | 90 | :param data: data to process 91 | """ 92 | 93 | return self._pool.submit(self._send_request, 'set_state', data) 94 | 95 | def predict(self, data): 96 | """ 97 | predict launches prediction ml algorithm on server on provided data. 98 | 99 | :param data: data to process 100 | """ 101 | 102 | return self._pool.submit(self._send_request, 'predict', data) 103 | -------------------------------------------------------------------------------- /mls/client/utils.py: -------------------------------------------------------------------------------- 1 | """utils for clients""" 2 | 3 | import pickle 4 | 5 | 6 | def _prepare_input(data): 7 | return pickle.dumps(data) 8 | 9 | 10 | def _prepare_output(data): 11 | return pickle.loads(data) 12 | -------------------------------------------------------------------------------- /mls/server/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | from .server import Server 4 | from .state_machine import StateMachine 5 | 6 | __all__ = ['Server', 'StateMachine'] 7 | -------------------------------------------------------------------------------- /mls/server/base.py: -------------------------------------------------------------------------------- 1 | """BaseServer implementation.""" 2 | 3 | 4 | import logging 5 | import traceback 6 | import gc 7 | import sys 8 | from concurrent.futures import ThreadPoolExecutor 9 | 10 | # from jsonrpc import JSONRPCResponseManager, dispatcher 11 | from werkzeug.serving import run_simple 12 | from werkzeug.wrappers import Request, Response 13 | 14 | from .utils import serialize_data 15 | 16 | 17 | class BaseServer: 18 | """ 19 | Server is a representation of a server. 20 | 21 | :param port: port in form of integer 22 | :param ml: interface of ml object, need to implement predict method 23 | :param log: turn logging off or on. True by default. 24 | :param address: address of a server, keep default value if you don't understand what it's for 25 | """ 26 | 27 | def __init__(self, port, ml, address='0.0.0.0', ml_config=None, log=True, mem_limit=50000000): 28 | if not isinstance(mem_limit, int): 29 | raise ValueError('mem_limit must be int') 30 | if not log: 31 | logging.getLogger('werkzeug').setLevel(logging.ERROR) 32 | for name, _ in logging.Logger.manager.loggerDict.items(): 33 | if any([x in name for x in ['requests', 'werkzeug', 'urllib']]): 34 | logging.getLogger(name).setLevel(logging.ERROR) 35 | self._address = address 36 | self._port = port 37 | self._pool = ThreadPoolExecutor(1) 38 | self._dispatcher = {} 39 | 40 | self._ml_config = ml_config 41 | if ml_config is not None: 42 | self._ml_constructor = ml 43 | self._ml = None 44 | else: 45 | self._ml = ml 46 | 47 | self._total_requests_memory_consumption = 0 48 | self._MEMORY_LIMIT = mem_limit 49 | 50 | self.set_dispatcher({'ready': self._ready}) 51 | 52 | def _init_ml(self): 53 | self._ml = self._ml_constructor(**self._ml_config) 54 | 55 | @serialize_data 56 | def _ready(self, _): 57 | return self._ml is not None 58 | 59 | def _check_memory_consumption(self, data): 60 | """ 61 | We got a problem with memory consumption in Werkzeug in large requests (5-10+MB) 62 | As it appears Werkzeug scheduler hardly ever clear app memory 63 | Here we do it for him 64 | 65 | :param data: request.data 66 | """ 67 | 68 | if self._total_requests_memory_consumption > self._MEMORY_LIMIT: 69 | gc.collect() 70 | 71 | self._total_requests_memory_consumption = 0 72 | 73 | self._total_requests_memory_consumption += sys.getsizeof(data) 74 | 75 | def set_dispatcher(self, route_table, clear=False): 76 | """ 77 | set_dispatcher sets dispacther keys and values. 78 | 79 | :param route_table: route_table is a dict with keys as routes and values as callables 80 | """ 81 | 82 | if clear: 83 | self._dispatcher = {} 84 | for route, func in route_table.items(): 85 | self._dispatcher[route] = func 86 | 87 | return self 88 | 89 | @staticmethod 90 | def _init_cb(future): 91 | if future.exception() is not None: 92 | raise RuntimeError(future.exception()) 93 | 94 | def __call__(self): 95 | if self._ml_config is not None: 96 | self._pool.submit(self._init_ml).add_done_callback(self._init_cb) 97 | 98 | @Request.application 99 | def _application(request): 100 | self._check_memory_consumption(request.data) 101 | # dispatcher is dictionary {: callable} 102 | method = request.headers['method'] 103 | headers = {'error': ''} 104 | 105 | try: 106 | response = self._dispatcher[method](request.data) 107 | except Exception as e: 108 | response = b'' 109 | headers['error'] = str(e) 110 | logging.critical(str(e)) 111 | error = traceback.format_exc() 112 | logging.critical(error) 113 | 114 | return Response(response, headers=headers) 115 | 116 | run_simple(self._address, self._port, _application) 117 | -------------------------------------------------------------------------------- /mls/server/preparator.py: -------------------------------------------------------------------------------- 1 | """preparator contains EchoPreparator class.""" 2 | 3 | 4 | class EchoPreparator: 5 | """EchoPreparator returns input and output as it is.""" 6 | 7 | @staticmethod 8 | def prepare_input(data): 9 | """prepare_input prepares input.""" 10 | return data 11 | 12 | @staticmethod 13 | def prepare_output(data): 14 | """prepare_output prepares output.""" 15 | return data 16 | -------------------------------------------------------------------------------- /mls/server/server.py: -------------------------------------------------------------------------------- 1 | """Server implementation.""" 2 | 3 | 4 | from .base import BaseServer 5 | from .preparator import EchoPreparator 6 | from .utils import serialize_data 7 | 8 | 9 | class Server(BaseServer): 10 | """ 11 | Server is a representation of a server. 12 | 13 | :param port: port in form of integer 14 | :param log: is logging enabled or not 15 | :param ml: interface of ml object, need to implement predict method 16 | :param address: address of a server, keep default value if you don't understand what it's for 17 | """ 18 | 19 | def __init__(self, port, ml, address='0.0.0.0', preparator=EchoPreparator, 20 | ml_config=None, log=True): 21 | if not any(method in dir(ml) for method in [ 22 | 'predict', 'train']): 23 | raise ValueError('ml must have predict or train method') 24 | 25 | if not all(method in dir(preparator) for method in [ 26 | 'prepare_output', 'prepare_input']): 27 | raise ValueError( 28 | 'preparator must have prepare_output and prepare_input methods') 29 | 30 | super().__init__(port, ml, address, ml_config, log) 31 | 32 | self.set_dispatcher({ 33 | 'train': self._train, 34 | 'predict': self._predict 35 | }) 36 | 37 | self._preparator = preparator 38 | 39 | @serialize_data 40 | def _predict(self, data): 41 | return self._ml.predict(data) 42 | 43 | @serialize_data 44 | def _train(self, data): 45 | prepared_data = self._preparator.prepare_input(data) 46 | res = self._ml.train(prepared_data) 47 | prepared_res = self._preparator.prepare_output(res) 48 | return prepared_res 49 | -------------------------------------------------------------------------------- /mls/server/state_machine.py: -------------------------------------------------------------------------------- 1 | """StateMachine implementation.""" 2 | 3 | from .base import BaseServer 4 | from .utils import serialize_data 5 | 6 | 7 | class StateMachine(BaseServer): 8 | """ 9 | StateMachine is a representation of a state machine. 10 | 11 | :param port: port in form of integer 12 | :param log: is logging enabled or not 13 | :param ml: interface of ml object, need to implement predict method 14 | :param address: address of a server, keep default value if you don't understand what it's for 15 | """ 16 | 17 | def __init__(self, port, ml, address='0.0.0.0', 18 | ml_config=None, log=True): 19 | if not all(method in dir(ml) for method in [ 20 | 'update', 'set_state']): 21 | raise ValueError('ml must have update and set_state method') 22 | 23 | super().__init__(port, ml, address, ml_config, log) 24 | 25 | self.set_dispatcher({ 26 | 'update': self._update, 27 | 'set_state': self._set_state 28 | }) 29 | 30 | @serialize_data 31 | def _update(self, data): 32 | return self._ml.update(data) 33 | 34 | @serialize_data 35 | def _set_state(self, data): 36 | return self._ml.set_state(data) 37 | -------------------------------------------------------------------------------- /mls/server/utils.py: -------------------------------------------------------------------------------- 1 | """utils for servers""" 2 | 3 | import pickle 4 | 5 | 6 | def serialize_data(func): 7 | """parse_data is a decorator which serialize incoming and outgoing data.""" 8 | def _function_wrapper(*args): 9 | return _prepare_output(func(args[0], _prepare_input(args[-1]))) 10 | 11 | return _function_wrapper 12 | 13 | 14 | def _prepare_input(data): 15 | return pickle.loads(data) 16 | 17 | 18 | def _prepare_output(data): 19 | return pickle.dumps(data) 20 | -------------------------------------------------------------------------------- /mls/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectome-ai/mls/f3511b62ae629caae5f66ca96300af6897da5b84/mls/tests/__init__.py -------------------------------------------------------------------------------- /mls/tests/test.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | import os 4 | import unittest 5 | from datetime import datetime, timedelta 6 | from multiprocessing import Process 7 | from time import sleep 8 | 9 | import numpy as np 10 | 11 | from mls import Client, Server, StateMachine 12 | 13 | TEST_ALL = os.getenv('TEST_ALL') 14 | STEP_TIME = 0.1 15 | SHORTER_SLEEP_TIME = 0.3 16 | SLEEP_TIME = SHORTER_SLEEP_TIME + STEP_TIME 17 | LONGER_SLEEP_TIME = SLEEP_TIME + STEP_TIME 18 | 19 | 20 | def _sync_call(addr, data): 21 | client = Client(address=addr) 22 | res = client.predict(data) 23 | _ = res.result() 24 | 25 | 26 | class FittingInterface: 27 | 28 | def __init__(self, long_predict=False, long_init=False): 29 | self._long_predict = long_predict 30 | if long_init: 31 | sleep(SLEEP_TIME) 32 | 33 | def predict(self, data): 34 | if self._long_predict: 35 | sleep(SLEEP_TIME) 36 | 37 | return data 38 | 39 | def update(self, data): 40 | return None 41 | 42 | def set_state(self, data): 43 | return None 44 | 45 | def train(self, data): 46 | if self._long_predict: 47 | sleep(SLEEP_TIME) 48 | 49 | return data 50 | 51 | 52 | class FailingInterface(FittingInterface): 53 | 54 | def predict(self, data): 55 | raise ValueError('some error') 56 | 57 | 58 | class TestServer(unittest.TestCase): 59 | 60 | @classmethod 61 | def setUpClass(cls): 62 | a = [[255, 128, 3], 63 | [255, 127, 2], 64 | [253, 125, 0]] 65 | cls._data = np.array([[[a, a, a], [a, a, a]], [[a, a, a], [a, a, a]]]) 66 | cls._short_server = Server( 67 | port=36789, ml=FittingInterface(), log=False) 68 | cls._short_server_address = 'http://127.0.0.1:36789' 69 | cls._pr1 = Process(target=cls._short_server) 70 | cls._long_server = Server( 71 | port=36790, ml=FittingInterface(True), log=False) 72 | cls._pr2 = Process(target=cls._long_server) 73 | cls._long_server_address = 'http://127.0.0.1:36790' 74 | cls._sm = StateMachine( 75 | port=36791, ml=FittingInterface(False), log=False) 76 | cls._pr3 = Process(target=cls._sm) 77 | cls._sm_address = 'http://127.0.0.1:36791' 78 | cls._pr1.start() 79 | cls._pr2.start() 80 | cls._pr3.start() 81 | 82 | @classmethod 83 | def tearDownClass(cls): 84 | cls._pr1.terminate() 85 | cls._pr2.terminate() 86 | cls._pr3.terminate() 87 | 88 | def test_bad_server_init(self): 89 | s = Server(port=36710, ml=FittingInterface, ml_config={ 90 | 'shit': False, 'poop': True}) 91 | with self.assertRaises(RuntimeError): 92 | s() 93 | 94 | def test_sm(self): 95 | client = Client(address=self._sm_address) 96 | client.set_state(self._data) 97 | client.update(self._data) 98 | 99 | def test_ml(self): 100 | ml = FittingInterface.__init__(FittingInterface, **{ 101 | 'long_predict': False, 'long_init': True}) 102 | 103 | def test_ready(self): 104 | s = Server(port=36794, ml=FittingInterface, ml_config={ 105 | 'long_predict': False, 'long_init': True}) 106 | pr = Process(target=s) 107 | client = Client(address='http://127.0.0.1:36794') 108 | self.assertFalse(client.started()) 109 | 110 | res = client.predict(self._data) 111 | self.assertIsNotNone(res.exception()) 112 | pr.start() 113 | while not client.started(): 114 | pass 115 | self.assertFalse(client.ready()) 116 | while not client.ready(): 117 | pass 118 | self.assertTrue(client.ready()) 119 | pr.terminate() 120 | 121 | def test_ready_safe(self): 122 | s = Server(port=36795, ml=FittingInterface, ml_config={ 123 | 'long_predict': False, 'long_init': True}) 124 | pr = Process(target=s) 125 | client = Client(address='http://127.0.0.1:36795') 126 | pr.start() 127 | while not client.ready(): 128 | pass 129 | pr.terminate() 130 | 131 | def test_fails(self): 132 | s = Server(port=36795, ml=FailingInterface, ml_config={ 133 | 'long_predict': False, 'long_init': True}) 134 | pr = Process(target=s) 135 | client = Client(address='http://127.0.0.1:36795') 136 | pr.start() 137 | while not client.ready(): 138 | pass 139 | with self.assertRaises(ValueError): 140 | res = client.predict(self._data) 141 | res.result() 142 | pr.terminate() 143 | 144 | def test_init_ml(self): 145 | Server(port=36795, ml=FittingInterface, ml_config={}) 146 | 147 | def test_interface(self): 148 | with self.assertRaises(ValueError): 149 | Server(port=36789, ml='test') 150 | 151 | def test_train(self): 152 | client = Client(address=self._short_server_address) 153 | res = client.train(self._data) 154 | self.assertListEqual(res.result().tolist(), self._data.tolist()) 155 | 156 | def test_server_sync(self): 157 | client = Client(address=self._short_server_address) 158 | res = client.predict(self._data) 159 | self.assertListEqual(res.result().tolist(), self._data.tolist()) 160 | 161 | def test_server_sync_long(self): 162 | client = Client(address=self._long_server_address) 163 | now = datetime.now() 164 | res = client.predict(self._data) 165 | res = res.result() 166 | self.assertTrue(datetime.now() - now > 167 | timedelta(seconds=SLEEP_TIME - STEP_TIME)) 168 | self.assertListEqual(res.tolist(), self._data.tolist()) 169 | 170 | def test_server_async(self): 171 | client = Client(address=self._long_server_address) 172 | now = datetime.now() 173 | res = client.predict(self._data) 174 | self.assertFalse(res.done()) 175 | sleep(LONGER_SLEEP_TIME) 176 | self.assertListEqual(res.result().tolist(), self._data.tolist()) 177 | 178 | def test_multi_client(self): 179 | client1 = Client(address=self._long_server_address) 180 | client2 = Client(address=self._long_server_address) 181 | 182 | def test_server_multi_requests_one_source(self): 183 | client1 = Client(address=self._long_server_address) 184 | client2 = Client(address=self._long_server_address) 185 | while not client1.ready() and not client2.ready(): 186 | pass 187 | res1 = client1.predict(self._data) 188 | sleep(SHORTER_SLEEP_TIME) 189 | res2 = client2.predict(self._data) 190 | self.assertFalse(res1.done()) 191 | self.assertFalse(res2.done()) 192 | sleep(SLEEP_TIME - SHORTER_SLEEP_TIME + STEP_TIME) 193 | self.assertTrue(res1.done()) 194 | self.assertFalse(res2.done()) 195 | sleep(SLEEP_TIME) 196 | self.assertTrue(res1.done()) 197 | self.assertTrue(res2.done()) 198 | 199 | def test_server_multi_requests_different_sources(self): 200 | pr1 = Process(target=_sync_call, args=( 201 | self._long_server_address, self._data,)) 202 | pr2 = Process(target=_sync_call, args=( 203 | self._long_server_address, self._data,)) 204 | now = datetime.now() 205 | pr1.start() 206 | pr2.start() 207 | pr1.join() 208 | pr2.join() 209 | self.assertTrue(datetime.now() - now > 210 | timedelta(seconds=SLEEP_TIME * 2 - STEP_TIME)) 211 | 212 | def test_callback(self): 213 | client = Client(address=self._long_server_address) 214 | res = client.predict(self._data) 215 | res.add_done_callback( 216 | lambda x: self.assertListEqual(x.result().tolist(), self._data.tolist())) 217 | sleep(LONGER_SLEEP_TIME) 218 | 219 | 220 | if __name__ == '__main__': 221 | unittest.main() 222 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | import distutils.cmd 4 | import distutils.log 5 | import os 6 | import subprocess 7 | 8 | from setuptools import find_packages, setup 9 | 10 | 11 | class PylintCommand(distutils.cmd.Command): 12 | """A custom command to run Pylint on all Python source files.""" 13 | 14 | description = 'run Pylint on Python source files' 15 | user_options = [ 16 | # The format is (long option, short option, description). 17 | ('pylint-rcfile=', None, 'path to Pylint config file'), 18 | ] 19 | 20 | def initialize_options(self): 21 | """Set default values for options.""" 22 | # Each user option must be listed here with their default value. 23 | self.pylint_rcfile = '.pylintrc' 24 | 25 | def finalize_options(self): 26 | """Post-process options.""" 27 | if self.pylint_rcfile: 28 | assert os.path.exists(self.pylint_rcfile), ( 29 | 'Pylint config file %s does not exist.' % self.pylint_rcfile) 30 | 31 | def run(self): 32 | """Run command.""" 33 | command = ['pylint'] 34 | if self.pylint_rcfile: 35 | command.append('--rcfile=%s' % self.pylint_rcfile) 36 | command.append(os.getcwd() + '/mls') 37 | self.announce( 38 | 'Running command: %s' % str(command), 39 | level=distutils.log.INFO) 40 | subprocess.check_call(command) 41 | 42 | 43 | tests_require = [ 44 | 'numpy>=1.12.1', 45 | ] 46 | 47 | install_requires = [ 48 | 'requests>=2.13.0', 49 | 'Werkzeug>=0.12.1' 50 | ] 51 | 52 | CONFIG = { 53 | 'name': 'myLittleServer', 54 | 'url': 'https://github.com/connectome-ai/mls', 55 | 'download_url': 'https://github.com/connectome-ai/mls/archive/1.2.3.tar.gz', 56 | 'version': '1.2.3', 57 | 'description': 'mls is a wrapper around ml code.', 58 | 'author': 'connectome.ai', 59 | 'test_suite': 'mls', 60 | 'packages': find_packages(exclude=['tests', '*.tests', '*.tests.*']), 61 | 'tests_require': tests_require, 62 | 'install_requires': install_requires, 63 | 'cmdclass': { 64 | 'pylint': PylintCommand 65 | }, 66 | } 67 | 68 | setup(**CONFIG) 69 | --------------------------------------------------------------------------------