From fa4a8267a2ec050bc8a15d6a15289ac81e43e92a Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 10 Dec 2018 14:21:14 +0100 Subject: [PATCH] commit to publish --- .gitignore | 231 ++ .noseids | Bin 0 -> 16537 bytes Dockerfile | 14 + LICENSE | 201 ++ NOTICE | 73 + Procfile | 3 + README.rst | 187 ++ THIRDPARTY | 154 ++ conda-requirements.txt | 13 + docker-compose-dev.yml | 12 + docker-compose.yml | 37 + docs/Makefile | 250 ++ docs/README | 18 + docs/requirements.txt | 5 + docs/source/_static/custom.css | 6 + docs/source/admin/configuration.rst | 112 + docs/source/admin/design.rst | 46 + docs/source/admin/index.rst | 13 + docs/source/admin/kickstart.rst | 78 + docs/source/admin/putget.rst | 34 + docs/source/admin/spark.rst | 9 + docs/source/conf.py | 364 +++ docs/source/devguide/architecture.rst | 164 ++ docs/source/devguide/custom.rst | 43 + docs/source/devguide/index.rst | 11 + docs/source/devguide/metadata.rst | 52 + docs/source/devguide/mixins.rst | 113 + docs/source/devguide/storage.rst | 209 ++ docs/source/guide/custombackends.rst | 19 + docs/source/guide/filterdf.rst | 454 ++++ docs/source/guide/getstarted.rst | 44 + docs/source/guide/index.rst | 17 + docs/source/guide/lambda.rst | 93 + docs/source/guide/mdfapply.rst | 222 ++ docs/source/guide/modelstore.rst | 175 ++ docs/source/guide/restapi.rst | 464 ++++ docs/source/guide/runtimes.rst | 60 + docs/source/guide/store_retrieve.rst | 70 + docs/source/guide/streaming.rst | 124 + docs/source/images/deployment.gif | Bin 0 -> 10887 bytes docs/source/images/deployment.jpg | Bin 0 -> 62941 bytes docs/source/images/deployment.png | Bin 0 -> 13014 bytes docs/source/images/deployment.svg | 317 +++ docs/source/images/deployment.uxf | 282 +++ docs/source/images/screenshots/dashboard.png | Bin 0 -> 90122 bytes docs/source/images/screenshots/deployment.png | Bin 0 -> 13014 bytes docs/source/images/screenshots/help.png | Bin 0 -> 202886 bytes docs/source/images/screenshots/notebook.png | Bin 0 -> 258904 bytes docs/source/images/screenshots/restapi.png | Bin 0 -> 132504 bytes docs/source/index.rst | 14 + docs/source/nb/omegaml sample code.ipynb | 2122 +++++++++++++++++ docs/source/reference/developer.rst | 73 + docs/source/reference/index.rst | 10 + docs/source/reference/public.rst | 109 + docs/source/screenshots.rst | 28 + omegaml/__init__.py | 31 + omegaml/_version.py | 1 + omegaml/backends/__init__.py | 3 + omegaml/backends/basedata.py | 58 + omegaml/backends/basemodel.py | 167 ++ omegaml/backends/scikitlearn.py | 247 ++ omegaml/celery_util.py | 39 + omegaml/celeryapp.py | 18 + omegaml/defaults.py | 155 ++ omegaml/documents.py | 84 + omegaml/jobs.py | 266 +++ omegaml/mdataframe.py | 1007 ++++++++ omegaml/mixins/__init__.py | 1 + omegaml/mixins/mdf/__init__.py | 2 + omegaml/mixins/mdf/apply.py | 997 ++++++++ omegaml/mixins/mdf/filterops.py | 36 + omegaml/mixins/store/__init__.py | 1 + omegaml/mixins/store/projected.py | 101 + omegaml/notebook/__init__.py | 0 omegaml/notebook/checkpoints.py | 30 + omegaml/notebook/jupyter/__init__.py | 0 omegaml/notebook/jupyter/ipystart.py | 10 + omegaml/notebook/jupyter/ipython_config.py | 10 + .../jupyter/jupyter_notebook_config.py | 633 +++++ omegaml/notebook/omegacontentsmgr.py | 223 ++ omegaml/notebook/tasks.py | 94 + omegaml/omega.py | 107 + omegaml/restapi/__init__.py | 146 ++ omegaml/restapi/__main__.py | 5 + omegaml/restapi/app.py | 11 + omegaml/restapi/tests/__init__.py | 0 omegaml/restapi/tests/test_api.py | 120 + omegaml/restapi/util.py | 26 + omegaml/runtimes/__init__.py | 4 + omegaml/runtimes/daskruntime.py | 119 + omegaml/runtimes/jobproxy.py | 52 + omegaml/runtimes/mixins/__init__.py | 2 + omegaml/runtimes/mixins/gridsearch.py | 12 + omegaml/runtimes/mixins/modelmixin.py | 191 ++ omegaml/runtimes/modelproxy.py | 73 + omegaml/runtimes/runtime.py | 72 + omegaml/store/__init__.py | 6 + omegaml/store/base.py | 976 ++++++++ omegaml/store/fastinsert.py | 57 + omegaml/store/filtered.py | 169 ++ omegaml/store/query.py | 402 ++++ omegaml/store/queryops.py | 353 +++ omegaml/tasks.py | 105 + omegaml/tests/__init__.py | 1 + omegaml/tests/test_backendbase.py | 89 + omegaml/tests/test_filtered_collection.py | 113 + omegaml/tests/test_jobs.py | 122 + omegaml/tests/test_mdataframe.py | 451 ++++ omegaml/tests/test_mdfmixins.py | 508 ++++ omegaml/tests/test_query.py | 105 + omegaml/tests/test_runtime.py | 325 +++ omegaml/tests/test_store.py | 622 +++++ omegaml/util.py | 448 ++++ requirements.txt | 6 + scripts/mongoinit.js | 6 + setup.py | 44 + shippable.yml | 40 + 117 files changed, 17251 insertions(+) create mode 100644 .gitignore create mode 100644 .noseids create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 Procfile create mode 100644 README.rst create mode 100644 THIRDPARTY create mode 100644 conda-requirements.txt create mode 100644 docker-compose-dev.yml create mode 100644 docker-compose.yml create mode 100644 docs/Makefile create mode 100644 docs/README create mode 100644 docs/requirements.txt create mode 100644 docs/source/_static/custom.css create mode 100644 docs/source/admin/configuration.rst create mode 100644 docs/source/admin/design.rst create mode 100644 docs/source/admin/index.rst create mode 100644 docs/source/admin/kickstart.rst create mode 100644 docs/source/admin/putget.rst create mode 100644 docs/source/admin/spark.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/devguide/architecture.rst create mode 100644 docs/source/devguide/custom.rst create mode 100644 docs/source/devguide/index.rst create mode 100644 docs/source/devguide/metadata.rst create mode 100644 docs/source/devguide/mixins.rst create mode 100644 docs/source/devguide/storage.rst create mode 100644 docs/source/guide/custombackends.rst create mode 100644 docs/source/guide/filterdf.rst create mode 100644 docs/source/guide/getstarted.rst create mode 100644 docs/source/guide/index.rst create mode 100644 docs/source/guide/lambda.rst create mode 100644 docs/source/guide/mdfapply.rst create mode 100644 docs/source/guide/modelstore.rst create mode 100644 docs/source/guide/restapi.rst create mode 100644 docs/source/guide/runtimes.rst create mode 100644 docs/source/guide/store_retrieve.rst create mode 100644 docs/source/guide/streaming.rst create mode 100644 docs/source/images/deployment.gif create mode 100644 docs/source/images/deployment.jpg create mode 100644 docs/source/images/deployment.png create mode 100644 docs/source/images/deployment.svg create mode 100644 docs/source/images/deployment.uxf create mode 100644 docs/source/images/screenshots/dashboard.png create mode 100644 docs/source/images/screenshots/deployment.png create mode 100644 docs/source/images/screenshots/help.png create mode 100644 docs/source/images/screenshots/notebook.png create mode 100644 docs/source/images/screenshots/restapi.png create mode 100644 docs/source/index.rst create mode 100644 docs/source/nb/omegaml sample code.ipynb create mode 100644 docs/source/reference/developer.rst create mode 100644 docs/source/reference/index.rst create mode 100644 docs/source/reference/public.rst create mode 100644 docs/source/screenshots.rst create mode 100644 omegaml/__init__.py create mode 100644 omegaml/_version.py create mode 100644 omegaml/backends/__init__.py create mode 100644 omegaml/backends/basedata.py create mode 100644 omegaml/backends/basemodel.py create mode 100644 omegaml/backends/scikitlearn.py create mode 100644 omegaml/celery_util.py create mode 100644 omegaml/celeryapp.py create mode 100644 omegaml/defaults.py create mode 100644 omegaml/documents.py create mode 100644 omegaml/jobs.py create mode 100644 omegaml/mdataframe.py create mode 100644 omegaml/mixins/__init__.py create mode 100644 omegaml/mixins/mdf/__init__.py create mode 100644 omegaml/mixins/mdf/apply.py create mode 100644 omegaml/mixins/mdf/filterops.py create mode 100644 omegaml/mixins/store/__init__.py create mode 100644 omegaml/mixins/store/projected.py create mode 100644 omegaml/notebook/__init__.py create mode 100644 omegaml/notebook/checkpoints.py create mode 100644 omegaml/notebook/jupyter/__init__.py create mode 100644 omegaml/notebook/jupyter/ipystart.py create mode 100644 omegaml/notebook/jupyter/ipython_config.py create mode 100644 omegaml/notebook/jupyter/jupyter_notebook_config.py create mode 100644 omegaml/notebook/omegacontentsmgr.py create mode 100644 omegaml/notebook/tasks.py create mode 100644 omegaml/omega.py create mode 100644 omegaml/restapi/__init__.py create mode 100644 omegaml/restapi/__main__.py create mode 100644 omegaml/restapi/app.py create mode 100644 omegaml/restapi/tests/__init__.py create mode 100644 omegaml/restapi/tests/test_api.py create mode 100644 omegaml/restapi/util.py create mode 100644 omegaml/runtimes/__init__.py create mode 100644 omegaml/runtimes/daskruntime.py create mode 100644 omegaml/runtimes/jobproxy.py create mode 100644 omegaml/runtimes/mixins/__init__.py create mode 100644 omegaml/runtimes/mixins/gridsearch.py create mode 100644 omegaml/runtimes/mixins/modelmixin.py create mode 100644 omegaml/runtimes/modelproxy.py create mode 100644 omegaml/runtimes/runtime.py create mode 100644 omegaml/store/__init__.py create mode 100644 omegaml/store/base.py create mode 100644 omegaml/store/fastinsert.py create mode 100644 omegaml/store/filtered.py create mode 100644 omegaml/store/query.py create mode 100644 omegaml/store/queryops.py create mode 100644 omegaml/tasks.py create mode 100644 omegaml/tests/__init__.py create mode 100644 omegaml/tests/test_backendbase.py create mode 100644 omegaml/tests/test_filtered_collection.py create mode 100644 omegaml/tests/test_jobs.py create mode 100644 omegaml/tests/test_mdataframe.py create mode 100644 omegaml/tests/test_mdfmixins.py create mode 100644 omegaml/tests/test_query.py create mode 100644 omegaml/tests/test_runtime.py create mode 100644 omegaml/tests/test_store.py create mode 100644 omegaml/util.py create mode 100644 requirements.txt create mode 100644 scripts/mongoinit.js create mode 100644 setup.py create mode 100644 shippable.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..57fac3e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,231 @@ +node_modules/ +.idea +celerybeat* +omegaweb/node_modules/* +omegaweb/package-lock.json +jupyterhub_cookie_secret +jupyterhub.sqlite +ybdata + +# Created by https://www.gitignore.io/api/eclipse + +### Eclipse ### + +.metadata +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# End of https://www.gitignore.io/api/eclipse + +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# End of https://www.gitignore.io/api/python + +# Created by https://www.gitignore.io/api/pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + + +# End of https://www.gitignore.io/api/pycharm diff --git a/.noseids b/.noseids new file mode 100644 index 0000000000000000000000000000000000000000..45b1f059b3d9ef5f78180a38b1aff036f1b10684 GIT binary patch literal 16537 zcmb`OXJFJu7Jy9&B!oZ;y_()_2q7c{LMR~#6GA8B;rp zd+(R)z4zXG@BJ><@7^1&<+UyCuJQZ98rzb-Z=T-t@u^YAp=D`qR7*>XT?k;raTvK& zjL2=n@7v2>#cKD>P}=#!?Y{Int$Y}?;}1(_#aWfNdjH!ltspdgyFJ9Of&Mh`+cv)k zM&;(LjAA2?L-NALWQZ);17|B3FGjPm&q4NzCfP>Oc0x3BiEbgW6kn(JUvt_{Vu zMM3CQw#N6@Wwg2!4xr;O>}B`Qf_5qi;9QU!svCFreL}_GbixA=Id@v zu;Mpu6^y*+I9S(h&!wQv$SuYv_SWH=pru?7Gj(;^p>xJ6Y)c+g8$mB}Lztytwm6Gj zc20SwZcXYM6zm|hT}-dJ3R=ZDc8Pf?F%M%_SE6XU1(>g3fjFC8U|};+AOXX3Em)*r zu^7)Tu>>VnHxmVtFie!Gw((09EEDIj`z%MFHAC`|mhYH(3sxvtDJHNBtr~KGA|Klw z3)&R4i;3($tI>x#!@6l(l3wJ)8U+`MbJ-2nqQR0z1{KpcuzQH`#ayIdoj8wO<6_jH zKA>(O`ZNsNb!@Ch>lIug&d+VcJ5Q_q?esbVYH%;jO~pr~G`MsDY{<>U-|qA};wIH| z3I?^w2L95Jk!y6Bj$MGu6l@e14Bur_c|&e?El;zoZr_tJvK&?%xLmhLkw zAZB_h21l_*$qHeo0wJaipL8vf2ImIlAQj4X<4oM8;5spV__Wnaly|kEhy@EYr z#_(BtkwqPNQp0*Iy1wCR3m%vdJSk*)r+^AyV zI$Sjfw<|a%mU1~9M~BS1P!ogz4RAbkLcvL~j0=7W;WSH3O~x8{XjiOQpc;ca6x=D6 zb7|a#8Z;P8)u`!x?^bY+SiwcTw_f4?v~f8+iVn0A-8c-ifc2PyCy910_LG}r zV=GXy;3*28Dpqr`pVs8-*KAy|_i;rPp03~-VhxwVGf`oE6QPe43U;xGZIN55W@G@* zQt)hXA(zc_(1zB>8zn+)W4kU6PoAsbd15V>!Sm6end%T5xL(B!^JRE}f)|R5xEx-D z4r`l^f^W*uHXS22d9i|*h;>{hFCEhRkanpY!pjuATwKg$@Cr1b-I_)j5#X{2yi&od z#Ck6Ds}Y^qJJ+;~-Hu63!)p}0R$Rgbe;vXzyG6Y>Ww?>!!0Q#fL0rm3eIrt7Au=`E zk!vH^s18Qnq~OhB0~h!$2+o{-b>U8&j!eZy4b-){|3^VX@!DN;@H&9n<0Et!MDU! zO#E*ne_0bP0P*AHiti}+uDF^B|2@PvQwKCYu<#u?@O=e85Z5pX{163bgRpO8kpA{b zz_DETk%AwKolNkb3?cZAcLXN|KUMHEA(-$#9|At`9Xo_yDEOtgmI?nW#5bGzLDYe7 z0Di6DH)0nP|F_6*qT+kLCC$*20sKzE@5OaY^glEVeo>!!@%+GuhlYPt@F%gG3IFGY z!SBOYN5+3q@KGYNel zw;As^t=SiD=+d$>Cb>KdAEmb#Q}VoenH)#gOh(w=_`ZIL9?G+|(|B^ak;Um8d{n1~ z(n-lwsNvhbMTbx11Wlhv^c)NQ+=Na$f`jQLi3f%;@$+SQo@Sp6 zyVz8;Lpe#qClmZ;7Wk9|PNy|e;JEN8r)ugnqTa$not{wX@J0$XJ`pQtXz)w|-^v1> zm4Inrl?gV=g<{{rhn%f7=8(og7LB=y1}(v7Yxt4048JEVd#-HN67xvn5R1h8L?XS$ z$++ShW)zm?0!?2?bc2PyD52B&ufZV*@}9KhVvSuw*xOFO)y2cjQa5}{@j+P~yuL^- zqbsN$?xTBZ8olaKXvgZZoIozuUMtAUWbj&v59-wDNh#=p^nAHW6WfT`!9Z+Jh_tXV zu&{V=sxSSkhvMaGZLo$6@(czSCI(ao>KjxSnB-cma1kjK7!=ke3bg&ulmZ?Yuh252 zyjZKOCl!l9<&s2YUWQ81lX;|9TZ2nAegol)4EW0uJe@il7zxKb+GE5^(Q=~(Zz6Ds z0epD^rg8TmFdjZLqJAT4tbbNvLU6-N(O!FiV!9|KLDw)#z=6wi(dd6Eux9 z1{N6q6r^Z(WE!)2gdA{TiYlW}xp(=&2z` zdU7~W3XQ&&&<+E7SAwP~^`MZ~qT^$IdND~}r!97qMTNoQ`XN~Ko&S(~w8dVsa2YK2 zB^ERQsviq&5tySqC1qPq4&e& z&D!D?vH%8)TN4W!3a7o0xEVUAv4;pN8L&ozP3wK>@O1p=RyzD~t0ZsJ3MMH83<@2I z0?q!@N*?!h25qazyoMJD9x}kK1fG?GHSoAqz-=X2)D|VOh!`x&iA7eU7~r92!^ID2 zYjh`}yBN@i6Ev-J4vc<49Z+A3Y?y&jDC)fy>1dA%c^qNza1#&O_iw@jr)JXA4n8?_ zGdMuvKr^9C2a6g+T!*4%sDRi#4D2vrw`Q}k0$>}6uRunc-bM7I4D=%joi@7COK#&P zcPYZgulGnCrDGtwwMh?|+|FQfG%=z5&U6z37pjLL>3c04NAaxaB_!sxRV@CFgV}Rw+(!zh7!>YL6lg^_J^!>q#R^Rts>%nnz=QZ1<;LJWMH6>m q3-TfS!9n>je&)vGPXjN)d7Ey+1Nn&l-J|q7{MXVZAHy$Z)b@XX_!Oi7 literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..bad269e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM conda/miniconda3 + +ADD . /app +RUN apt-get update -y && \ + apt-get install -y python-dev build-essential +RUN conda install -y conda=4.3 && \ + conda update -y python +RUN conda install -y --file /app/conda-requirements.txt && \ + conda clean --all +RUN pip install --no-cache-dir -q -r /app/requirements.txt +RUN mkdir -p ~/.jupyter && \ + cp /app/omegaml/notebook/jupyter/*py ~/.jupyter && \ + cd /app && pip install . +CMD ["jupyter", '--config-dir', '/app/.jupyter'] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b0de4629 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..cdc3d73f --- /dev/null +++ b/NOTICE @@ -0,0 +1,73 @@ +Copyright 2014-2018 one2seven GmbH, Zurich +Copyright 2014-2018 Patrick Senti, Zurich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License +as per the included LICENSE file. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Third Party Packages are used by this Work as listed in the THIRDPARTY +file. By using this Work you agree to adhere to the respective license +of any and all third party packages. Runtime Dependencies are listed +below. + +Runtime Dependencies are not included in this Work and shall be obtained +and used by You in agreement with the respective third party in the name +of and at the risk and cost of You or your Legal Entity. + + +Runtime Dependencies: + +RabbitMQ - https://www.rabbitmq.com/ (Mozilla Public License) +MongoDB - https://www.mongodb.com/ (Server Side Public License, GNU AGPL v3.0) +Docker - https://www.docker.com/ (Apache 2.0 license) [1] + +[1] is recommended, not required + + +Software & Hardware requirements: + +* Single node / single user deployment + + CPU: at least dual core 64-bit 2.4 GHz + RAM: at least 8GB, 16GB recommended + Disk: at least 100GB free disk space, SSD recommended + Operating System: Linux Ubuntu, Mint, Debian + +* Multi node / multi user + + Effectively operating a cluster of a scalable data science + platform is an engineering challenge. We have solved this + challenge and provide the omega|ml enterprise edition for + this purpose. Request more information at https://omegaml.io + or by sending an email to info@omegaml.io + + for every worker node + CPU: at least dual core 64-bit 2.4 GHz + RAM: at least 8GB, 16GB recommended + Disk: at least 100GB free disk space, SSD recommended + Operating System: Linux Ubuntu, Mint, Debian + + Windows is not currently supported - if you have a Windows requirements + please let us know at info@omegaml.io + + for every MongoDB node + CPU: at least dual core 64-bit 2.4 GHz + RAM: at least 16GB, 32GB recommended + Disk: at least 500GB free disk space, SSD 1TB free disk space recommended + Operating System: Linux Ubuntu, Mint, Debian (recommended) + + see https://docs.mongodb.com/manual/administration/production-notes/#hardware-considerations + + for every RabbitMQ node + CPU: at least dual core 64-bit 2.4 GHz + RAM: at least 4GB + Disk: at least 10GB free disk space + Operating System: Linux Ubuntu, Mint, Debian (recommended) + + see https://www.rabbitmq.com/production-checklist.html diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..cc88133e --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +worker: celery worker --app omegaml.celeryapp -E -B --loglevel=DEBUG +notebook: jupyter notebook --allow-root --ip 0.0.0.0 --no-browser +restapi: python -m omegaml.restapi diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..35f8c665 --- /dev/null +++ b/README.rst @@ -0,0 +1,187 @@ +What is it? +=========== + +*omega|ml is the production and integration platform for data science +that scales from laptop to teams to enterprise. Batteries included.* + +Documentation: https://omegaml.github.io/omegaml/ +Commercial License: https://omegaml.io + + +Features +======== + +*Community Edition* + +omega|ml is a great choice if you want to + +- get a fully integrated data science workplace within minutes [1] +- easily share models, data, jupyter notebooks and reports with your collaborators +- perform out-of-core computations on a pure-python or Apache Spark compute cluster [2] +- have a shared NoSQL database, out of the box, that behaves like a Pandas dataframe [3] +- deploy models to production with a single line of code +- serve and use models or datasets from a REST API +- use a compute cluster to train your models with no additional setup +- scale your data science work from your laptop to team to production with no code changes +- integrate any machine learning framework or third party data science platform with a common API + +[1] supporting scikit-learn, Spark MLLib out of the box, Keras and +Tensorflow available shortly. Note the Spark integration is currently only available with +the enterprise edition. +[2] using Celery, Dask Distributed or Spark +[3] leveraging MongoDB's excellent aggregation framework + +In addition omega|ml provides an easy-to-use extensions API to support any kind of models, +compute cluster, database and data source. + +*Enterprise Edition* + +omega|ml enterprise provides security on every level and is ready made for Kubernetes +deployment. It is licensed separately for on-premise, private or hybrid cloud. +Sign up at https://omegaml.io + + +Get started +=========== + +.. code:: + + $ wget https://github.com/omegaml/omegaml/blob/master/docker-compose.yml + $ docker-compose up -d + +Next open your browser at http://localhost:8899 to open Jupyter Notebook. Any notebook +you create will automatically be stored within the omega|ml database, thus making it +easy to work with colleagues. The REST API is available at http://localhost:5000. + + +Examples +======== + +Get more information at https://omegaml.github.io/omegaml/ + +.. code:: + + # transparently store Pandas Series and DataFrames or any Python object + om.datasets.put(df, 'stats') + om.datasets.get('stats', sales__gte=100) + + # transparently store and get models + clf = LogisticRegression() + om.models.put(clf, 'forecast') + clf = om.models.get('forecast') + + # run and scale models directly on the integrated Python or Spark compute cluster + om.runtime.model('forecast').fit('stats[^sales]', 'stats[sales]') + om.runtime.model('forecast').predict('stats') + om.runtime.model('forecast').gridsearch(X, Y) + + # use the REST API to store and retrieve data, run predictions + requests.put('/v1/dataset/stats', json={...}) + requests.get('/v1/dataset/stats?sales__gte=100') + requests.put('/v1/model/forecast', json={...}) + + +FAQ +=== + +* How does omega|ml relate to other frameworks such as mlflow or kubeflow + + omega|ml can readily integrate with mlflow and kubeflow by using the + extensions API. We are happy to accept any contributions or work with + you if you have a need. + + mlflow is a generic framework to package and integrate machine learning + models as a software asset. kubeflow provides the tooling for kuberentes + deployment mainly for Tensorflow although extensions are planned. Neither + package provides a standardized, out-of-the-box data science platform + comparable to omega|ml. + +* When should I choose omega|ml over other frameworks? + + You should choose omega|ml if you want to start working right now - + omega|ml gets you from no data science to full-scale within just a + few minutes and using a single command (we also provide a hosted + service at https://omegaml.io). + + $ docker-compose up + # access your notebook at http://localhost:8899 + + omega|ml is a great choice if you want your team of data scientists + and application developers to start to collaborate efficiently and leverage + your investment in the PyData stack without being side tracked by the + high complexity of hard to integrate third party storage and compute + infrastructure. + + As an enterprise organization omega|ml provides a secure, managed central + place for all your data scientist to collaborate and provide models to + production applications. omega|ml thanks to its open scalable architecture + works either on premise, in your private cloud or as a hybrid cloud + leveraging all your compute and storage capabilities across data centers + and geographies. + + However you do not need to limit yourself from using mlflow or kubeflow + or any other framework as omega|ml by design can be readily integrated + with almost any third party systems. + +* Is it possible to use an existing data lake or datawarehouse in SQL or NoSQL + database? How can omega|ml access data stored in an object storage or + distributed filed system? + + Absolutely. omega|ml's extensions API provides a straight forward way + to implement access to any storage. In particular, implement a data backend + as follows. Please consider contributing your implementation. + + # register this class in defaults.OMEGA_STORE_BACKENDS + class MyStorageEngine(BaseDataBackend): + def supports(self, obj, name, **kwargs): + return True #if obj can be stored + + def put(self, obj, name, **kwargs): + # your code to store data + return Metadata(...) + + def get(self, name, **kwargs): + # your code to retrieve data + return data + +* Is it possible to use a NoSQL or SQL database completely replacing MongoDB? + + The short answer is yes, the extensions API enable any storage backend + to be contributed transparently. However there are some caveats in terms of + performance and scalability if you do so: + + omega|ml has been designed for high scalability from the ground up. Every + component (API, notebooks, storage, compute, message broker) can be scaled + independently and according to the specific needs. MongoDB follows this + scalability approach by providing out of the box replication and sharding + that enables data locality in every omega|ml compute node, if required. + + In summary while principally supported, this scalability feature is not + easily achieved with every other database. + +* Is omega|ml open source software? Is it free of charge? + + Yes and yes! + + omega|ml is Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License + as per the included LICENSE file. + + omega|ml is also available as an enterprise edition with full multi-user + security, user-separated notebooks, a notebook publishing and reporting API + and readily packaged for Kubernetes deployments. + +* I need security features and multi-user support. + + The database can easily be protected, see scripts/mongoinit.js. Then add + `command: --auth` to the mongodb section in docker-compose.yml and + amend the OMEGA_MONGO_URL variable. The community edition does not currently + offer security beyond the database. + + Your fastest option to get state of the art security and multi-user features + is to use our commercial license. It provides a multi-user security layer for + all parts of the software, including the database, the REST API, the Jupyter + Notebooks and all docker containers. It also provides additional deployment + options such as Spark clusters or easy use of GPUs, deployed on Kubernetes in + a private or hybrid cloud. We also offer additional support options in order + to meet your specific requirements. diff --git a/THIRDPARTY b/THIRDPARTY new file mode 100644 index 00000000..d04ce8fc --- /dev/null +++ b/THIRDPARTY @@ -0,0 +1,154 @@ + Name Version License URL + Fabric 1.14.0 UNKNOWN http://fabfile.org + Flask 1.0.2 BSD https://www.palletsprojects.com/p/flask/ + Flask-Testing 0.7.1 BSD https://github.com/jarus/flask-testing + Jinja2 2.10 BSD http://jinja.pocoo.org/ + Mako 1.0.7 MIT http://www.makotemplates.org/ + MarkupSafe 1.0 BSD http://github.com/pallets/markupsafe + PyJWT 1.6.4 MIT http://github.com/jpadilla/pyjwt + PyNaCl 1.2.1 Apache License 2.0 https://github.com/pyca/pynacl/ + PySocks 1.6.8 BSD https://github.com/Anorov/PySocks + PyYAML 3.13 MIT http://pyyaml.org/wiki/PyYAML + Pygments 2.2.0 BSD License http://pygments.org/ + SQLAlchemy 1.2.12 MIT License http://www.sqlalchemy.org + Unidecode 1.0.22 GPL UNKNOWN + Werkzeug 0.14.1 BSD https://www.palletsprojects.org/p/werkzeug/ + alembic 0.9.9 MIT http://bitbucket.org/zzzeek/alembic + amqp 1.4.9 LGPL http://github.com/celery/py-amqp + aniso8601 4.0.1 UNKNOWN https://bitbucket.org/nielsenb/aniso8601 + anyjson 0.3.3 BSD http://bitbucket.org/runeh/anyjson/ + argh 0.26.2 GNU Lesser General Public License (LGPL), Version 3 http://github.com/neithere/argh/ + asn1crypto 0.24.0 MIT https://github.com/wbond/asn1crypto + async-generator 1.10 MIT -or- Apache License 2.0 https://github.com/python-trio/async_generator + backcall 0.1.0 BSD https://github.com/takluyver/backcall + bcrypt 3.1.4 Apache License, Version 2.0 https://github.com/pyca/bcrypt/ + billiard 3.3.0.23 BSD http://github.com/celery/billiard + bleach 2.1.3 Apache Software License http://github.com/mozilla/bleach + bokeh 0.13.0 New BSD http://github.com/bokeh/bokeh + boto3 1.7.74 Apache License 2.0 https://github.com/boto/boto3 + botocore 1.10.74 Apache License 2.0 https://github.com/boto/botocore + braintree 3.47.0 MIT https://developers.braintreepayments.com/python/sdk/server/overview + callable-pip 1.0.0 UNKNOWN https://github.com/sirosen/callable-pip + celery 3.1.23 BSD http://celeryproject.org + certifi 2018.10.15 MPL-2.0 http://certifi.io/ + cffi 1.11.5 MIT http://cffi.readthedocs.org + chardet 3.0.4 LGPL https://github.com/chardet/chardet + click 6.7 UNKNOWN http://github.com/mitsuhiko/click + cloudpickle 0.5.3 BSD 3-Clause License https://github.com/cloudpipe/cloudpickle + coverage 4.5.1 Apache 2.0 https://bitbucket.org/ned/coveragepy + croniter 0.3.12 MIT License http://github.com/kiorky/croniter + cryptography 2.3.1 BSD or Apache License, Version 2.0 https://github.com/pyca/cryptography + cryptography-vectors 2.3.1 BSD or Apache License, Version 2.0 https://github.com/pyca/cryptography + dask 0.15.2 BSD http://github.com/dask/dask/ + decorator 4.3.0 new BSD License https://github.com/micheles/decorator + defusedxml 0.5.0 PSFL https://github.com/tiran/defusedxml + dill 0.2.6 3-clause BSD http://www.cacr.caltech.edu/~mmckerns/dill.htm + distributed 1.18.1 BSD https://distributed.readthedocs.io/en/latest/ + dj-database-url 0.4.2 BSD https://github.com/kennethreitz/dj-database-url + docutils 0.14 public domain, Python, 2-Clause BSD, GPL 3 (see COPYING.txt) http://docutils.sourceforge.net/ + entrypoints 0.2.3 UNKNOWN https://github.com/takluyver/entrypoints + flask-restplus 0.12.1 BSD-3-Clause https://github.com/noirbizarre/flask-restplus + gunicorn 19.7.1 MIT http://gunicorn.org + heapdict 1.0.0 BSD http://stutzbachenterprises.com/ + honcho 1.0.1 MIT https://github.com/nickstenning/honcho + html5lib 1.0.1 MIT License https://github.com/html5lib/html5lib-python + idna 2.7 BSD-like https://github.com/kjd/idna + ipykernel 4.8.2 BSD http://ipython.org + ipython 6.5.0 BSD https://ipython.org + ipython-genutils 0.2.0 BSD http://ipython.org + ipywidgets 7.4.0 BSD http://ipython.org + itsdangerous 1.1.0 BSD https://palletsprojects.com/p/itsdangerous/ + jedi 0.12.1 MIT https://github.com/davidhalter/jedi + jmespath 0.9.3 MIT https://github.com/jmespath/jmespath.py + joblib 0.9.4 UNKNOWN UNKNOWN + jsonfield 2.0.2 MIT https://github.com/dmkoch/django-jsonfield/ + jsonschema 2.6.0 MIT http://github.com/Julian/jsonschema + jupyter 1.0.0 BSD http://jupyter.org + jupyter-client 5.2.3 BSD https://jupyter.org + jupyter-console 5.2.0 BSD https://jupyter.org + jupyter-core 4.4.0 BSD https://jupyter.org + jupyterhub 0.8.1 BSD http://jupyter.org + kombu 3.0.37 UNKNOWN https://kombu.readthedocs.io + livereload 2.5.2 BSD https://github.com/lepture/python-livereload + locket 0.2.0 UNKNOWN http://github.com/mwilliamson/locket.py + mistune 0.8.3 BSD https://github.com/lepture/mistune + mock 2.0.0 UNKNOWN https://github.com/testing-cabal/mock + mongoengine 0.10.6 MIT http://mongoengine.org/ + msgpack 0.5.6 Apache 2.0 http://msgpack.org/ + msgpack-python 0.5.6 Apache 2.0 http://msgpack.org/ + mysqlclient 1.3.12 GPL https://github.com/PyMySQL/mysqlclient-python + nbconvert 5.3.1 BSD http://jupyter.org + nbformat 4.4.0 BSD http://jupyter.org + nose 1.3.7 GNU LGPL http://readthedocs.org/docs/nose/ + nosexcover 1.0.11 BSD http://github.com/cmheisel/nose-xcover/ + notebook 5.0.0 BSD http://jupyter.org + numexpr 2.6.6 MIT https://github.com/pydata/numexpr + numpy 1.13.1 BSD http://www.numpy.org + oauthlib 2.1.0 BSD https://github.com/oauthlib/oauthlib + omegaml-ce 0.1 commercial http://www.shrebo.com/ + ordered-set 3.0.1 MIT-LICENSE http://github.com/LuminosoInsight/ordered-set + packaging 17.1 BSD or Apache License, Version 2.0 https://github.com/pypa/packaging + pamela 0.3.0 MIT http://github.com/minrk/pamela + pandas 0.20.3 BSD http://pandas.pydata.org + pandocfilters 1.4.2 UNKNOWN http://github.com/jgm/pandocfilters + paramiko 2.4.1 LGPL https://github.com/paramiko/paramiko/ + parso 0.3.1 MIT https://github.com/davidhalter/parso + partd 0.3.8 BSD http://github.com/dask/partd/ + pathtools 0.1.2 MIT License http://github.com/gorakhargosh/pathtools + pbr 4.2.0 UNKNOWN https://docs.openstack.org/pbr/latest/ + pexpect 4.6.0 ISC license https://pexpect.readthedocs.io/ + pickleshare 0.7.4 MIT https://github.com/pickleshare/pickleshare + port-for 0.3.1 MIT license https://github.com/kmike/port-for/ + progressbar2 3.38.0 BSD https://github.com/WoLpH/python-progressbar + prometheus-client 0.4.2 Apache Software License 2.0 https://github.com/prometheus/client_python + prompt-toolkit 1.0.15 UNKNOWN https://github.com/jonathanslenders/python-prompt-toolkit + psutil 5.4.6 BSD https://github.com/giampaolo/psutil + ptyprocess 0.6.0 UNKNOWN https://github.com/pexpect/ptyprocess + pyOpenSSL 18.0.0 Apache License, Version 2.0 https://pyopenssl.org/ + pyaes 1.6.1 License :: OSI Approved :: MIT License https://github.com/ricmoo/pyaes + pyasn1 0.4.4 BSD https://github.com/etingof/pyasn1 + pycparser 2.19 BSD https://github.com/eliben/pycparser + pycurl 7.43.0.2 LGPL/MIT http://pycurl.io/ + pyminifier 2.1 Proprietary https://github.com/liftoff/pyminifier + pymongo 3.5.1 Apache License, Version 2.0 http://github.com/mongodb/mongo-python-driver + pyparsing 2.2.0 MIT License http://pyparsing.wikispaces.com/ + python-dateutil 2.7.3 Dual License https://dateutil.readthedocs.io + python-editor 1.0.3 Apache https://github.com/fmoo/python-editor + python-mimeparse 1.6.0 UNKNOWN https://github.com/dbtsai/python-mimeparse + python-oauth2 1.0.1 UNKNOWN https://github.com/wndhydrnt/python-oauth2 + python-utils 2.3.0 BSD https://github.com/WoLpH/python-utils + python3-openid 3.1.0 UNKNOWN http://github.com/necaris/python3-openid + pytz 2018.5 MIT http://pythonhosted.org/pytz + pyzmq 17.1.0 LGPL+BSD https://pyzmq.readthedocs.org + qtconsole 4.3.1 BSD http://jupyter.org + requests 2.20.0 Apache 2.0 http://python-requests.org + requests-oauthlib 1.0.0 ISC https://github.com/requests/requests-oauthlib + runipy 0.1.5 BSD 2-Clause https://github.com/paulgb/runipy + s3fs 0.1.3 BSD http://github.com/dask/s3fs/ + s3transfer 0.1.13 Apache License 2.0 https://github.com/boto/s3transfer + scikit-learn 0.19.0 new BSD http://scikit-learn.org + scipy 0.19.1 BSD https://www.scipy.org + simplegeneric 0.8.1 ZPL 2.1 http://cheeseshop.python.org/pypi/simplegeneric + six 1.11.0 MIT http://pypi.python.org/pypi/six/ + sortedcontainers 2.0.4 Apache 2.0 http://www.grantjenks.com/docs/sortedcontainers/ + sphinx-autobuild 0.6.0 MIT https://github.com/GaretJax/sphinx-autobuild + sqlparse 0.2.4 BSD https://github.com/andialbrecht/sqlparse + stripe 2.4.0 MIT https://github.com/stripe/stripe-python + suds-jurko 0.6 (specified using classifiers) http://bitbucket.org/jurko/suds + tables 3.4.2 BSD 2-Clause http://www.pytables.org/ + tblib 1.3.2 BSD https://github.com/ionelmc/python-tblib + tee 0.0.2 MIT License https://github.com/algrebe/python-tee + terminado 0.8.1 UNKNOWN https://github.com/jupyter/terminado + testpath 0.3.1 UNKNOWN https://github.com/jupyter/testpath + toolz 0.9.0 BSD http://github.com/pytoolz/toolz/ + tornado 4.5.3 http://www.apache.org/licenses/LICENSE-2.0 http://www.tornadoweb.org/ + traitlets 4.3.2 BSD http://ipython.org + urllib3 1.23 MIT https://urllib3.readthedocs.io/ + utf8forgood 0.1.4 BSD UNKNOWN + watchdog 0.8.3 Apache License 2.0 http://github.com/gorakhargosh/watchdog + wcwidth 0.1.7 MIT https://github.com/jquast/wcwidth + webencodings 0.5.1 BSD https://github.com/SimonSapin/python-webencodings + whitenoise 3.3.1 MIT http://whitenoise.evans.io + widgetsnbextension 3.4.0 BSD http://ipython.org + xmltodict 0.11.0 MIT https://github.com/martinblech/xmltodict + zict 0.1.3 BSD http://zict.readthedocs.io/en/latest/ diff --git a/conda-requirements.txt b/conda-requirements.txt new file mode 100644 index 00000000..cbb677ef --- /dev/null +++ b/conda-requirements.txt @@ -0,0 +1,13 @@ +# conda-requirements.txt +scipy=0.19.1 +numpy=1.13.1 +pandas=0.20.3 +scikit-learn=0.19.0 +jupyter=1.0.0 +pytables=3.4.2 +nose=1.3.7 +nosexcover=1.0.11 +distributed=1.18.1 +dask=0.15.2 +notebook=5.0.0 +tornado=4.5.3 diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000..26e37d78 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,12 @@ +version: '3' +services: + mongo: + image: mongo:3.6.8-stretch + ports: + - "27019:27017" + - "27020:28017" + command: ["--auth"] + rabbitmq: + image: rabbitmq + ports: + - "5672:5672" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..8f1eaffb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3' +services: + omegaml: + build: . + image: omegaml:latest + hostname: omegaml + ports: + - "8899:8888" + - "5000:5000" + links: + - rabbitmq + - mongodb + working_dir: /app + command: honcho start notebook restapi + environment: + - APP=omegaml + - OMEGA_MONGO_URL=mongodb://mongodb/omega + - OMEGA_BROKER=amqp://rabbitmq:5672// + - JUPYTER_PASSWORD=sha1:24fa20fec60f:c7cd7e46afa507d484c59abeadbefa05022583b8 + worker: + image: omegaml:latest + hostname: worker + links: + - rabbitmq + - mongodb + working_dir: /app + command: honcho start worker + environment: + - OMEGA_MONGO_URL=mongodb://mongodb/omega + - OMEGA_BROKER=amqp://rabbitmq:5672// + - C_FORCE_ROOT=yes + rabbitmq: + image: rabbitmq + hostname: rabbitmq + mongodb: + image: mongo:3.6.8-stretch + hostname: mongodb diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..47e991c1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,250 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/omegaml.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/omegaml.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/omegaml" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/omegaml" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echofind . -name "*rst" | xargs sed 's/\ omegaml\ /\ omega\|ml\ /' + @echo "Build finished. Dummy builder generates no files." + +# https://pypi.python.org/pypi/sphinx-autobuild +livehtml: + sphinx-autobuild -p 8001 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + +# fix brand naming +fix: + find . -name "*rst" | xargs sed -i 's/ omegaml / omega\|ml /' + find . -name "*rst" | xargs sed -i 's/OmegaML/omega\|ml/' + find . -name "*rst" | xargs sed -i 's/^omegaml /omega\|ml /' + find . -name "*rst" | xargs sed -i 's/omegaml$$/omega\|ml/' + find . -name "*rst" | xargs sed -i 's/import omega.ml as om/import omegaml as om/' + find . -name "*rst" | xargs sed -i "s/from omega.ml import /from omegaml import /" + +publish: + make clean html + cd build/html && tar -czf /tmp/omegamldocs.tgz . + git checkout gh-pages + cd ../ && find . -name "*pyc" | xargs rm + cd ../ && git rm -rf . && tar -xzf /tmp/omegamldocs.tgz && touch .nojekyll && git add . && git commit -m "updates ghpages" + echo "Done. git push and go back to your original branch" + + + + diff --git a/docs/README b/docs/README new file mode 100644 index 00000000..55661bec --- /dev/null +++ b/docs/README @@ -0,0 +1,18 @@ +This sphinx project is setup to work as follows: + +1. change to the virtualenv for omegaml +2. cd docs +3. make livehtml + +livehtml will monitor the source folder and automatically +rebuild the html pages whenever a new file is added or an +existing file changes. + +So to edit and browse the results: + +4. edit files in source/*.rst +5. open localhost:8000 + + + + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..24a28018 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +Sphinx==1.4.8 +sphinx-autobuild==0.6.0 + +sphinx-rtd-theme==0.2.4 +sphinx-autobuild==0.6.0 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 00000000..211c2f22 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,6 @@ +img.om-box { + border-color: gray; + border-width: 1px; + border-style: solid; +} + diff --git a/docs/source/admin/configuration.rst b/docs/source/admin/configuration.rst new file mode 100644 index 00000000..9f1d9d0e --- /dev/null +++ b/docs/source/admin/configuration.rst @@ -0,0 +1,112 @@ +Configuration +============= + +.. contents:: + +Configuration hooks +------------------- + +omega|ml is configured by constants defined in the :code:`omegaml.defaults` module. +Since this module is usually not directly changeable by the user or +administrator, the following hooks are provided: + +configuration file (:code:`$HOME/.omegaml/config.yml`) +++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + The configuration file in Yaml format is read automatically on omega|ml + startup, if available. + + .. note:: `Enterprise Edition` + A user-specific configuration file can be obtained by + any authorized user by running the :code:`python -m omegacli init` command. + To override defaults, specify the corresponding variable in Yaml format. + +system environment variables +++++++++++++++++++++++++++++ + + To change a particular default, set the environment variable of the same + name as the respective configuration parameter. As an example, to change + the :code:`OMEGA_MONGO_URL` parameter, set the :code:`OMEGA_MONGO_URL` + environment variable. Note to change an entry in :code:`OMEGA_CELERY_CONFIG`, + add the :code:`OMEGA_CELERY_` prefix to the environment variable name. As + an example, to change :code:`OMEGA_CELERY[BROKER_URL]` use the env var + :code:`OMEGA_CELERY_BROKER_URL`. + +in-code update +++++++++++++++ + + If you integrate omega|ml into your application, the + :code:`omegaml.defaults.update_from_obj` provides a direct way to update + the defaults from any object. The object needs to support :code:`getattr` + on the corresponding defaults parameter. + +The hooks are applied in the above order, that is the defaults are overriden +by configuration file, then operating system environment variables, then +constance parameters and finally your own code. Note that parameter values +specified in constance cannote be overridden by our own code except by changing +the value in the constance Django table directly. + + +Basic configuration +-------------------- + +.. autodata:: omegaml.defaults.OMEGA_TMP + :annotation: + + Defaults to :code:`/tmp` + +.. autodata:: omegaml.defaults.OMEGA_MONGO_URL + :annotation: + + Format :code:`mongodb://user:password@host:port/database` + +.. autodata:: omegaml.defaults.OMEGA_MONGO_COLLECTION + + +Storage configuration +--------------------- + +.. autodata:: omegaml.defaults.OMEGA_STORE_BACKENDS + + Dictionary of pairs :code:`{ 'kind': class }`, where + *kind* is the Metadata.kind of the stored object, and + class is the python loadable name of the class that implements + handling of this kind. The storage backends listed in this variable are + automatically loaded. + +.. autodata:: omegaml.defaults.OMEGA_STORE_MIXINS + + List of storage mixin classes. The mixins listed here are automatically + applied to each :code:`OmegaStore` instance. + + +Celery Cluster configuration +---------------------------- + +.. autodata:: omegaml.defaults.OMEGA_BROKER + +.. autodata:: omegaml.defaults.OMEGA_RESULT_BACKEND + +.. autodata:: omegaml.defaults.OMEGA_CELERY_CONFIG + + This is used by omemgal to configure the celery application. Note + that the configuration must be the same for both client and cluster + worker. + + +Client-side configuration (constance) +------------------------------------- + +These parameters are in the admin UI at +http://localhost:5000/admin/constance/config: + +* :code:`BROKER_URL` - this is the rabbitmq broker used by the Celery cluster. + Set as :code:`ampq://public-omegaml-hostname:port//`. + Set vhost depending on your rabbitmq configuration. By default the vhost + is an empty string + +* :code:`MONGO_HOST` - set as :code:`public-mongodb-hostname:port` + +* :code:`CELERY_ALWAYS_EAGER` - if this :code:`True`, all calls to the + runtime are in fact executed locally on the calling machine. Note this + also means that the REST API will not submit any tasks to the cluster. \ No newline at end of file diff --git a/docs/source/admin/design.rst b/docs/source/admin/design.rst new file mode 100644 index 00000000..c3551394 --- /dev/null +++ b/docs/source/admin/design.rst @@ -0,0 +1,46 @@ +Design +====== + +The runtime implements remote execution of models and jobs. The runtime +is implemented as follows: + +* :code:`OmegaRuntime` - the client API to get access to a remote model +* :code:`OmegaModelProxy` - the client API to the remote model +* :code:`runtime.tasks` - the celery tasks implementing the actual execution + +A :code:`OmegaRuntime` instance is available as :code:`om.runtime`: + +.. code:: python + + om = Omega() + # get the OmegaModelProxy instance + model = om.runtime.model('mymodel') + # call methods on OmegaModelProxy, effecting remote task execution + model.fit('X', 'Y') + pred = model.predict('X') + +Note that any method called on the :code:`model` are translated into calls +to respective celery tasks. A celery task lives in a celery worker at a remote +note. On execution a task will re-create the :code:`Omega` instance to retrieve +the *X,Y* data as well as the actual model. + + +Security concerns +----------------- + +Note that the open source `omega|ml Core` does not implement any security by default. +The omega|ml Enterprise Edition however addresses all security concerns: + +* **user authentication**. Users authenticate to REST endpoints by username + + Apikey. Communication is protected by HTTPS. + +* **the database** is protected by user/passwords. There is an admin database + which uses the :code:`MONGO_ADMIN_URL`. This URL is not exposed to + users. The per-user databases are only exposed on a per-user basis. + +* **communication to the database** using mongo TSL support + +* **communication to the message broker** access protection to RabbitMQ, + channel encryption (TLS) and message signing. + +* **configuration of client workstations**. via userid and apikey diff --git a/docs/source/admin/index.rst b/docs/source/admin/index.rst new file mode 100644 index 00000000..2b1f800e --- /dev/null +++ b/docs/source/admin/index.rst @@ -0,0 +1,13 @@ +Administration +============== + +.. toctree:: + :maxdepth: 2 + + + kickstart + configuration + spark + putget + design + \ No newline at end of file diff --git a/docs/source/admin/kickstart.rst b/docs/source/admin/kickstart.rst new file mode 100644 index 00000000..ffa2ea39 --- /dev/null +++ b/docs/source/admin/kickstart.rst @@ -0,0 +1,78 @@ +Getting Started with omega|ml +============================= + +omega|ml is the data science integration platform that consists of a compute cluster, +a highly scalable distributed NoSQL database and a REST API. The omega|ml Enterprise Edition +in addition provides a web dashboard, user profile management and security features. + +omega|ml enables data scientists to offload all the heavy-lifting involved +with machine learning and analytics workflows, while enabling third-party apps +to use machine learning models in production the REST API. + + +Single node deployment +---------------------- + +In command line mode run + +.. code:: + + $ docker run mongodb + $ pip install omegaml + $ python -m omegaml.webapi + $ curl http://localhost/v1/api/ping + +From any Python prompt + +.. code:: + + [1] import pandas as pd + import omegaml as om + + df = pd.DataFrame(...) + om.datasets.put(df, 'stats') + df2 = om.datasets.get('stats') + ... + +Multi node deployment +--------------------- + +In addition to the above also run + +.. code:: + + $ docker run rabbitmq + $ celery worker --app omegaml.celeryapp -E -B --loglevel=debug + +This will start a celery compute cluster that connects to the RabbitMQ instance as per default settings. + + + +Client Configuration +-------------------- + +omega|ml supports two types of clients: + +1. Data Science workstation - a local workstation / PC / laptop with a + full-scale data science setup, ready for a Data Scientist to work locally. + When ready she will deploy data and models onto the runtime (the omega|ml + compute and data cluster), run models and jobs on the cluster or provide + datasets for access by her colleagues. This configuration requires a + local installation of omegaml, including machine learning libraries and + client-side distribution components. + +2. Application clients - some third-party application that access omega|ml + datasets, models or jobs using omegaml's REST API. This configuration + has no specific requirements other than access to the REST API and the + ability to send and receive JSON documents via HTTP. + +.. note:: + + The Data Science workstation directly connects to RabbitMQ and MongoDB. + The Enterprise Edition comes with respective security built in, including + user management, secured RabbitMQ channels and per-user MongoDB instances. + + If you have security needs you should subscribe to Enterprise Edition to + avoid the cost of managing this complexity. + + diff --git a/docs/source/admin/putget.rst b/docs/source/admin/putget.rst new file mode 100644 index 00000000..40b10b7c --- /dev/null +++ b/docs/source/admin/putget.rst @@ -0,0 +1,34 @@ +Interface with omega|ml to get/put models and objects. +===================================================== + +The below snippet displays how omega|ml can be used to store and retrieve +models and objects. + +:: + + from omegaml import Omega + om = Omega() + x = np.array(range(10, 20)) + y = x * 2 + df = pd.DataFrame(dict(x=x, y=y)) + + # store dataset object + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + + # fit locally and store model for comparison + lr = LinearRegression() + lr.fit(X, Y) + pred = lr.predict(X) + # store the fitted model + om.models.put(lr, 'duplicate') + + # for spark models + # create and store spark KMeans model + # for the below to work + # 'pyspark.mllib.clustering.KMeans' must be a working model provided by spark + # further required parameters can be sent to the model for processing using params + om.models.put('pyspark.mllib.clustering.KMeans', 'sparktest', params=dict(k=10)) + + # retrieve dataset + datax = om.datasets.get('datax') diff --git a/docs/source/admin/spark.rst b/docs/source/admin/spark.rst new file mode 100644 index 00000000..3097958f --- /dev/null +++ b/docs/source/admin/spark.rst @@ -0,0 +1,9 @@ +Using omega|ml on private Spark Clusters +--------------------------------------- + +omega|ml can be installed on any machine of your preference following below steps: + +* install omega|ml using pip +* set env variables for mongodb and rabbitmq +* start celery worker +* import omega|ml on start of pyspark \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..9c219bd1 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,364 @@ +# -*- coding: utf-8 -*- +# +# omegaml documentation build configuration file, created by +# sphinx-quickstart on Tue Oct 11 19:28:24 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.autosummary', + #'sphinx.ext.githubpages', +] + +# https://stackoverflow.com/a/40809562 +autodoc_default_flags = ['members'] +autosummary_generate = True + +# https://github.com/snide/sphinx_rtd_theme +import sphinx_rtd_theme +html_theme = 'sphinx_rtd_theme' +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'omega|ml' +copyright = u'2014-2018, omegaml.io by one2seven GmbH' +author = u'omegaml.io' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1.0' +# The full version, including alpha/beta/rc tags. +release = u'1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +#html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = u'omegaml v1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'omegamldoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'omegaml.tex', u'omegaml Documentation', + u'omegaml.io', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'omegaml', u'omegaml Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'omegaml', u'omegaml Documentation', + author, 'omegaml', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False + +import sys +import os +sys.path.insert(0, os.path.join(os.getcwd(), '..', '..')) +print(sys.path) +import omegaml + +def setup(app): + app.add_stylesheet('custom.css') + \ No newline at end of file diff --git a/docs/source/devguide/architecture.rst b/docs/source/devguide/architecture.rst new file mode 100644 index 00000000..7d226c27 --- /dev/null +++ b/docs/source/devguide/architecture.rst @@ -0,0 +1,164 @@ +Architecture +============ + +.. contents:: + +Why omega|ml +------------ + +A typical data science workflow consists of the following core steps: + +1. acquire data & store for subsequent processes +2. clean data & publish for uses +3. train & evaluate models +4. publish models & reports +5. execute prediction using previously trained models + +In any production scenario, each step requires a scalable storage to store raw and cleaned data, models and +APIs to execute models. You will also need a compute cluster that is easy to access and provides all the +required packages. Engineering such a system from scratch is hard, takes considerable time and skills. omega|ml +provides all of this in an integrated, scalable fashion. + +omega|ml provides + +* the central storage for data and models, using MongoDB as the highly-scalable storage provider +* a client API to out-of-core data processing that follows Pandas semantics +* a client API to models that follows scikit-learn semantics, integrating scikit-learn and Apache Spark models +* an integrated compute cluster runtime to train and execute models, as well as to execute arbitrary scripts and + automatically publish reports +* a sophisticated REST API to data, models, scripts and runtime +* a user interface to access information on all of the above + +Extensibility +------------- + +With the exception of the REST API, all of the above are easily extensible using mixins. + +In addition, omemgal provides interfaces to existing compute clusters like Anaconda's Distributed and +Apache Spark. omega|ml also provides an extensible framework to add custom backends and compute clusters +through a common API. + +Thanks to extensibility at the core of the architecture, omega|ml can easily accommodate any third-party storage +or machine learning backend, or add new types of operations on data and models. + +How omega|ml works +------------------ + +* data is stored via the :code:`datasets.put` API. :code:`datasets.put` + supports native Python objects like dicts and lists, Pandas DataFrames + and Series, numpy arrays as well as externally stored files that are + accessible through http, ftp or stored on cloud services like Amazon's s3. + Other datatypes can be easily added by a custom data backend. + +* machine learning models are stored via the :code:`models.put` API. + :code:`models.put` supports scikit-learn and Spark mllib models. Other + machine learning frameworks can be easily added by a custom model backend. + +* jobs (custom python scripts in the form of Jupyter notebooks) are stored + via the :code:`jobs.put` API. + +* the runtime cluster and any other authorized user can access the data + models and jobs through the :code:`datasets.get`, :code:`models.get` and + :code:`jobs.get` methods, respectively. Using this common API any compute + job e.g. to train a model can directly access the relevant data without + the need to transfer the data to the worker instance first. + + +omega|ml is composed of the following main components: + +Core components +--------------- + +The core components provide the storage for data and models. Models can +be trained locally and stored in the cluster for prediction via the REST +API. + +* :code:`Omega` - the main API and programming interface to omega|ml +* :code:`OmegaStore` - the storage for data and models +* :code:`OmegaRuntime` - the celery runtime cluster to train and execute models and jobs + + +Enterprise Edition +------------------ + +The omega|ml Enterprise Edition provides a full integrated, enterprise-scale data science platform as a service. +It is the best match for a multi-user environment with enterprise features and an extended set of functionality. + +* :code:`security features` - security features covering all components (REST API, MongoDB, RabbitMQ etc.) +* :code:`omegajobs` - JupyterHub with per-user Notebooks +* :code:`omegapkg` - Lambda functions +* :code:`omegaweb` - the REST API, web interface and dashboard +* :code:`omegaops` - the operations API +* :code:`omegacli` - client components to the REST API +* :code:`omegastream` - Python native Streaming API +* :code:`OmegaRuntimeDask` - the dask distributed runtime cluster to execute models and jobs +* :code:`SparkBackend` - the Apache Spark mllib backend implementation +* :code:`custom mixins` - custom storage mixins to process data +* :code:`custom backends` - custom storage backends to third-party storage + + +Optional components +------------------- + +* :code:`omegaetl` - the distributed ETL engine to run ETL jobs on the cluster +* :code:`workit` - the distributed scheduler for large-scale job management + +Third-party dependencies +------------------------ + +.. _MongoDB: https://www.mongodb.com/ +.. _RabbitMQ: https://www.rabbitmq.com/ +.. _Celery: http://www.celeryproject.org/ +.. _Anaconda: https://www.anaconda.com/what-is-anaconda/ +.. _MySQL: https://dev.mysql.com/ + +omega|ml depends on the following third-party products (all open source): + +* MongoDB_ - the highly scalable NoSQL database, ideal for data science workloads +* RabbitMQ_ - the most-widely used open source message broker +* Celery_ - the efficient and highly-throughput Distributed Task Queue for Python applications +* Anaconda_ - the most popular Python Data Science Distribution +* MySQL_ - the world's most popular open source database, backed by Oracle + +Note that omegaml's license does not include the above products. However, +omega|ml provides the required docker build instructions to download, +install and configure these applications for use with omegaml. + +A number of smaller third-party components in the Python ecosystem are used +in omegaml. Refer to the LICENSES file for details. + + +Positioning omega|ml +-------------------- + +The core focus of omega|ml is to enable enterprise-grade application integration +of data science workflows, at scale. While the following products provide specific elements +for application integration and also integrate directly with omegaml, none of +them provide the simplicity of the omega|ml API and the versatility of its standard and +extensible backends. + +* *Apache Spark* - Spark is a JVM-based distributed compute cluster based on + a in-memory data layer spread across the cluster. Spark is great if you + rely on Scala or other JVM languages and have a Hadoop file system ready + to store data in a native format. While there is a Python API, PySpark, it + is relatively cumbersome and complex to use. Most importantly, Spark does + not have support for scikit-learn models nor does it provide an enterprise- + ready REST API. omega|ml provides the :code:`SparkBackend` to integate with + an existing Spark Cluster, providing the same easy API to Spark, shielding + users from the complexities and pitfalls of the PySpark API. + +* *Anaconda Dask Distributed* - Dask Distributed is Anaconda's answer to + Spark in the Python ecosystem. Fully implemented in Python, Dask Distributed + is a great additition to any Python-based data science workflow. However, + very much like Apache Spark, Dask Distributed also does not provide an + integration API nor does it provide persistent storage, neither for data + nor for models. omega|ml provides a runtime implementation that integrates + directly with a Dask Distributed cluster, making it easy and straight forward + to use its power while keeping the advantage of omegaml's API and flexibility. + +* *mlflow* - ML Flow provides tools for packaging and deploying models as a + software component. While this is a good approach if there one or a few models + in an organization, it does not solve the underlying meta data and management + issues. omega|ml provides a platform, not just model packaging. In a future + release omega|ml might make use of mlflow features for model packaging and + serve as a deployment backend. \ No newline at end of file diff --git a/docs/source/devguide/custom.rst b/docs/source/devguide/custom.rst new file mode 100644 index 00000000..c5dd5d5e --- /dev/null +++ b/docs/source/devguide/custom.rst @@ -0,0 +1,43 @@ +Custom development +------------------ + +Custom development for omega|ml is available for the following extensions: + +* storage backend & mixins - process and store new data types +* model backend - integrate other machine learning frameworks or custom algorithms +* runtime mixins & tasks - enable new tasks in the distributed compute-cluster + +Backends provide the :code:`put,get` semantics to store and retrieve objects +where as mixins provide overrides or extensions to existing implementations. +Think of a backend as the storage engine for objects a specific data type +(e.g. a Pandas Dataframe) while a mixin provide the pre- or post-processing +applied to these objects on specific method calls. + +Semantics ++++++++++ + +Technically, storage and model backends, as well as storage mixins, extend the +capability of :code:`OmegaStore`. Runtime mixins and tasks extend the +capability of :code:`OmegaRuntime`. :code:`MDataFrame` mixins extend the capability +of lazy-evaluation dataframes. + +A data backend shall adhere to the protocol established by :code:`BaseDataBackend`. +Similarly a model backend shall adhere to to the protocol established by +the :code:`BaseModelBackend`. + +Both backend types support the general storage :code:`put,get` semantics to +store and retrieve objects, respectively. Model backends in addition provide +methods for specific model actions (e.g. *fit* and *predict*), following the +semantics of scikit-learn_. + +.. note:: + + In principle a backend need not be a subclass of either of the two base + backends, however there is some default processing implemented in the base + backends :code:`__init__` methods so that sub-classing is the more practical + method. + +Mixins are objects that implement arbitrary methods for their respective target. +For example, a mixin for :code:`OmegaStore` may implement a :code:`get` method, +extending the store's default implementation. Mixins are applied to their target +the same way as a subclass would be. \ No newline at end of file diff --git a/docs/source/devguide/index.rst b/docs/source/devguide/index.rst new file mode 100644 index 00000000..a11abdad --- /dev/null +++ b/docs/source/devguide/index.rst @@ -0,0 +1,11 @@ +Development Guide +================= + +.. toctree:: + :maxdepth: 2 + + architecture + custom + metadata + storage + mixins diff --git a/docs/source/devguide/metadata.rst b/docs/source/devguide/metadata.rst new file mode 100644 index 00000000..c7fdf4cf --- /dev/null +++ b/docs/source/devguide/metadata.rst @@ -0,0 +1,52 @@ +Metadata +++++++++ + +Before we go in to the technical details of each type of backend we need +to understand how omega|ml keeps track of the objects it stores. For every +object stored in omegaml, :code:`OmegaStore` creates a `Metadata` object. + +A :code:`Metadata` object is returned to the caller on every call to +:code:`om.datasets.put`, :code:`om.models.put` and :code:`om.jobs.put`: + +.. code:: + + om.datasets.put(df, 'sample') + => + , + collection=omegaml.data_.sample.datastore,objid=None, + created=2017-09-29 16:08:51.042000,name=sample)> + +Metadata in combination with customizable backends are a key component +to omegaml's flexibility as it enables the storage of arbitrary objects. + +Each object is assigned a :code:`Metadata.kind`, a unique string that +identifies the object type. On retrieving (:code:`get`) it is this value +that provides the lookup in :code:`defaults.OMEGA_STORE_BACKENDS` for +the specific backend implementation to handle this object type. + +.. code:: + + # code snippet from OmegaStore + def get_backend(...): + ... + backend_cls = load_class(self.defaults.OMEGA_STORE_BACKENDS.get(meta.kind)) + if backend_cls: + backend = backend_cls(**kwargs) + return backend + ... + + +On storing an object (:code:`put`) it is the backend's +:code:`supports` method that identifies whether the backend can deal with +the object type. + +.. code:: + + # in some class FooDataBackend(BaseDataBackend) or FooModelBackend(BaseModelBackend) + def supports(self, obj, name, **kwargs): + # check if obj with given name and kwargs is supported + check = ... + return check \ No newline at end of file diff --git a/docs/source/devguide/mixins.rst b/docs/source/devguide/mixins.rst new file mode 100644 index 00000000..1a548b26 --- /dev/null +++ b/docs/source/devguide/mixins.rst @@ -0,0 +1,113 @@ +Mixins +------ + +Mixins come in several flavors: + +* :code:`OmegaStore` mixins, enabling data pre-/post processing +* :code:`MDataFrame` mixins, enabling custom operations on lazy-evaluation dataframes +* :code:`OmegaModelProxy` mixins, enabling tasks to run on the compute cluster +* :code:`MDataFrame, MSeries` mixins, enabling custom operations on MDataFrame and MSeries objects +* :code:`ApplyContext` mixins, enabling custom operations in :code:`apply()` contexts + + +Storage mixins +++++++++++++++ + +Storage mixins typically override the :code:`get` and :code:`put` methods +to extend the functionality of backends. + +Consider users intend to store plain-text Yaml documents, which is not +natively supported by any of the existing backends. However the default +backend supports storing Python dictionaries, so we could ask the user to +convert the Yaml documents to Pyton dictionaries first, and then use +:code:`om.datasets.put` to store the object. + +As a convenience to users, we provide this conversion in a storage mixin: + +.. code:: + + class YamlDataMixin(object): + def put(obj, name, attributes=None, **kwargs): + attributes = attributes or {} + try: + obj = yaml.loads(obj) + except: + pass # assume obj was some other valid type + else: + attributes['as_yaml'] = True + # call the default implementation + return super(YamlDataMixin, self).put(obj, name, attributes=attributes, + **kwargs) + + def get(name, **kwargs): + meta = self.metadata(name) + data = super(YamlDataMixin, self).get(name, **kwargs) + if meta.attributes.get('as_yaml'): + data = yaml.puts(obj) + return data + +To enable this mixin, call :code:`om.datasets.register_mixin`: + +.. code:: + + # on startup + om.datasets.register_mixin(YamlDataMixin) + +.. note:: + + Celery clusters require that the module providing YamlDaskMixin is available on + both the client and the worker instance. This limitation is planned + to be removed in future versions of omega|ml using ccbackend, which provides + for arbitrary functions to be executed on a celery cluster. Dask Distributed + clusters do not have this limitation. + +Runtime mixins +++++++++++++++ + +Runtime mixins provide client-side extensions to `om.runtime`, specifically +to :code:`OmegaModelProxy`. OmegalModelProxy is responsible for submitting +user-requested functions to the compute cluster. + +Consider users want to run a cross-validation procedure in some particular +way that is not supported by the default runtime. While they could use +a job (notebook) to accomplish this, we provide a runtime mixin as a +convenience. + +.. code:: + + # in crossvalidate.py + class CrossValidationMixin(object): + def cross_validate(modelName, Xname, Yname, *args, **kwargs): + # get the cross validation task + task = self.runtime.task('custom.tasks.cross_validate') + return task.delay(modelName, Xname, Yname, *args, **kwargs) + + + # in custom.tasks + def cross_validate(modelName, Xname, Yname, *args, **kwargs): + # get model and data + model = om.models.get(modelName) + xdata = om.datasets.get(Xname) + ydata = om.datasets.get(Yname) + # perform cross validation + results = ... + # + return results + + +To enable this mixin, add the class to :code:`om.defaults.OMEGA_RUNTIME_MIXINS`: + +.. code:: + + OMEGA_STORE_MIXINS = [ + 'crossvalidate.CrossValidationMixin', + ] + + +.. note:: + + Celery clusters require that the custom.tasks module is available on + both the client and the worker instance. This limitation is planned + to be removed in future versions of omega|ml using ccbackend, which provides + for arbitrary functions to be executed on a celery cluster. Dask Distributed + clusters do not have this limitation. \ No newline at end of file diff --git a/docs/source/devguide/storage.rst b/docs/source/devguide/storage.rst new file mode 100644 index 00000000..cf4e3b23 --- /dev/null +++ b/docs/source/devguide/storage.rst @@ -0,0 +1,209 @@ +Storage backends +---------------- + +.. _scikit-learn: http://scikit-learn.org + +A storage backend can support additional data types for the +:code:`datasets,models,jobs` stores. All stores share the same backends as +they use the same implementation throughout. There are two types of backends: +data backends and model backends. + +All storage backends are initialized to know their respective data and model +stores as :code:`self.data_store` and `self.model_store`, respectively. + +Accessing MongoDB ++++++++++++++++++ + +:code:`OmegaStore`, accessible from a backend implementation as +:code:`self.data_store`, provides several methods and properties to interact +with mongodb. + +* :code:`self.data_store.metadata(name)` - return the meta data object + for the given object + +* :code:`self.data_store.collection(name)` - return the mongodb Collection + instance for the given object + +* :code:`self.data_store.fs` (property) - return the mongodb GridFS instance + +* :code:`self.data_store.mongodb` (property) - return the mongodb Database + instance + +.. warning:: + + A custom backend shall not use any other means to access mongodb as doing + so may cause unexpected side-effects. + +Generating unique names ++++++++++++++++++++++++ + +To generate a unique name for an object that is compatible with MongoDB +collection and GridFS naming rules use the :code:`self.data_store.object_store_key` +method. + +.. code: + + # some class FooDataBackend(BaseDataBackend): + def put(self, obj, name, **kwargs): + name = self.data_store.object_store_key(name, ext): + # store using the name + ... + return ... + +.. note:: + + The :code:`.collection` method already uses :code:`object_store_key()` + to set the collection name for a given object. + +Choosing MongoDB GridFS or collection ++++++++++++++++++++++++++++++++++++++ + +.. _MongoDB documents: http://api.mongodb.com/python/current/tutorial.html#documents +.. _MongoDB type mapping: http://api.mongodb.com/python/current/api/bson/index.html?highlight=data%20type + +If the data type of an object is some form of iterable a MongoDB collection +may be well suited to store the data. + +.. code:: + + # some class FooDataBackend(BaseDataBackend): + def put(self, obj, name, **kwargs): + # store obj in collection + collection = self.data_store.collection(name) + collection.insert_many([dict(item) for item in obj]) + # create meta data + meta = self.data_store.make_metadata(name, 'foo.rows') + meta.collection = collection + return meta.save() + +.. note:: + + The :code:`dict(item)` call may or may not be necessary. In general for + MongoDB to be able to store the object, it must be BSON serializable. See + the tutorial on `MongoDB documents` and `MongoDB type mapping`_. + +If the data type is of binary form, a GridFS file may be the better choice. + +.. code:: + + # some class FooDataBackend(BaseDataBackend): + def put(self, obj, name, **kwargs): + # store obj in gridfs + filename = self.data_store.object_store_key(name, 'foo') + buf = BytesIO(obj) + fileid = self.data_store.fs.put(buf, filename=filename) + # create meta data + meta = self.data_store.make_metadata(name, 'foo.file') + meta.gridfile = GridFSProxy(grid_id=fileid) + return meta.save() + +.. note:: + + The above code snippets only show the :code:`put` method. Implement the + :code:`get` method to retrieve the object from the object's collection or + GridFS file, as indicated by :code:`meta.kind`. It is the responsibility of + the backend to apply whatever data conversions are necessary, i.e. + :code:`OmegaStore` does not implement any automatic conversions. + + +Storing data outside MongoDB +++++++++++++++++++++++++++++ + +:code:`OmegaStore` is oblivious to the storage location of the actual data of +an object, as long as there is a backend that handles storing (put) and +retrieval (get). In other words OmegaStore in combination with a backend +implementation can deal with arbitrary data and storage methods. + +For data stored in MongoDB, :code:`Metadata.collection` and :code:`Metadata.gridfile` provide +the necessary pointers. For data stored outside mongodb, :code:`Metadata.uri` provides +an arbitrary URI that a backend can set (on :code:`put`) and use for retrieval +(on :code:`get`). + +.. code:: + + # some class FooDataBackend(BaseDataBackend): + def put(self, obj, name, **kwargs): + # store obj in some external file system + filename = self.date_store.object_store_key(name, 'foo') + buf = BytesIO(obj) + # get instance of external file system and create file URI + # note the URI can be anything as long as your get method knows how + # to dereference + foofs = ... + fileid = foofs.put(obj, filename=filename) + uri = 'foofs://{}'.format(fileid) + # create meta data + meta = self.data_store.make_metadata(name, 'foo.file') + meta.uri = uri + return meta.save() + + def get(self, name, **kwargs): + # get metadata and URI + meta = self.data_store.metadata(name) + uri = meta.uri + # get object back using some service that understands this uri + service = ... + obj = service.get(uri) + return obj + + +Data backend +++++++++++++ + +A data backend minimally provides the :code:`put` and :code:`get` methods: + +.. code:: + + # some class FooDataBackend(BaseDataBackend): + def put(self, obj, name, **kwargs): + # code to store the object + ... + # create or update the metadata object + meta = self.data_store.metadata(name) + if meta is None: + meta = self.data_store.make_metadata(name, kind) + # always save the Metadata instance before returning + return meta.save() + + def get(self, name, **kwargs): + # code to retrieve the object + obj = ... + return obj + + +Model backend ++++++++++++++ + +.. _model persistency: http://scikit-learn.org/stable/modules/model_persistence.html + +Model backends store and retrieve instances of models (in the scikit-learn +sense of `model persistency`_). In addition, they act as the model proxy used +by :code:`OmegaRuntime` to perform arbitrary actions on an saved model using +named data objects. + +The actions expected to be available minimally to :code:`OmegaRuntime` +on a saved model are as follows. Note that these methods accept the *modelname*, +*XName*, and *Yname* parameters, which must all reference existing objects +in the `om.models` and `om.datasets` stores, respectively. + +.. note:: + + Technically, these methods are called from a worker in the compute cluster + *without* prior loading of the model nor the data. The worker uses + `om.models.get_backend()` to retrieve the model's backend, then calls + the requested method. Thus it is the responsibility of the backend to + retrieve the model and any data required. + +.. currentmodule:: omegaml.backends.basemodel + +.. automethod:: BaseModelBackend.fit +.. automethod:: BaseModelBackend.predict +.. automethod:: BaseModelBackend.transform + +This is in addition to the :code:`put` and :code:`get` methods required by +any storage backend. + +Ideally and for user convenience, more methods should be supported, +see the reference on :code:`BaseModelBackend`. Methods that are not supported +will raise the :code:`NotImplemented` exception. + diff --git a/docs/source/guide/custombackends.rst b/docs/source/guide/custombackends.rst new file mode 100644 index 00000000..bf00d204 --- /dev/null +++ b/docs/source/guide/custombackends.rst @@ -0,0 +1,19 @@ +Implementing a Custom Backend +============================== + +omega|ml currently provides two types of extensible backends: + +1. :code:`CustomDataBackend` - to store and retrieve data objects +2. :code:`CustomModelBackend` - to store, retrieve and execute model objects + (execute = fit, predict, ...) + +To implement your own data backend, implement all methods in each backend. +Once implemented, use :code:`OmegaStore.register_backend` to have the omegaml's +storage layer work with your backend implementation. All storage methods are +then supported out of the box, mainly :code:`put,get,list,drop`. + +.. note:: + + Your backend's respective :code:`put, put_model` methods need to + return a saved :code:`Metadata` object. Create a metadata + object using :code:`OmegaStore.make_metadata()`. \ No newline at end of file diff --git a/docs/source/guide/filterdf.rst b/docs/source/guide/filterdf.rst new file mode 100644 index 00000000..c6677a80 --- /dev/null +++ b/docs/source/guide/filterdf.rst @@ -0,0 +1,454 @@ +Filtering Data +============== + +Query filtering ++++++++++++++++ + +The :code:`.get` method when operating on a Pandas DataFrame provides +keyword-style filtering and an optional lazy evaluation mode. Filters are +applied remotely inside the database and thus perform much faster than if +filtered in the returned dataframe. + +.. code:: + + om.datasets.get('foodf', x__gt=5) + => + x + 6 6 + 7 7 + 8 8 + 9 9 + +The filter syntax is :code:`__=`, where the operator +is one of the following: + +* :code:`eq` compare equal (this is also the default, when using the short form, i.e. + :code:`=` +* :code:`gt` greater than +* :code:`lt` less than +* :code:`ge` greator or equal +* :code:`le` less or equal +* :code:`between` between two values, specify :code:`value` as a 2-tuple +* :code:`contains` contains a value, specify :code:`value` as a sequence +* :code:`startswith` starts with a string +* :code:`endswith` ends with a string +* :code:`isnull` is a null value, specify :code:`value` as a boolean + +In general :code:`get` returns a Pandas :code:`DataFrame`. See the Pandas +documentation for ways to work with DataFrames. + +However, unlike Pandas omega|ml provides methods to work with data that is +larger than memory. This is covered in the next section. + +Large, Out of Core-sized DataFrames ++++++++++++++++++++++++++++++++++++ + +Using lazy evaluation we can get back a proxy DataFrame, an :code:`MDataFrame`, +which provides many of the features of a Pandas DataFrame including :code:`.loc` +indexing and slicing, column projection and aggregation. All of these +operations, however, are executed by the database and thus support out-of-core +sized DataFrames, that is DataFrames of arbitrary size. + +.. code:: + + # ask for a reference to the dfx dataset with lazy evaluation + om.datasets.get('dfx', lazy=True) + => + + + # same thing, getl is convenience method that automatically specifies lazy=True + om.datasets.getl('dfx') + => + + +:code:`MDataFrame` in many ways behaves like a normal dataframe, however the +evaluation of operations is _lazy_ and is executed by the database as opposed +to in-memory. This allows us to process data that is larger than memory. + +In order to evaluate :code:`MDataFrame` and return an actual +:code:`pandas.DataFrame` just access the :code:`.value` property: + +.. code:: + + om.datasets.get('dfx', lazy=True).value + => + x y + 0 0 0 + 1 1 1 + 2 2 2 + 3 3 3 + 4 4 4 + +Column projection +----------------- + +Specify the list of columns to be accessed: + +.. code:: + + om.datasets.get('dfx', lazy=True)[['x', 'y']].head(5).value + => + x y + 0 0 0 + 1 1 1 + 2 2 2 + 3 3 3 + 4 4 4 + +Masked-style selection +---------------------- + +As with Pandas DataFrames, omega|ml MDataFrames can be subset using filter masks: + +.. code:: + + mdf = om.datasets.getl('dfx') + flt = (mdf['x'] > 2) & (mdf['x] < 4) + mdf[flt].value + => + x y + 3 3 3 + +.. note:: + + MDataFrame masks are not series of True/False as they are in Pandas. Instead a + MDataFrame filter mask translates into a query filter that is applied on accessing + the :code:`.value` property. Consider MDataFrame a syntactical convenience that makes + it easy to transform code for a Pandas DataFrame to an MDataFrame. + + +Index-Row selection +------------------- + +Specify the index of the rows to be accessed: + +.. code:: + + # numeric index + om.datasets.get('dfx', lazy=True).loc[2:5].value + => + x y + 2 2 2 + 3 3 3 + 4 4 4 + 5 5 5 + + # alphanumeric index + om.datasets.get('dfx', lazy=True).loc['abc'].value + => + x y + abc 2 2 + + +Numeric row selection +--------------------- + +Specify the numeric row id. Note this requires that the dataset was created with a continuous row id +(automatically created when using :code:`datasets.put`) + +.. code:: + + # numeric index + om.datasets.get('dfx', lazy=True).iloc[2:5].value + => + x y + 2 2 2 + 3 3 3 + 4 4 4 + 5 5 5 + +.. note:: + + The :code:`.iloc` accessor is also used by scikit-learn's KFold and grid search features. Since + MDataFrame's are very efficiently serializable (only specifications are serialized, not actual data) + this feature makes MDataFrames an attractive choice for gridsearch in a compute cluster. Actually + MDataFrame instances can be used directly with gridsearch, whereas for example Dask's DataFrame implementation + cannot. + + +Filter data +----------- + +Filtering works the same on an MDataFrame as with the eager :code:`get` +method, by specifying the filter as the keyword arguments: + +.. code:: + + om.datasets.get('foodf', x__gt=5, lazy=True).value + => + x + 6 6 + 7 7 + 8 8 + 9 9 + + +Permanently setting a filter +---------------------------- + +Note that the :code:`query` method returns a new :code:`MDataFrame` instance +with the filter applied. To set a permanent filter for any subsequent +operations on a specific :code:`MDataFrame` instance, use the +:code:`query_inplace` method: + +.. code:: + + mdf = om.datasets.get('dfx', lazy=True) + id(mdf) + => 140341971534792 + + # mdf2 is a new object + mdf2 = mdf.query(x__gt=2, x__lt=5) + id(mdf2) + => 140341971587648 + + # note how mdf3 is the same object as mdf above + mdf3 = mdf.query_inplace(x__gt=2, x__lt=5)) + id(mdf3) + => 140341971523792 + + mdf = om.datasets.get('dfx', lazy=True).query_inplace(x__gt=2, x__lt=5) + mdf.value + => + x y + 3 3 3 + 4 4 4 + 3 3 3 + 4 4 4 + +.. note:: + + A new :code:`MDataFrame` object returned by the :code:`query` method + does *not* create a new collection in MongoDB. That is, the new instance + operates on the same data. The only difference is that one new instance + has a permanent filter applied and any subsequent operations on it will + work on the subset of the data returned by the filter. + +Sorting +------- + +Sorting works by specifying the sort columns. Use :code:`-` and :code:`+` +before any column name to specify the sort order as descending or ascending, +respectively (ascending is the default). + +.. code:: + + om.datasets.get('dfx', lazy=True).sort(['-x', '+y']).head(5).value + => + x y + 999 999 999 + 998 998 998 + 997 997 997 + 996 996 996 + 995 995 995 + + +Limiting and skipping rows +-------------------------- + +The :code:`head(n)` and :code:`skip(n)` methods return and skip the top _n_ +rows, respectively: + +.. code:: + + om.datasets.get('dfx', lazy=True).skip(5).head(3).value + => + x y + 5 5 5 + 6 6 6 + 7 7 7 + +Merging data +------------ + +Merging supports left, inner and right joins of two :code:`MDataFrame`. +The result is stored as a collection in MongoDB and all merge operations +are executed by MongoDB. The result of the :code:`merge()` method is a new +:code:`MDataFrame` on the result + +.. code:: + + import pandas as pd + # create two dataframes and store in omega|ml + dfl = pd.DataFrame({'x': range(3)}) + dfr = pd.DataFrame({'x': range(3), 'y': range(3)}) + om.datasets.put(dfl, 'dfxl', append=False) + om.datasets.put(dfr, 'dfxr', append=False) + # merge the dataframes + mdfl = om.datasets.get('dfxl', lazy=True) + mdfr = om.datasets.get('dfxr', lazy=True) + mdfl.merge(mdfr, on='x').value + => + x y + 0 0 0 + 1 1 1 + 2 2 2 + + +Statistics +---------- + +The following statistics can be computed on pairs of numeric columns of a :code:`MDataFrame` and on :code:`MSeries`: + +* :code:`correlation` - returns the pearson correlation matrix +* :code:`covariance` - returns the covariance matrix + +.. code:: + + mdf = om.datasets.getl('foo') + mdf['x', 'y].correlation().value + mdf['x', 'y].covariance().value + + +The following statisics can be computed on all numeric columns: + +* :code:`mean` +* :code:`min` +* :code:`max` +* :code:`std` +* :code:`quantile` - by defaults calculates the .5 quantile, specify a list of percentiles + + +.. code:: + + mdf = om.datasets.getl('foo') + mdf['x', 'y].mean() + mdf['x', 'y].min() + ... + + +Aggregation +----------- + +Much like a Pandas DataFrame, :code:`MDataFrame` supports aggregation. All +aggregation operations are executed by MongoDB. + +.. code:: + + mdf = om.datasets.getl('dfx') + mdf.groupby('x').x.mean().head(5) + => + x_mean + x + 0 0.0 + 1 1.0 + 2 2.0 + 3 3.0 + 4 4.0 + +Multiple aggregations can be applied at once by the :code:`agg()` method: + +.. code:: + + mdf = om.datasets.getl('dfx') + print(mdf.groupby('x').agg(dict(x='sum', y='mean')).head(5)) + +The following aggregations are currently supported: + +* :code:`sum` - sum +* :code:`mean` or :code:`avg` - mean +* :code:`max` - the max value in the group +* :code:`min` - the min value in the group +* :code:`std` - standard deviation in the sample +* :code:`first` - the first in the group +* :code:`last` - the last in the group + + +Geo proximity filtering +----------------------- + +If you have licensed the geo location proximity extensions, +:code:`MDataFrame` supports filtering on geodesic proximity by specifying +the :code:`__near` operator and a pair of (lat, lon) coordinates. The result +is the list of matching locations sorted by distance from the given coordinates. + +.. code:: + + om.datasets.getl('geosample', + location__near=dict(location=(7.4474468, 46.9479739))).value['place'] + => + 2 Bern + 3 Zurich + 1 Geneva + 0 New York + Name: place, dtype: object + +Understanding the actual MongoDB query +-------------------------------------- + +Sometimes it is useful to know the actual MongoDB query that is executed, +e.g. for debugging or performance tuning purpose. :code:`.inspect()` returns +the actual query that will be executed on accessing the :code:`.value`: +property. + +.. code:: + + om.datasets.get('dfx', lazy=True).query(x__gt=2, x__lt=5).inspect() + => + {'explain': 'specify explain=True', + 'projection': ['x', 'y'], + 'query': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}} + + +Explaining the access path +-------------------------- + +To understand the full access path and indicies used by MongoDB, use the +:code:`explain=True` keyword. + +.. code:: + + om.datasets.get('dfx', lazy=True).query(x__gt=2, x__lt=5).inspect(explain=True) + => + {'explain': {'executionStats': {'allPlansExecution': [], + 'executionStages': {'advanced': 4, + 'executionTimeMillisEstimate': 0, + 'inputStage': {'advanced': 4, + 'direction': 'forward', + 'docsExamined': 1100, + 'executionTimeMillisEstimate': 0, + 'filter': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}, + 'invalidates': 0, + 'isEOF': 1, + 'nReturned': 4, + 'needTime': 1097, + 'needYield': 0, + 'restoreState': 8, + 'saveState': 8, + 'stage': 'COLLSCAN', + 'works': 1102}, + 'invalidates': 0, + 'isEOF': 1, + 'nReturned': 4, + 'needTime': 1097, + 'needYield': 0, + 'restoreState': 8, + 'saveState': 8, + 'stage': 'PROJECTION', + 'transformBy': {'_idx#0_0': 1, 'x': 1, 'y': 1}, + 'works': 1102}, + 'executionSuccess': True, + 'executionTimeMillis': 1, + 'nReturned': 4, + 'totalDocsExamined': 1100, + 'totalKeysExamined': 0}, + 'ok': 1.0, + 'queryPlanner': {'indexFilterSet': False, + 'namespace': 'testing3.omegaml.data_.dfx.datastore', + 'parsedQuery': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}, + 'plannerVersion': 1, + 'rejectedPlans': [], + 'winningPlan': {'inputStage': {'direction': 'forward', + 'filter': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}, + 'stage': 'COLLSCAN'}, + 'stage': 'PROJECTION', + 'transformBy': {'_idx#0_0': 1, 'x': 1, 'y': 1}}}, + 'serverInfo': {'gitVersion': '22ec9e93b40c85fc7cae7d56e7d6a02fd811088c', + 'host': 'c24ade3fa980', + 'port': 27017, + 'version': '3.2.9'}}, + 'projection': ['x', 'y'], + 'query': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}} + + + diff --git a/docs/source/guide/getstarted.rst b/docs/source/guide/getstarted.rst new file mode 100644 index 00000000..e8625a4d --- /dev/null +++ b/docs/source/guide/getstarted.rst @@ -0,0 +1,44 @@ +Getting started +=============== + +Getting User Credentials +------------------------ + +If you have secured MongoDB and RabbitMQ make sure to specify the +user credentials in the respective environment variables or the +omegaml configuration file. + +*Enterprise Edition* + +A configuration can be retrieved as follows + +.. code:: + + python -m omegacli init --userid test5 --apikey APIKEY --url OMEGA_URL + +Loading omega|ml +---------------- + +Start by loading omega|ml. + +.. code:: python + + import omegaml as om + +Once loaded :code:`om` provides 3 environments that are immediately usable: + +* :code:`om.datasets` - the omega|ml database for Python and Pandas objects +* :code:`om.models` - the omega|ml database for models +* :code:`om.scripts` - the omega|ml database for custom modules (a.k.a. lambda modules) +* :code:`om.runtime` - the omega|ml remote execution environment + + +Loading omega|ml from python +---------------------------- + +.. code:: python + + import omegaml as om + + + diff --git a/docs/source/guide/index.rst b/docs/source/guide/index.rst new file mode 100644 index 00000000..7c1ada4c --- /dev/null +++ b/docs/source/guide/index.rst @@ -0,0 +1,17 @@ +User Guide +========== + +.. toctree:: + :maxdepth: 2 + + + getstarted + store_retrieve + filterdf + mdfapply + modelstore + lambda + restapi + custombackends + runtimes + streaming diff --git a/docs/source/guide/lambda.rst b/docs/source/guide/lambda.rst new file mode 100644 index 00000000..236f5cfb --- /dev/null +++ b/docs/source/guide/lambda.rst @@ -0,0 +1,93 @@ +Lambda Modules +============== + +*Enterprise Edition* + +omega|ml supports execution of arbitrary modules packaged by pip on the runtime cluster. This +is the equivalent of AWS Lambda with the added bonus of having the full set of omega|ml capabilities +available to your modules. + +Writing a Lambda Module +----------------------- + +Creating a Lambda Module is straight forward: + +1. write your code +2. add :code:`setup.py` +3. use :code:`om.scripts.put()` to deploy the package + +To make your code executable through the REST API or in :code:`om.runtime.script` your +code's top-level package must contain a :code:`run()` method: + +.. code:: + + def run(*args, **kwargs): + ... + + +:code:`kwargs` will contain the key/value pair passed to the module on execution. + +The simplest :code:`setup.py` is as simple as follows (: + +.. code:: + + from distutils.core import setup + setup(name='helloworld', version='1.0', + description='simple omegaml hello world script', author='omegaml', + author_email='info@omegaml.io', url='http://omegaml.io', + packages=['helloworld'],) + + +Deploying a module +------------------ + +To deploy a Lambda module use :code:`om.scripts.put()`: + +.. code:: + + om.scripts.put('pkg://path/to/helloworld`, 'helloworld') + +This will build the package and store it in omega|ml. It is automatically +available for execution using the REST API or :code:`om.runtime.script()`. + + +Executing a module +------------------ + +Using the REST API +++++++++++++++++++ + +Use the :code:`/api/script/` REST API to execute a module: + +.. code:: + + POST /api/script/hellworld/?param=value + => + { + 'script': 'helloworld' + 'kwargs': { 'param': value }, + 'result': , + 'runtime': , + 'started': 'datetime in iso 8601 format', + } + + +Using the runtime API ++++++++++++++++++++++ + +Use the :code:`om.runtime.script()` API to run a module on the cluster: + +.. code:: + + result = om.runtime.script('helloworld').run(foo='bar') + result.get() + => + { + 'script': 'helloworld' + 'kwargs': { 'foo': 'bar' }, + 'result': , + 'runtime': , + 'started': 'datetime in iso 8601 format', + } + + diff --git a/docs/source/guide/mdfapply.rst b/docs/source/guide/mdfapply.rst new file mode 100644 index 00000000..1275380c --- /dev/null +++ b/docs/source/guide/mdfapply.rst @@ -0,0 +1,222 @@ +Aggregation Framework +===================== + +.. _MongoDB's aggregate: https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/#db.collection.aggregate + +omega|ml provides a rich aggregation framework that leverages `MongoDB's aggregate`_ operator while keeping +the ease-of-use of Pandas syntax. Typical Pandas aggregation operations like group-by and descriptive statistics +have direct equivalents in omegal|ml with the same or very similar syntax, using :code:`MDataFrame.groupby`: + +Standard Groupby aggregation +---------------------------- + +.. code:: + + mdf = om.datasets.getl('dfx') + mdf.groupby('x').x.mean().head(5) + => + x_mean + x + 0 0.0 + 1 1.0 + 2 2.0 + 3 3.0 + 4 4.0 + +Multiple aggregations can be applied at once by the :code:`agg()` method: + +.. code:: + + mdf = om.datasets.getl('dfx') + print(mdf.groupby('x').agg(dict(x='sum', y='mean')).head(5)) + +The following aggregations are currently supported: + +* :code:`sum` - sum +* :code:`mean` or :code:`avg` - mean +* :code:`max` - the max value in the group +* :code:`min` - the min value in the group +* :code:`std` - standard deviation in the sample +* :code:`first` - the first in the group +* :code:`last` - the last in the group + +Motivating example +------------------ + +If the standard operations provided in `MDataFrame.groupby` do not provide the required functionality, custom +operators or chains of operators can be easily applied using the :code:`MDataFrame.apply()` functionality. Much like +Pandas :code:`DataFrame.apply`, :code:`MDataFrame.apply` takes a callable that operates on the data: + +.. code:: + + # apply to all columns on a dataframe + mdf.apply(lambda ctx: ctx * 5) + +In this example, the lambda will multiply every value in the dataframe by 5. All math operators (*, +, -, /, % etc.) +are supported, as well as a number of other operations. + +.. note:: + + Unlike with Pandas, the callable passed to :code:`.apply()` is *not* executed on every row. Instead the + callable is executed once during preparation of the MongoDB query. The callable receives an :code:`ApplyContext` + which is responsible for translating requested operations to MongoDB query syntax when the dataframe is + resolved by accessing the :code:`.value` property. Call :code:`MDataFrame.inspect()` to see the actual MongoDB + query. + +:code:`.apply()` can be called either on a :code:`MDataFrame` or on a :code:`MSeries`. Further, when called on a +MDataFrame, the operations specified are applied to all columns. When called on a MSeries or when the column is +selected from the :code:`ApplyContext`, the operations are applied only to the one column. + +.. code:: + + # apply to all columns on a dataframe + mdf.apply(lambda ctx: ctx * 5) + + # apply to a column, returning a series + mdf['x'].apply(lambda ctx: ctx * 5) + + # apply to a column, return a dataframe + mdf.apply(lambda ctx: ctx['x'] * 5) + + +Math operations +--------------- + +All standard Python math operators are supported, in particular: + +* :code:`__mul__` (*) +* :code:`__add__` (+) +* :code:`__sub__` (-) +* :code:`__div__` (/) +* :code:`__floordiv__` (//) +* :code:`__mod__` (%) +* :code:`__pow__` (pow) +* :code:`__ceil__` (ceil) +* :code:`__floor__` (floor) +* :code:`__trunc__` (trunc) +* :code:`__abs__` (abs) +* :code:`sqrt` (math.sqrt) + +Math operators can be chained. While operator priority is taken care of by the Python compiler, you should use +brackets to ensure readability and correct operations in special scenarios: + +.. code:: + + # while this works, it is not recommended syntax + mdf.apply(lambda ctx: ctx * 5 + 2) + + # recommended + mdf.apply(lambda ctx: (ctx * 5) + 2) + +Datetime Operators +------------------ + +.. code:: + + mdf.apply(lambda ctx: ctx['v'].dt.year) + mdf.apply(lambda ctx: ctx['v'].dt.month) + mdf.apply(lambda ctx: ctx['v'].dt.week) + mdf.apply(lambda ctx: ctx['v'].dt.day) + mdf.apply(lambda ctx: ctx['v'].dt.hour) + mdf.apply(lambda ctx: ctx['v'].dt.minute) + mdf.apply(lambda ctx: ctx['v'].dt.second) + mdf.apply(lambda ctx: ctx['v'].dt.millisecond) + mdf.apply(lambda ctx: ctx['v'].dt.dayofyear) + mdf.apply(lambda ctx: ctx['v'].dt.dayofweek) + +String Operators +---------------- + +.. code:: + + mdf.apply(lambda ctx: ctx['v'].str.len()) + mdf.apply(lambda ctx: ctx['v'].str.concat(['xyz'])) + mdf.apply(lambda ctx: ctx['v'].str.split(',')) + mdf.apply(lambda ctx: ctx['v'].str.upper()) + mdf.apply(lambda ctx: ctx['v'].str.lower()) + mdf.apply(lambda ctx: ctx['v'].str.substr(start, end)) + mdf.apply(lambda ctx: ctx['v'].str.isequal('string') + mdf.apply(lambda ctx: ctx['v'].str.index('substring')) + + +Cached operations +----------------- + +Any :code:`apply()` call results can be cached to speed-up future queries. To do so call :code:`persist()`: + +.. code:: + + mdf.apply(...).persist() + +Any subsequent call to the same apply operations, :code:`.value` will retrieve the results from the results +produced by :code:`persist()`. Note that :code:`persist()` returns the cache key, not the actual results. + +.. note:: + + Using cached operations can tremendously speed up data science work flows for complex aggregation + queries that need to be executed repeatedly or are common in your scenario. As an example, consider + an aggregation on a 50GB dataset that takes several minutes to compute. Using :code:`persist()` this + calculation can be executed once and stored for subsequent and automatic retrieval by anyone on your team. + +Complex operations +------------------ + +.. _MongoDB aggregation reference: https://docs.mongodb.com/manual/meta/aggregation-quick-reference/ + +:code:`MDataFrame.groupby` supports only few descriptive statics, namely :code:`mean(), std(), min(), max()` since +these are the MongoDB-provided operations. However using :code:`.apply()` more complex operators can be easily +created. See the `MongoDB aggregation reference`_ for details on syntax. + +Multiple statistics can be calculated for the same column: + +.. code:: + + mdf.apply(lambda ctx: ctx.groupby('x', v=['sum', 'mean', 'std']) + + +Custom statistics using MongoDB syntax + +.. code:: + + # specify the groupby in mongo db syntax + expr = {'$sum': '$v'} + # add a stage + mdf.apply(labmda ctx: ctx.groupby('x', v=expr) + + +Parallel execution of multiple calculations: + +.. code:: + + mdf.apply(lambda ctx: dict(a=ctx['v'] * 5, b=ctx['v'] / 2)) + + +Custom projections: + +.. code:: + + mdf.apply(lambda ctx: ctx.project(a={'$divide': ['$v', 2]})) + + +Arbitrary pipeline stages: + +.. code:: + + # specify the stage in mongo db syntax + stage = { + '$': { '<$operator>' : .... } + } + # add a stage + mdf.apply(labmda ctx: ctx.add(stage)) + + +.. note:: + + The callable to :code:`apply()` shall return any of the following result types: + + * :code:`None` - this is equivalent to returning the :code:`ApplyContext` passed on calling + * :code:`ApplyContext` - the context will be used to generate the stages passed to MongoDB's :code:`aggregate()` + * :code:`dict` - a mapping of result-column names to an ApplyContext or a valid list of stages in MongoDB-syntax + * :code:`list` - a list of stages in MongoDB-syntax + + diff --git a/docs/source/guide/modelstore.rst b/docs/source/guide/modelstore.rst new file mode 100644 index 00000000..f3ecd4ea --- /dev/null +++ b/docs/source/guide/modelstore.rst @@ -0,0 +1,175 @@ +Working with Machine Learning Models +==================================== + +omega|ml currently implements two backends to store models. More backends can +be implemented using the model backend-API. + +* scikit-learn models +* Apache Spark models + +Storing models +-------------- + +Storing models (and Pipeline) is as straight forward as storing Pandas DataFrames and Series. +Simply create the model, then use :code:`om.models.put()` to store: + +.. code:: + + from sklearn.linear_model import LinearRegression + + # train a linear regression model + df = pd.DataFrame(dict(x=range(10), y=range(20,30))) + clf = LinearRegression() + clf.fit(df[['x']], df[['y']]) + # store the trained model + om.models.put(clf, 'lrmodel') + +Models can also be stored untrained: + +.. code:: + + df = pd.DataFrame(dict(x=range(10), y=range(20,30))) + clf = LinearRegression() + # store the trained model + om.models.put(clf, 'lrmodel') + +Using models to predict +----------------------- + +Retrieving a model is equally straight forward: + +.. code:: + + clf = om.models.get('lrmodel') + clf + => + LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False) + +Once retrieved the model can be accessed as any model kept in memory, e.g. +to predict using new data: + +.. code:: + + clf = om.models.get('lrmodel') + df = pd.DataFrame(dict(x=range(70,80))) + clf.predict(df[['x']]) + => + array([[ 90.], + [ 91.], + [ 92.], + [ 93.], + [ 94.], + [ 95.], + [ 96.], + [ 97.], + [ 98.], + [ 99.]]) + + +Using the compute cluster +------------------------- + +Prediction +++++++++++ + +omega|ml provides a state-of-the art compute cluster, called the *runtime*. Using +the runtime you can delegate model tasks to the cluster: + +.. code:: + + model = om.runtime.model('lrmodel') + result = model.predict(df[['x']]) + result.get() + => + array([[ 20.], + [ 21.], + [ 22.], + [ 23.], + [ 24.], + [ 25.], + [ 26.], + [ 27.], + [ 28.], + [ 29.]]) + +Note that the :code:`result` is a deferred object that we resolve using +:code:`get`. + +Instead of passing data, you may also pass the name of a DataFrame stored +in omegaml: + +.. code:: + + # create a dataframe and store it + df = pd.DataFrame(dict(x=range(70,80))) + om.datasets.put(df, 'testlrmodel') + # use it to predict + result = om.runtime.model('lrmodel').predict('testlrmodel') + result.get() + +Model Fitting ++++++++++++++ + +To train a model using the runtime, use the :code:`fit` method on the runtime's model, as you would +on a local model: + +.. code:: + + # create a dataframe and store it + df = pd.DataFrame(dict(x=range(10), y=range(20,30))) + om.datasets.put(df, 'testlrmodel') + # use it to fit the model + result = om.runtime.model('lrmodel').fit('testlrmodel[x]', 'testlrmodel[y]') + result.get() + + +GridSearch +++++++++++ + +**currently supported for sckit-learn** + +To use cross validated grid search on a model, use the :code:`gridsearch` method on the runtime's model. This +creates, fits and stores a :code:`GridSearchCV` instance and automatically links it to the model. Use the +GridSearchCV model to evaluate the performance of multiple parameter settings. + +.. note:: + + Instead of using this default implementation of :code:`GridSearchCV` you may create your + own :code:`GridSearchCV` instance locally and then fit it using the runtime. In this case + be sure to link the model used for grid searching and the original model by changing the + attributes on the model's metadata. + +.. code:: + + X, y = make_classification() + logreg = LogisticRegression() + om.models.put(logreg, 'logreg') + params = { + 'C': [0.1, 0.5, 1.0] + } + # gridsearch on runtime + om.runtime.model('logreg').gridsearch(X, y, parameters=params) + meta = om.models.metadata('logreg') + # check gridsearch was saved + self.assertIn('gridsearch', meta.attributes) + self.assertEqual(len(meta.attributes['gridsearch']), 1) + self.assertIn('gsModel', meta.attributes['gridsearch'][0]) + # check we can get back the gridsearch model + gs_model = om.models.get(meta.attributes['gridsearch'][0]['gsModel']) + self.assertIsInstance(gs_model, GridSearchCV) + + +Other Model tasks ++++++++++++++++++ + +The runtime provides more than just model training and prediction. The runtime implements +a common API to all supported backends that follows the scikit-learn estimator model. That is the +runtime supports the following methods on a model: + +* :code:`fit` +* :code:`partial_fit` +* :code:`transform` +* :code:`score` + +For details refer to the API reference. + diff --git a/docs/source/guide/restapi.rst b/docs/source/guide/restapi.rst new file mode 100644 index 00000000..02050462 --- /dev/null +++ b/docs/source/guide/restapi.rst @@ -0,0 +1,464 @@ +REST API +======== + +The REST API provides a direct interface to models and datasets from any +connected client. Unlike the Python API, the client does not need access +to either MongoDB or RabbitMQ to make use of omegaml, nor does the client +need to use the Python language. Use the REST API to interface from any +third-party system to omegaml. + + +API Reference +------------- + +The API reference is accessible online from your omega|ml instance at: + +* :code:`/api/doc/v1`- Swagger UI +* :code:`/api/doc/v1/specs/swagger.json` - the Swagger specs (JSON) +* :code:`/api/redoc` - ReDoc UI, based on Swagger specs + + +API Semantics +------------- + +The omega|ml REST API resources are all of the form +:code:`/api/version/resource-name/resource-key/?param=value`. + +The valid resource names are: + +* dataset - provides access to data +* model - provides access to models +* job - provides access to jobs *Enterprise Edition* +* config - provides access to the user-specific omega|ml configuration *Enterprise Edition* + +The resource-key and query parameters are optional. If a resource-key +is not provided, a list of existing resources is returned. If a resource-key +is provided the API will look up the respective specific resource for this +key and return its content. + +Note that the dataset and job resources will return dataset and job contents, +respectively. The model resource will only provide meta data, but not the +actual contents of the model. + +All resources support a set of HTTP GET, PUT, POST or DELETE methods. + +* successful GET => HTTP 200 OK +* successful POST => HTTP 201 created +* successful PUT => HTTP 202 accepted + +*Enterprise Edition* + +* error due to bad input parameters => HTTP 400 Bad Request +* error due to authentication => HTTP 401 Unauthorized +* error due to wrong authorization => HTTP 403 Forbidden +* error due to non existing resource => HTTP 404 Not found +* error due to not allowed method => HTTP 405 Method not allowed +* severe server errors => HTTP 500 Internal Server error + + +Setting up authorization +------------------------ + +*Enterprise Edition* + +From your omega|ml portal, get the userid and api key. + +.. code:: + + from omegacli.auth import OmegaRestApiAuth + auth = OmegaRestApiAuth(userid, apikey) + + +Working with data +----------------- + + +Listing datasets +++++++++++++++++ + +.. code:: + + resp = requests.get('http://host:port/api/v1/dataset/', auth=auth) + resp.json() + => + {'meta': {'limit': 20, + 'next': None, + 'offset': 0, + 'previous': None, + 'total_count': 3}, + 'objects': [{'data': {'kind': 'pandas.dfrows', 'name': 'sample'}, + 'dtypes': None, + 'index': None, + 'name': None, + 'orient': None, + 'resource_uri': '/api/v1/dataset/sample/'}, + {'data': {'kind': 'pandas.dfrows', 'name': 'sample2'}, + 'dtypes': None, + 'index': None, + 'name': None, + 'orient': None, + 'resource_uri': '/api/v1/dataset/sample2/'}, + {'data': {'kind': 'pandas.dfrows', 'name': 'sample99'}, + 'dtypes': None, + 'index': None, + 'name': None, + 'orient': None, + 'resource_uri': '/api/v1/dataset/sample99/'}]} + + + +Reading data ++++++++++++++ + +.. code:: + + resp = requests.get('http://host:port/api/v1/dataset/sample', auth=auth) + resp.json() + => + {'data': {'x': {'0': 0, + '1': 1, + '10': 0, + '11': 1, + '12': 2, + '13': 3, + '14': 4, + '15': 5, + '16': 6, + '17': 7, + '18': 8, + '19': 9, + '2': 2, + '20': 0, + '21': 1, + '22': 2, + '23': 3, + '24': 4, + '25': 5, + '26': 6, + '27': 7, + '28': 8, + '29': 9, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9}}, + 'dtypes': {'x': 'int64'}, + 'index': {'type': 'Int64Index', + 'values': [0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9]}, + 'name': 'sample', + 'orient': 'dict', + 'resource_uri': '/api/v1/dataset/None/'} + +.. note:: + + To get a valid dataframe back do as follows. + + .. code:: + + import pandas as pd + df = pd.DataFrame.from_dict(resp.json().get('data')) + df.index = index=resp.json().get('index').get('values') + + + It is important to set the index to restore the correct row order. This + is due to Python's arbitrary order of keys in the :code:`data` dict. + + +Writing data +++++++++++++ + +Writing data is equally straight forward. Note this works for both new +and existing datasets. By default data is appended to an existing dataset. + +.. code:: + + data = {'data': {'x': {'0': 0, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9}}, + 'dtypes': {'x': 'int64'}, + 'orient': 'dict', + 'index': {'type': 'Int64Index', 'values': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, + 'name': 'sample'} + requests.put('http://host:port/api/v1/dataset/sample/', auth=auth, + json=data) + => + + +To overwrite an existing data set, use :code:`append: false` + +.. code:: + + data = {'data': {'x': {'0': 0, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9}}, + 'dtypes': {'x': 'int64'}, + 'append': False, + 'orient': 'dict', + 'index': {'type': 'Int64Index', 'values': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}, + 'name': 'sample'} + requests.put('http://localhost:8001/api/v1/dataset/sample/', auth=auth, + json=data) + => + + + +Transform a DataFrame to API format ++++++++++++++++++++++++++++++++++++ + +To transform a Pandas DataFrame into the format expected by the API, use +the following code snippet. + +.. code:: + + def pandas_to_apidata(df, append=False): + # TODO put logic for this into client lib + data = { + 'append': append, + 'data': json.loads(df.to_json()), + 'dtypes': {k: str(v) + for k, v in iteritems(df.dtypes.to_dict())}, + 'orient': 'columns', + 'index': { + 'type': type(df.index).__name__, + # ensure type conversion to object for Py3 tastypie does + # not recognize numpy.int64 + 'values': list(df.index.astype('O').values), + } + } + return data + + +Working with models +------------------- + +Create a model +++++++++++++++ + +.. code:: + + data = {'name': 'mymodel', + 'pipeline': [ + # step name, model class, kwargs + ['LinearRegression', dict()], + ]} + requests.post('http://localhost:8001/api/v1/model/', + json=data, + auth=auth) + => + + {'model': {'bucket': 'store', + 'created': '2016-01-16 22:05:06.192000', + 'kind': 'sklearn.joblib', + 'name': 'mymodel'}} + +Fit a model ++++++++++++ + +Create some data first: + +.. code:: + + # a simple data frame to learn + df = pd.DataFrame({'x': range(10)}) + df['y'] = df['x'] * 2 + datax = pandas_to_apidata(df[['x']]) + datay = pandas_to_apidata(df[['y']]) + + # store data + requests.put('http://localhost:8001/api/v1/dataset/datax/', auth=auth, + data=json.dumps(datax)) + requests.put('http://localhost:8001/api/v1/dataset/datay/', auth=auth, + json=datay) + => + + + +Then we can fit the model: + +.. code:: + + resp = requests.put('http://localhost:8001/api/v1/model/mymodel/fit/?datax=datax&datay=datay', auth=auth, data={}) + resp.json() + => + {'datax': 'datax', 'datay': 'datay', 'result': 'ok'} + + +Subsequently, the model is ready for prediction: + +.. code:: + + resp = requests.get('http://localhost:8001/api/v1/model/mymodel/predict/?datax=datax', auth=auth, data={}) + resp.json() + => + {'datax': 'datax', + 'datay': None, + 'result': [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0]} + + +Working with jobs +----------------- + +*Enterprise Edition* + +The jobs api supports creating, executing and status-checking jobs on +the cluster. + +.. warning:: + + Creating jobs via the API assumes that the user creating the job + is trusted. Any code can be inserted and could potentially compromise + your cluster. + + +Creating a job +++++++++++++++ + +.. code:: + + data = { + 'code': "print('hello')", + } + resp = requests.post('http://localhost:8001/api/v1/job/testjob/', + json=data, auth=auth) + resp.json() + => + {u'job_runs': [], u'job_results': {}, + u'name': u'testjob.ipynb', + u'created': u'2016-02-06T21:31:39.326097'} + + +Listing jobs +++++++++++++ + +.. code:: + + resp = requests.get('http://localhost:8001/api/v1/job/', + auth=auth) + resp.json() + => + {u'meta': {u'previous': None, u'total_count': 1, + u'offset': 0, u'limit': 20, u'next': None}, + u'objects': [{u'job_runs': [], + u'job_results': {}, u'name': u'testjob.ipynb', + u'created': u'2016-02-06T21:33:49.833000'}]} + +Getting information on a job +++++++++++++++++++++++++++++ + +.. code:: + + resp = requests.get('http://localhost:8001/api/v1/job/testjob/', + json=data, auth=auth) + resp.json() + => + {u'content': {u'nbformat_minor': 0, u'nbformat': 4, + u'cells': [{u'execution_count': None, u'cell_type': + u'code', u'source': u"print('hello')", + u'outputs': [], u'metadata': {}}], + u'metadata': {}}, u'job_runs': [], + u'job_results': {}, + u'name': u'testjob.ipynb', + u'created': u'2016-02-06T21:44:59.290000'} + + +Running a job ++++++++++++++ + +.. code:: + + resp = requests.post('http://localhost:8001/api/v1/job/testjob/run/', + auth=auth) + resp.json() + => + {u'job_runs': {u'1517953074': u'OK'}, + u'job_results': [u'results/testjob_1517953074.ipynb'], + u'name': u'testjob.ipynb', + u'created': u'2016-02-06T21:37:54.014000'} + + +Getting job results ++++++++++++++++++++ + +To get job results in iPython notebook format, use + +.. code:: + + + resp = requests.get('http://localhost:8001/api/v1/job/results/testjob_1517953074.ipynb/', + auth=auth) + resp.json() + => + {u'source_job': u'testjob', u'job_results': {}, + u'created': u'2016-02-06T21:36:06.704000', + u'content': {u'nbformat_minor': 0, u'nbformat': 4, + u'cells': [{u'execution_count': 1, u'cell_type': u'code', + u'source': u"print('hello')", + u'outputs': [{u'output_type': + u'stream', u'name': u'stdout', + u'text': u'hello\n'}], + u'metadata': {}}], + u'metadata': {}}, + u'job_runs': [], + u'name': u'results/testjob_1517952965.ipynb'} + + +Getting a job report +++++++++++++++++++++ + +To get job results in HTML format, use + +.. code:: + + resp = requests.get('http://localhost:8001/api/v1/job/export/testjob_1517953074.ipynb/', + auth=auth) + resp.json() + => + {u'content': " ... ", + u'name': 'testjob_1517953074.ipynb'} + diff --git a/docs/source/guide/runtimes.rst b/docs/source/guide/runtimes.rst new file mode 100644 index 00000000..411d3ee7 --- /dev/null +++ b/docs/source/guide/runtimes.rst @@ -0,0 +1,60 @@ +Runtime Clusters +================ + +By default omega|ml uses a Celery cluster for remote computation. However +the runtime is flexible to other clusters, provided the cluster supports +submitting arbitrary functions (in particular, omegaml's task functions). + +Celery Runtime (default) +------------------------ + +The Celery runtime is the default implementation, provided as +:code:`om.runtime`. It provides the following interfaces: + +* :code:`.model()` - to get a model proxy to a remote model, :code:`OmegaModelProxy` +* :code:`.job()` - to get a job proxy to a remote job, :code:`OmegaJobProxy` *Enterprise Edition* +* :code:`.script()` - to get a script proxy to a remote script (lambda module), :code:`OmegaScriptProxy` *Enterprise Edition* + +The model proxy supports most methods of scikit-learn models, e.g. + +* :code:`fit()` +* :code:`predict()` +* :code:`transform()` +* etc. + +.. note:: + + All omega|ml proxies support the same interface, although the specific + backend implementation may not support all functionality or apply slightly + different semantics + +See the `Working with Machine learning models`_ for more details. + +The job proxy supports two methods: + +* :code:`run()` - to run a job immediately +* :code:`schedule()` - to schedule a job in the future + + +Dask runtime (optional) +----------------------- + +The Dask (distributed) runtime supports executing omega|ml tasks and jobs on a +dask cluster, using the same semantics as the celery cluster. + +To enable the Dask cluster, + +.. code:: + + # get your omega instance + om = Omega(...) + + # create a dask runtime and set it as the omega runtime + om.runtime = OmegaRuntimeDask('http://dask-scheduler-host:port', + auth=om.runtime.auth) + + +Once this is done, om.runtime works as with the default runtime, except that +now all tasks previously executed on the celery cluster will now be executed +on the dask cluster. + diff --git a/docs/source/guide/store_retrieve.rst b/docs/source/guide/store_retrieve.rst new file mode 100644 index 00000000..362c86e2 --- /dev/null +++ b/docs/source/guide/store_retrieve.rst @@ -0,0 +1,70 @@ +Storing and retrieving data +=========================== + +:code:`om.datasets.` provides two simple APIs to store and retrieve data: + +* :code:`om.datasets.put(object, 'name')` +* :code:`om.datasets.get('name')` + +Native Python objects ++++++++++++++++++++++ + +Any Python native :code:`list` or :code:`dict` object can be stored and +read back directly: + +.. code:: + + myvar = ['data'] + om.datasets.put(myvar, 'foo') + data = om.datasets.get('foo') + => + [['data']] + +Note the result is now a list of the objects stored. This is because any +object is stored as a document in a monogodb collection. What you get back +is a list of all the documents in the collection. By default :code:`put` will +append an existing collection with new documents. + +.. code:: + + om.datasets.put(myvar, 'foo') + om.datasets.put(myvar, 'foo') + data = om.datasets.get('foo') + => + [['data'], ['data'], ['data']] + +To replace all documents in a collection use the :code:`append=False` kwarg. + +.. code:: + + myvar = ['data'] + om.datasets.put(myvar, 'foo', append=False) + data = om.datasets.get('foo') + => + [['data']] + +Pandas DataFrames, Series ++++++++++++++++++++++++++ + +Pandas Dataframes are stored in much the same way. Note however that DataFrames +provide additional support on querying, as shown in the next section + +.. code:: + + import pandas as pd + df = pd.DataFrame({'x': range(10)}) + om.datasets.put(df, 'foodf', append=False) + om.datasets.get('foodf') + => + x + 0 0 + 1 1 + 2 2 + 3 3 + 4 4 + 5 5 + 6 6 + 7 7 + 8 8 + 9 9 + diff --git a/docs/source/guide/streaming.rst b/docs/source/guide/streaming.rst new file mode 100644 index 00000000..0110fe68 --- /dev/null +++ b/docs/source/guide/streaming.rst @@ -0,0 +1,124 @@ +Mini-Batch Streaming +==================== + +*Enterprise Edition* + +omega|ml provides a straight-forward, Python-native approach to mini-batch streaming and complex-event +processing that is highly scalable. Streaming primarily consists of + +* a producer, which is some function inserting data into the stream +* a consumer, which is some function retrieving data from the stream + +Instead of directly connection producers and consumers, a producer sends messages to a stream. Think +of a stream as an endless buffer, or a pipeline, that takes input from many producers on one end, and +outputs messages to a consumer on the other end. This transfer of messages happens asynchronously, that +is the producer can send messages to the stream independent of whether the consumer is ready to receive, and the +consumer can take messages from the stream independent of whether the producer is ready to send. + +Unlike usual asynchronous messaging, however, we want the consumer to receive messages in small batches as +to optimize throughput. That is, we want the pipeline to *emit* messages only subject to some criteria +of grouping messages, where each group is called a *mini-batch*. The function that determines whether the +batching criteria is met (e.g. time elapsed, number of messages in the pipeline) is called *emitter strategy*, +and the output it produces is called *window*. + +Thus in order to connect producers and consumers we need a few more parts to our streaming system: + +* a :code:`Stream`, acting as the buffer where messages sent by producers are stored until the emitting +* a :code:`WindowEmitter` implementing the emitter strategy +* a :code:`Window` representing the output produced by the emitter strategy + + +.. note:: + + The producer accepts input from some external system, say a Kafka queue. The producer's responsibility + is to enter the data into the streaming buffer. The consumer uses some emitter strategy to produce + a Window of data that is then forwarded to the user's processing code. + +Creating a stream +----------------- + +Streams can be created by either consumers or producers. A stream can be connected to by both. + +.. code:: + + stream = Stream.get_or_create('test') + +Implementing a Producer +----------------------- + +.. code:: + + # a very simple producer + for i in range(100): + stream.append({'date': datetime.datetime.now().isoformat()}) + sleep(.5) + + +Implementing a Consumer +----------------------- + +.. code:: + + # a fixed size consumer -- emits windows of fixed sizes + @stream('test', size=2, keep=True) + def myprocess(window): + try: + db.processed.insert_one({'data': window.data or {}}) + print(window.data) + except Exception as e: + print(e) + return window + + => + + [{'date': '2018-04-30T20:18:22.918060'}, {'date': '2018-04-30T20:18:23.481320'}] + [{'date': '2018-04-30T20:18:24.041337'}, {'date': '2018-04-30T20:18:24.593545'} + ... + + +In this case the emitter strategy is :code:`CountWindow`. The following strategies are +available out of the box: + +* :code:`CountWindow` - emit fixed-sized windows. Waits until at least *n* messages are + available before emitting a new window +* :code:`FixedTimeWindow`- emit all messages retrieved within specific, time-fixed windows of + a given interval of *n* seconds. This guarnatees that messages were received in the specific + window. +* :code:`RelaxedTimeWindow` - every interval of *n* seconds emit all messages retrieved since + the last window was created. This does not guarantee that messages were received in a given + window. + + +Implementing a custom WindowEmitter +----------------------------------- + +Custom emitter strategies are implemented as a subclass to :code:`WindowEmitter`. The main methods +to implement are + +* :code:`window_ready` - returns the tuple :code:`(ready, data)`, where ready is True if there is data + to emit +* :code:`query` - returns the data for the new window. This function retrieves the :code:`data` part + of the return value of :code:`window_ready` + +See the API reference for more details. + +.. code:: + + class SortedWindow(WindowEmitter): + """ + sort all data by value and output only multiples of 2 in batches of interval size + """ + def window_ready(self): + qs = Buffer.objects.no_cache().filter(processed=False) + data = [] + for obj in sorted(qs, key=lambda obj : obj.data['value']): + if obj.data['value'] % 2 == 0: + data.append(obj) + if len(data) >= self.interval: + break + self._data = data + return len(self._data) == self.interval, () + + def query(self, *args): + return self._data + diff --git a/docs/source/images/deployment.gif b/docs/source/images/deployment.gif new file mode 100644 index 0000000000000000000000000000000000000000..49a97e2a5f114b2f275ae505f4bc3ed5758af4b4 GIT binary patch literal 10887 zcma)=)k6~wpoPa^FnXh#(XDg~qZ={$btnpMZdX zkdTmwh=`b&n1qCcl#~<#fk2^9GBPr9a&ig^3Q9^!Dk>^!YHAu98d_RfIyyRfdU^&1 z21Z6kCMG6kW@Z=+#=^qF%F4>d#>URh&cVUK$;rvZ#l_9d&BMdP%gf8h$H&jlFCZZB z-S64STH+Oe;4-XGdPtTVxU%q(@R$KEA%b{{H?*Br+f%ATTg6 zC@3g6IQZ?`x9{G)L!nS%VPWCn;SmuLk&%&6QBl#+(P%U}CMG5}Ha0#!J|Q9D{rmSH zK72?_OiW5jN={BrNl8gfO-)NnOHWVF$jHdd%*@Kl%FfRI`0-;-PEKxaZeCtqetv#I zK|x_*VNp?0adB};Nl9sGX?b~hMMcG@PoJu)s;aB2Yieq0YisN3>gwz38yXrK8ylON znwp!NTUuILTU$SW{@m8q*52OU(b3V_+1b_A)!p6Q)6>)2+uPUI*WceiFfcGUI5;#k zG(0>!GBPqcIyyEsHalqCU%#%btgNoCuC1-Dudi=xY<&CnZF6&TYinzJdwXYRXLomZZ*OmZfB)d%;PCM9 z=;-M9`1s`HP}f*KeD*u!T~uP^8i2b1$!j@1_q zMnl*%bJQD(hT~{N+&9J=ibp@dlwuh)8cW7gxSv;AjyIN0W(Yb=amAnuQ8l%0qlbNUB5G6iw|bDTG(>!=6IpXX11sM@9pJ|Kc3Bv zOus4{K|GS;jXoG#VE?rSKK?Oe8eY=n6$%b$7D;4-QNR`1Y=v$ja7aYwb!3+;1?MP% z{m-$CfgEkN;a7k;Fg^$k$5+~&Hc1Mhz}jXG1(Lbi%1*cz%)k!}z0yUr4Q=5>IbhKk zFqwhmhN@6LK+c2yOCX0 zFL~w8*f=GA%I}zrg|K+JXzcN$WyQJbg=^n{=zEFnD8BQy{dAA>_T#du^Ns_#j|MX3 zrd|Z?fO?OM?z_XPi=Kzyrx(4)22JtIOdA8Gn$0h&E(g+IC}0cw^QJQ?9Q3h-VezrA zI-8RZ6;F_YeG8&2LK#b{J7*fRzl=wH;{TEt@{g9vTVyna031%`?o<@X^v98`A(M@S zhY8M#c&Z9ZArV%n7J@usmy}H86OF+B;>Z8h1VQR$xu7d;Y+jR!V-v$jD(Tq!>GxV> z3?*t+OX%=gqt|UX&1=uA&rKnK8RKiDAdV#4fGgYyCyKF`&`iNMd>Qn%aVg&-cUSv) zIVOBR#@` z$8AZUp|J$PcvxpIN;w4?Y_X^|J7j5pc50>RGM~_4p`L7CM#-1U^C0o8D0`Cv6*z^>oyEkr%E?fihpxcRlDnD z3=ow6dr$Cq7Tt@d;;LwWZ)=VxtkhO^9PuQ1%$VGsm;Q0|oisrKe4kFyZRz6eJvlhS z6=@71E2-5rfW}?ZLF+2Xs*O2PoF#&oUO0-exSHcvPW&{Bh%n|z|H0{OG6`XLP099l zSlgnLRP1SPGqQ|j)YCf2@_ zaV7u+srXz(6>?-#8Db%!tkqhZ*V(sS$2xU^Une(zcieVgCe#HVX>I*k``WRUG#p7` z8zzZXg8fMYynB>M)GO%&@PI*y{82&)-){ReU(oCA38A6eD42?Cq|`(Mns$ZACDweO zBMTAdlgdUd5gHCx_q3;|l4NQtfb)(d5fGfQGO0hOTgbuPVfuSF3RgAO{}{OV!`g97 znX@#>>{`H4`hGlyF~+ORbH2rFP>EFxO#v2yC?XVK7U$O)dr)T-vYl;ciX-(akst~> zg|YQ4QqNx1c6rw>mFcaEpPVAA8Lglle;^>B6Jg!KeqSuz5VYtru8uCHfHw-oez@cCjM56|DhV&Ue z2u{-^^@MGl`fgfG4D0@{XUtgjE(&;8fwyO$Qk@^7av-qvw`TC}L_Rcrsa znd4t}YfO&Nw`ZD>e%K-TV5AY8TO|Huc-NcVvAzb!)6T{KzeRmi{7E5}0od`0AEsT) z8VuCXpejjXR6y#-t}7q!3bvx^4n z@l&YUSQ) zNl0Igx}^4AIGtk$;MGQxyR}~Ve%V%dxNMN~8PUsU|I*vD6Ea%`(dJ?zL+@bV3?bs` zdXkbwNe=sCq;pEX{bLwDWh%3Ng;Yqd#QFU-cA^szKbNrB(dg z1bH<_#G2?qRvr>z61>cXf+dAeNs1U6Lzu~h*+i(0b|kNCZNX)>*jR*OFy&*Gutk4& zT35dTE}Q9c@xpDa#sY2&RB#yt*kVi40s-3UhfA1pqf7i)qPQ)}3EEV}oRuJmJw-uI z2T^B-4Ni!jsn&gK;8$!fHBK+{HUYgfO?@$=r(+(dodwxJ-BO0F=$1(zcgyyAL`2ufsv82DzZjkD5?6EqTkV=$0%b+ zT6Cblq9|2RuT6b}@m#~B`CnGV7>tC>yP!$?qbed}Y*b=d3u8Z-qPiOy^YARJ{51KO zK-Kj@wKP6uVDfroFc%8dJr=5PMIZD#a`j6fMN<64F`eC*&Xj39r;}EBsMhCO3%lx(%jl!`aWi%O*z>Zvkd+|8gg$|Na0vNc^Lgcw%B!jWoMZXWr=pF$mHaxw=*$ zbv}dj4>Oat9OFK5QUNO5fZn0aBRqtXet`#4RX#7{qTLHHR16?mWo)5Pi@|sN0Ws|3 zgxeed&VW?)tW?eRRPEJN-TPDoZJL2pnxR>maX^}BR+@Qxn&oPm^?jNxK5YZYlw*-i zo)w}X%--gtd<8I@%z*490`Q<2wj)l?zny~WaqQYNXwwpb4em9+-LZF)Tb4g~-vr?% z5fghsVow;hOcgo|#~sOR{Sj!9M)o#1TX83T z)>Hsg4y|6zZokUTZbR9A;lx7#eL_k48!SL?Z1Ghg>@+#OW??Ys$Ho}?w-RL-~? z{Um-Y-%if#D%}Fw^*c-S*k_W#OKgWrb{N-dc8tqwPX*yC05woS1Bl8yFwppd&JnCw*#`vRxC`XhG3z*f>RnRuwoZ_M@3{( zdKYQO7G_`<5wJu^j`1YD2?(Snm01awU-5I~5&>S}0TCWyFhD3A;>_zW+r&-aU3jTp zhNEvsEe>%RwPSLQkY-EqYb>E$gNS3OsgP-klA}Jj^Oo@DV2yo?sA1##y^py~?~l`r z>L|3DO1$zkU;47q2!K^f1AU1bl0yv=0re@Mos{wPl>49Qx!HwPl<|WV6~PCk-%QDR z!=ua(s^mb`=?yQ-zVmh|1%I!of<_gJ1>|!7sb2VA-8fJ>H(14KCp)H8(}3pRBnViU zs1`LdxG;KUJ6^+ah1@16dsdo#ovmNeuEh{och~Xs<_~%ERm{KpI>0Gyb96r%wb7>a#fe07U_CY(Y7+99);J}jUZWDD+&cK#5hU!YN5%HvQ?-(S0;)=aTe$)h0> z74z0%5Hi97Ns@x3YC!q`z*7VSRe&vJ(JXgL4uBJZcRbY0J$s#7WofB|V!ch)yj43( z)#FlD={}1_R>)&D`ddJ(kIGQV5jIKXa%@x|kAgECD=fJrd?(*)0-MB|mFRar@>)QY zQ!O}(6lPk)Grq&JVokyJ?eGZYZcrpEy_`*_>MJd1v4xWkA4D?13d#Llf3JYUCP(Fc~Px8mu$*pE#2uE%}otNdJl>P|t%*F_cFPm|MslGI@I zsc&Gf|Kgz22|R%0Gu$|ZcXXz*Pj$kft>w-fpUFwvmD(uF-+A)MC{K8%I>33VQ~4e` zPc1;kp~Qy}Lf*@C+gqN!({iA9IrY+@#{D33RvPO=m_hIWfgK6O9FcG#{{jMs2o9uX zPgXZc$i|0GAh5bVL5*@yCZ6$xNdT!hPwe@ktIHy>GgS%8)|k|QUj1SjS;+SSPc)}oqozr`%`!m{2|k1q6{$J*}{S;0#l znNXJ>#c`_tNK`$OGH@ZuQkj43@4N`1IFgC66S`|ll@n3q4{U1bVYA2V6aU?kP$m$RQvvwUX2Lp1ZnFG;0)DgH-{)?-zX`ud{S zvbW@LTLSGwV9G$Ud3*heWA}9QeAj;LQssa?-!MT3dZRaCIA*8YI*xzQiwIB<*N>@8 zt7EJq`}G`2y!kod<=(fdgWR+&%erEoxpONQ+=MxT+_}Y&D8gzwnHu^otdCONTNzhh zK0_8neymG8ZUHuFpM8r5=WS#4=qTzecK=wTI9sI3TlaC>p(9!{uv*8h?Vio_XVQ}}Hx`v~T0;a!tu}4cIWLUcoI#&f5@z*4CbQl^4#MR_4hRQDPMVds7jjbwstgLymdh%a=sr z1;@(QdQr%$WltS^sx$HgdQ#jGi>^K9Wg*t+19lXwn=75r%nS${9kR|JTg+qw2Br`26zD!K8Du3ul7)_TwHOoU~f2KDWkN0g{H@jd{s%W;1G@BEVK;#%(F zr}f3}w-965@*)#fRcu{lzeiA$@`?|gLj&_In zQT|0=KLYq_du!9?Uv|U4ADD*|o4zqPj&f)tL?6Qtgt=!f06_J&;b9&upilZ&J?s$4AA@Od&ESYHMNSarVz(HozcHabn`sql$= z48~)x69g0b7sEZM_6NFcVG$H|*fNsz$5Y{by(qj z*IT0|wN@DISCd5_tYbSKag{52F>hECc|Pj?tyRE_Q;Y($)>hVb-6(cI29byp78C&z z9tqRa@dcE!EE>_%uDZ`be|krds37(mR9FZOf zcclF;iWA_ud%_Wkf*7Uht6|;24a{V1oeD)WvT8*D4>*PhMdAdm7CKvF#42#K6u08(x{Onp>%-$#HJm;b!3uDJd9j8PGJPKEjtj%Wg}?v+90+%qS68gV`YWBWz1A*P{YgCbSD)`5x41f4pqDcIR-amE=d- z*0Larsw&JjLT#f_y4Wn(6o$oYOMP5?fU6ra-LrLi}}1e{-Q16SGDA97rFA+#6j#hl}MXsG>iXy z0=%R`AE^a0o#-Elu2sDp8V!rrC>yW2ZaoYf|Y&{X-D}=?yiA=!*Vc2%& z8?~wce|SU$Cw~g~{#I!o3(BXQGnEA?mmFzV@yQ9FB$tVXAiDYhuuS_O{6`XfR@3ha z!2N)}2xT^D4fFt8G07yFlY(q4U{Z>0Vy$f?Wf=O>J}^pJa)Di1^~675qxkLFU90GC zA=Pk8v-f})6kK;UQORAhx<+5(Z_#Oux`mQuoO$Gmf0X zg*;p;-!WI|^M)KrWiiu+FPpTBn#+XR1K3~9IOiNu97=lNYk3#>s2>;>%N*cK`%?Mj ztp*=G`_-=Zwo=>CQbP+4l17A4Ef)MIDk(MR-inK+=tv!WV3*d9HC`;L_D)a|Ggm!V z{#nEfa#L5~O}mVDDdu`){_s=6AiZ_5lsRE80uf`7{l`Z^_Uc&6uj6@MQ5kPv+`KWB zv|$llb6L_S1KAIOy8HtC3cNo~)Ge9}&9YM}Z-U8Fz&J}4QOzYLr6~se6H|)9QB_uv zpCSaMjMZY(YP$IOjAvq(9sFvtTJ40|?tM{l?CBZcRAfzlMMGUJ2iqw^d%VGwj z4H)i(Q|;u5>9XB(v9$$?@;}PlR#>qnldse8@;Kx!r-=Xot6DZVUw|h3IE0acW5UYy zrauj@8#p{&9a|!&tEGR5fUCc@MFeGh=#N7|9mieAU@I+` z7vXpSW`qMQ^T7H}QA^G^;cIh!zfl41!73{BchKeHhnM&2t5!YEkynsxjO!a{L;snX zup>GU#{hIP55mTg8mI%?$IvAXsu({q1C|va+q{=;c%5Ej>??{Z>e}|$JWrl)S25lj z=;Z$5m?onhwR;Q{dean?P9ZYHLW)Ae%Dwur;DUI5L_>HP++)~%z{Yxa>X)zDF|Xb_ zfSW4X6URmN?BX=I6937)73#6bz1bxWrSvOZ&G(7B_clo0aq4Y#M5msNQhrXO#c zCw%6p23#}xGO0HpprVaDDviCbnQit-?^K4|N^8Nkm(DC8ArO07LbEGD=0A%PBDbFi z?5~USA92V|e%3r?N{h0|#Cddqi=;1R?fcviknJW8dsz2_d}MjM`obJN9T?;AKw=U7 zD24d&e+zyJ$QlLx{OiRRV%H54S?*X=B@>P}v~!n1N!+CDFv9W4P=Qtl1?= zmi&y!%5mlF+7HjBcrg#%yUkXLk z-{eny$G)Ed)bB{&{8?@@VyLjCHm_AuToD~|X7OifsY$L>Vp>DjsTUKp8N^lQU-JxX zu?4C9-g7&u_E!H)dSP4M8kzFU|0dcsiyzDA58s94Ox-)Rv%5Ecb*MK7dOwVyKge4Q z+W0q4f2b*~d-r0+L|%ISqU3kyR_Lm~^wXn@(m(5%{<{yoKfc#eb zxs2wW1(Wm)iZX3<3S?)c6zqLMeBf7OVFZGq)N>Kk8=Vpk4LJOgJv*$>eMyFDHpU@l zu7@)E)Pdl}KE`UrCnvr9Ia1u((orkn!SnJTJxJJ&!WxcB*x!>_oK!~B_Mu#(pnxGx3guu{WeEx} z&KNPOpaZo{^$Lp`eGmWbRzTdTY$uaJG$0*@2viLLXA9NCR#9ak2o_K zw6&~CSA*Dlw+2W=CK1kioJ^IP%3+4C0d7k)cSQt>yA$O_o0&u_c?tu>MN@hzv{+=* zDT9eaqfl^MVlQm+3S5_~e9Qy48dm>EZrgMkM{j3my{}<%pfb?RW3j= z>C86ST+*B9IQh487!a(*Up(C23~d^c>MDUZH)}nMpCmlfBJ-K-^JOQ*lJ8^Jma3c_ z3KkrsKo3Y)4=IBC4e*oE_!)E5W=QD7h5)~#_OPMaZTOI7lCrGhknCzo(WGceIE6-G zYMi8w8ULqu80Bw-~V#MGK7!%u^j0FI(uHw-I7^F#u00Kr-zjW5{Bb&$xc!fBvXsDwHgn(dJ+twWCz6uafn6Qi!r!$?2{LRchX zOM&NReFgRum=0pPK4V2Qd&OdM(FXhnF$XDp{~Q-8<_wesvCSX|LB^ra-b7ax;@=6i zqC$IIV{x?sfaj38r^5?R|4wc%wx2ig%TXAb0_K#uBbAL72OSNiztnvH8Sm<nKF}!qZ zp@qJrKtL%Cgwl}c=|x|aiw;8l?NC$6RJ>-<^DyH?OKn+;I0xc1eI81b>#|z#a8;7A z`=h8wB(rB)ysg!eUF4E}0yub{`fUu}JGYFNtGaF6nt=Ln`V|BUtbngH_@cDDrQmDG zNya;TY+sK4>#48vi%}S9ap?~*tWTcjMh*1QLu2_Mt6p98X(00e+*^GtC%XV6c zmCnhowA`%}YtRJ-(}KjzQjJ!T+hfT&`0*-+36Jt72WAmo3}Z{+@A7Ow&T) zanavt{p_`c!2#*>kaZpN^^O(`t2C{o1&gj1U)!fmdk>a6TNJ{LTD-qo3<)euIIOY^ zwAJ#7y&PDvl*FBkv}~a;A4o&D?ro6!Zs;vp8Xt$v|JaDewj5eC#L#f;4sBp$zdf;y ztbFvq z{tM-rU91I$OMD}raZj9ok83|yWZ8BTx+li8RX^Dyfx9O&6Dw8bu&D$2?dZ^I+V_P4 z?QNuk6Cb^tld1{*Dx;uNUN{}gU8ni8_$GH(fPUYK$5Aa^2U}EHN6XPUU|RL1vP**F zI)6>{_MTj=H9a(n6^5k?-gluJIrCk4H7rK!1(kaBmwUxb>BW7ZRC8=&5^7PL$Uv_(8<%!ZW*Pg1mlHJfNRX~OB*Ab5sg_a z(V=_r_aezwa|y5`W$_~~5_-Qkjf6FaqP067FP-@C@gze5(nxCY`@@nNdU~fhhUWC8uCO!>biH$^HLYC*iYYWFhA(eV zBhPx;fj;I$NGCDX_{3eGlzT4fSGT0Svz4f^5$mR#=MD~$B(~d)gHoVN!rikDb5C4ou*e@zGvGVuEaHVN1)o0QS|~_eBh*?&%8F)dgX&vz{mJ z)-XJkJCCRPgOYLT@i_De@%_08-yiNjtcW!{E9ox_XdGV_r{_h_jhq3NJXIione#JG z@sV9gkBOir%@7OC`n0w8jLu7WPyc8SOlGa6$SGD^j`O%{PWAlQ*nBY?#W(o3bAQH& zD+bpZ0N7amamuOTXudh-#Hj*)b?tod`_08|)Wu!;#eLbupVo`NBNq?L7mtS*|9)R! Ma4$~@!2p^60o*?YUjP6A literal 0 HcmV?d00001 diff --git a/docs/source/images/deployment.jpg b/docs/source/images/deployment.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1fa0abca8ab8bf0926742b03e1cc12d1dfeb3108 GIT binary patch literal 62941 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]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 F`yUg%Ftq>x literal 0 HcmV?d00001 diff --git a/docs/source/images/deployment.png b/docs/source/images/deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..6c33f8ee40dc92fc80ee9a7b7d566ed99cb28d7b GIT binary patch literal 13014 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 + +mongodbreplicamysql:3306workerruntime (compute & data cluster)mongodb:27017data scienceclientamqp:5672HTTP:5000rabbitmqmongodbmastermysqlworkeromegawebapp client diff --git a/docs/source/images/deployment.uxf b/docs/source/images/deployment.uxf new file mode 100644 index 00000000..3c72602d --- /dev/null +++ b/docs/source/images/deployment.uxf @@ -0,0 +1,282 @@ + + + 10 + + UMLDeployment + + 250 + 120 + 110 + 50 + + app client + + + + UMLDeployment + + 500 + 110 + 110 + 50 + + omegaweb + + + + UMLDeployment + + 500 + 290 + 110 + 50 + + worker + + + + UMLDeployment + + 630 + 110 + 110 + 50 + + mysql + + + + UMLDeployment + + 500 + 350 + 110 + 50 + + mongodb +master + + + + UMLDeployment + + 500 + 170 + 240 + 50 + + rabbitmq + + + + Relation + + 350 + 200 + 170 + 200 + + lt=-> + + 10.0;10.0;80.0;10.0;80.0;180.0;150.0;180.0 + + + Text + + 390 + 110 + 100 + 30 + + HTTP:5000 + + + + Relation + + 350 + 120 + 170 + 40 + + lt=- + + 10.0;20.0;150.0;20.0 + + + Text + + 390 + 180 + 100 + 30 + + amqp:5672 + + + + UMLDeployment + + 250 + 180 + 110 + 50 + + data science +client + + + + Relation + + 350 + 180 + 170 + 40 + + lt=- + + 10.0;20.0;150.0;20.0 + + + Text + + 360 + 280 + 140 + 30 + + mongodb:27017 + + + + UMLDeployment + + 500 + 230 + 240 + 170 + + runtime +(compute & data cluster) + + + + UMLDeployment + + 630 + 290 + 110 + 50 + + worker + + + + Relation + + 600 + 120 + 50 + 40 + + lt=- + + 10.0;20.0;30.0;20.0 + + + Relation + + 550 + 140 + 30 + 50 + + lt=- + + 10.0;20.0;10.0;30.0 + + + Relation + + 500 + 150 + 30 + 220 + + lt=- + + 10.0;10.0;10.0;200.0 + + + Relation + + 610 + 200 + 30 + 50 + + lt=- + + 10.0;20.0;10.0;30.0 + + + Relation + + 550 + 320 + 30 + 50 + + lt=- + + 10.0;20.0;10.0;30.0 + + + Relation + + 680 + 320 + 30 + 50 + + lt=- + + 10.0;20.0;10.0;30.0 + + + Text + + 580 + 80 + 100 + 30 + + mysql:3306 + + + + UMLDeployment + + 630 + 350 + 110 + 50 + + mongodb +replica + + + + Relation + + 600 + 360 + 50 + 40 + + lt=- + + 30.0;20.0;10.0;20.0 + + diff --git a/docs/source/images/screenshots/dashboard.png b/docs/source/images/screenshots/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..ff12ddf138b70fd09382fd6eef8f8f448bd2ae2e GIT binary patch literal 90122 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 UhO|2r&rsJ#1vUAq4<;f17b7Pl%>V!Z literal 0 HcmV?d00001 diff --git a/docs/source/images/screenshots/notebook.png b/docs/source/images/screenshots/notebook.png new file mode 100644 index 0000000000000000000000000000000000000000..5b255435bbb655da884cce8173f0bcd7264ebba3 GIT binary patch literal 258904 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
pic$CdUwi`}p7~P(ynibG_#GR4P+Fo| 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 z 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 z z*a!EySMgZuN{SGKCfQaM|MRs!^V?GLh@h$-8AV~+=)@3=ouAzx2D0y=lvIihah!A{ z%pUZh9079@LO*h`hlcR6pyj)Z@CSYt1A^249EEV$s3KZ_r>Am{#@g8TNuATj(0}aU zp7PeTcORYv;GN8s8nBO+skQL1HLkr*4|wvLZ}F(W!s?G94mKiS>)9*h?mWj2$aUTx zbZo+VxVadc?+6feKUoaJFPW1Z4)_Rl)E)3mES1p>iXRCg|`V!|>hhR(T(IC*t_UKYZJX&wgruvcOiv(C4rgjYvz=^IIm?UbK!y9EQVx_;rB%O&$ChiYaDj8n5cDOX z#g1U3t~}hcQ6vk~5FlYYq=dhrYd#;7exv>L>mzB?xFGZZWE1cX^JVGYIAE!TqnAGj ztU%M(w{?6z>Ymj^X~8`2bD&{*%}LyYJz|@W4~LoPvUa z`)}0j#b5+#?dU<(D1X;nIFwE>EgP$zKh#=IytZ4c?Mg~Eh#{?LA@^f%L*wV?@2nLU zN(_-I)hNGQ7}RnRa?zX3S|_L$D;f8s?|F0KD~`sqSqpW{9Kul}TV5-&nD|<$DbNkB z;&31pnff_(YJ!rN(qHBdM%{AHkS+R$R6)5>8UFH{e_ag1+62(5Qs%iby|55Cc16KW z*HZ1eNj^j-8U{+X0B*q1{`mn{{z*SdN%Tp(CsMdf#pI!CONp}boT!db?w7Pq>43FH zadmYthiMb`tJSeG1vS;LJw6KBV;?Dla#l~mhPJCsMk4Jv;AQ+pVr|kg(rEWJa6*V! zR=)M2swy*G-uvaCbsurWC8V1+Ofmide9eIgQTM4>qoF(APmv40hRBUbzGGLV^wiYI zygZt9oYX>k;0D6gl2XJEJ1iosb=pJAx9+Y%6MaL!Kq_9=I<0a$IKl=@I?rEe4}W}Y zg|7Amiyx-^ppQ(@Ehpsk+q2>O*SC|_1q#*8bd)0DXZY*H+YZnkVEbbHNldbQ5METH zfGGb3T-u0NiKS$wCsh~|A_GWreL?UYZx2q5{~k`j!o!Q$F4WJk*=$D!{4?~roY7)1 zYG(mbyGa2)#=3pPc zIQ3-~dMLd}SQ7BU8X=iE+fFuGd&Cs^32^!-!q9Eb_$mWLnR5|%?3Q0w%t5^CY!+N> zmh!7WP|vkfN>fwQ&%m*N`h)2l^+C)j?NU^9bgV*07}f{#P$sZ033Z~G{I9;p&p>vN}R zUZR}wBIemr4SOPkxiuFhU-{EI!H*ahH=XKbSeGZms@G%!t{+~is&C&SMi%RH#Jc<3 zhxN+6FL>B3_I@|mLCA%@#hn>b1dinKZv3R%v@^dg=xCqMy|1;NK{>O1#__J+>*fe4=;H=NJV2 z|GusOZi*a8C-8Un$Z*oYM2{})0)eG5B)8e5&Fv;=PdT1b6$u*bm2#g^?v-H`eQi#{ z+<}7>)j40@dDREmF6UpmNWZ9w?=UT7AKcgcs}n^dZRp)2vMEUnD38Uw^FMkzOiD@& z=YzL^AKAkgB@tc+s?&v;u1k4M)ImIiL}vO1ee|JJ8$$}&pN7X!hU-=uGLqo8s(k+HpYi?)rBurp12r+R8HithZ8GMv-)y(&3#T5$3ZUC4yV-Hab)F*3SQ1^Y&qt6X@0*V;L3IV58*^qV5f1OIP23f>UaEDk-3 z7DYdbg+F+?D3+JL`Y-^t#IOJEjtsL@BVu$W1iN3J2s4<$kFTPfE#$g#Q{|ehVBV(&Q^!xqf z{`nTYX4*%2J$ZrCgdW1T-jC5Xa?nb8)CZ>Xl9_S?i!J;u@EiG$AEg0sbfQE{N#Cm% z|5SU$nx;SK^^-8v#4kQauZuvC&X%L{z>JlOL5wG!(h9W z4CMYsMxTv`^!045)FS?j1XL^6_{iEDOjM}+=0MQc7jrw@Rna(u&ucAs9|(WhEUjr? zxW77H4g=z0Hh?pPvt_fm4wqfWTdtTS&e=@v3s{q zlmYZd^`1i>1zM?N)!(Q{c}s{OMt;!U!h3~E zE*mxTtV{(5RU#+a;kR ztz?0`y;FLCNR=Jc$4Fyh>t=F~dHZuCaI>3rj>TAJim(rledD_BRT?F-3KugkrbBer z&#Qpg$NTG-D6+=WpbHMy)3-D<-t!KTl<&;V%{QaZ95w+vkG@P{AIW9}^!@d7c{ZtC zHZrMHkJWGCx2OS~Llv!|fEWT@^x+g1gF+w#aCnWZM!f96CXdxiWY&kT34m{ZMW^`m ziqm|w2Y3^!(6OQ$G`n_8nT{%KVi%?bUQUB+G~*o zG&UoPaR~`Qie!tMv)R14-qIQ}_bxj3t0-0hxp$Y23;BdKckb9`2}Cbv!2h}Wp-7ao zI1**rrBEcjx4V?DM7gdyrlibGS-L$%Y{Wlp-s5S)CvSvmDTL$*4v8X^G2^iP{_$>$ zLdSPQwKNYA+?rz|pZ-T1qIsLWVd80vcBZ4RrlGxi2BfWt4K)Q&oG8L)xy=jiWzYL+Y^Dy;jkjq`urrBM?-@oKPz}b9<$Wy zyY!MXR-~&C9xpV)-tiE`W*`7@zF`_$5~3(?TSwrsn#W=ndS+pQD)q0HJayVsbbM*L z*x7vaphx0>&M}FuhQUbKcV$b2z4IJ@%VoOzW~qUY!uJ82t!3Z3mD5-V7za7j%5SXN z5}zpeQUKLkXIaBi5a9C+x;|d^JSb~$Jc@qgu-jKpWdF+a`}$6AlVf{o?A^n0i)6cw zj!fl7#r8{m0vOr6R}mSzT33nPjLnyKyjT+hd~vWb$c3WzrV1hF^zGLeBr_Ebwa?`( z@o8Cv6Hmb08PN5qh0v-OkU9a28S4C`?qrd`>7$Yj?#jAi;uS?@@E#J8IK zM^0>c&~4%}%0+oJ2}4RE#mx4Ql#`I>5C)|dsi8NaGEW^`yZaSKYT?19@stP&?%mZw zp;LclOI+#}dSqX|Y*=LoHe33O-B=JR8`eE!@}oFBes?6-YQ@H$MgMxaF?AgDP`uUt zQw(YX1wX$?>s{Ysqs#m|PrRx3lkWI-;WgurH+G%u^U&mQ7dF0rtfP@f(cx7109}RJ zgWnj{2$Thcgv)rol<;UC?59MUiwWj~K`~!)@NNyLV={}nvZ(P6j}3g{Gc(ylE;>fE z4872kiZ#s^o%*i79N8>2!i^FjK~eaDh%T zEdx}!U4vSc8-U%!-G0g0n&0(&@RRjSagJYDOydS{pzbW&L3K+Q7UY>Nt!o5PDS z4%hKlRRMj&4)3;hvj0pKCf^JX*~~R_1IAsJwOMWE&wPj-Pfvm(kAKzjlI5xvC#uwZ z0HY&$qRT&$875LY1|@YTGK;D-*Z_Yt$HN&T}02TIh5hVEaEF9`voS-)$-#F4V#M z-KRrZmxPuARym9GXCH1C9ui6s&V{|3EA#>kr4Sk9(i z=+bap74hq7e|1k1AZ$Ws=xddpJ@c`XQ=bia?z#heEZPr&I&>A8-9u#<<@TVWBLr)x zqOc8TT1cLdpnypzGDt5B1Ha!cJ|RPojvodV@ozsMC5>3L?|_GK4@XS{;LiruF5Nw| ztNccr`FHyBxror`y4MP6nN|TxY#k5p^9pEEq3zOl(|I(mf5u^$HA`!s$#HRUE3Idk zt`__K`|&tM%7}|Jd1v;3qYo!p-x+x@m8#VK4Z$uZ5f|21K&}EkT8vK}EVJdVc_i*a z{7~DWy1Oa>v}FHTHP3YG2yY$$lfE>j%1kv4m-R~3QhwPH{X=%Evn=Wz0ctdHQtzva9~W(3);`ijk#e%t17rqS6Y_IVB2e_$GRSsL9gu)I7nPpyJ>k3Na#` zHGGnrGIJt}w&`c?gjbot${r57^^oBn!!!h1F|`3BRplFu$99QnC5t_q+2Sz4Fw9`8 z_E$`rEJhJp{cVAIVXMI*a1HBSsh|mRP8NvDfuLDLbj$Toe8W)vNt%SHX|o8?69Hv^a5>mSxLA73&(XDf_iIa4#N zhT7Z?rmLk^zW+gf33dWxwGFHvSN-M;i2TQdwVOTTv8>WEoalgbJnD^sNEdB@f6rt5 z6a-K7+($}j_l`iH2y_)gCODpLCphs0#6sX@`Q24n2Gp33L|ql;N1i8D?o!|w|7TPg zj{C*M*s&4GhAeh>Dg03?uPSc%w#_nMJ9AM|#8^kR04C67uZ9N9#wEJKx2UN<*RI)t zkd}|E=pJ`OK(A9H}SD8Q^--(je4!DWK+G&itJsPe! zJxJ)F^ioy%@zD*f>DbxUuo*7P?-qU56x#VE_#2T^H~fv$aqK*)?9Z$c%^c?ixFs%6 zhRlE~TC|P}j6`|$DvvSy)~n;N93kQFoBKoT57yX7%VyJM7z7CFt}BNm^*b^-q1o z#b_K{%c>*IHb4F{M)>omp3L`;+vkooV*3;w&!JYF9TKV_VpH{^sNvR6f5{v<5+(=5 ziO5{U(hR=l2e5Qa6W6?@&->ZEFJk#2K%FTKq?bQvi`Q)3>&#-%jK_cLe<yfgW1*$y5`8{GdIqIYKm`N*gm)mcrKcfBlFzme}W z^xgC*BrQpFZKp+BaA@teYHV$%6IQtnB*2T(*ZvrG)_2qJ?tBH?u}MG2ZXIbaede2< zFw^1CZV+afV#SxEgObf;aKQH=ezJKHsYHo4v<0*!ZW_Rd0GTp{WKO4AOVevD(rti7jUQJ62L+g_g_E}3__3vE`sOV{Q+N^Y$V?!h5LE~L= zENi0zi>m90ii%e@6#rUr9D4Y?qR8xty31}lt{3<riE z)_PYvr*KN8LK~WY(@LOs!xe##myF zz&VGGMM$gsh7>^eiEhVXyHNg)naOkb?V#!V_x6TvGf}u(Gx>Vy+_-dOptYQkCnh?bEALjGUqS!utlxB4FPME(jlb$yt31K=d$xD zsx<&Fu?MfzunprYeL%Re$I|efPACXPC!N+7>uP>eiIRgKezD>BXU&*!m|?O$6U+6| z7vI({gdckDwi0zwBsZtIW8=kc7PYp^;@XA&ocHv;7nf-IQ@d{-?(V6!YTQL0dT>uI z!%q9NhN>KYQRGCCE!MB6eKRuI8VFlglq)3TL_rA@$ju0&%Q|P8$Vj_ToTK2iAE1{N$uI(sJC0o5Z9c?(MB}A5o&Z9Vsg> z*9&-aI`$D4f*bG9b%jW+db{C<*r{^`%3BZLual=-EbJPA{{AT(udz^4S7h(^Kc=x= zY==d}AQuDweSei3cXGF)!MwAfL%c$rv8Z*D#H)tfB z_xQm4e5=0lG~s^V>+(H5J|3}&{=b~GC#kM1v0Kg~`OQA#kyP%Rt*^1b#CPnEld;3$ zd>tSEMZWhnv2mD>V^@f}KzG#y5DW$^i zI#a-a0HDdIZ1v{GibR^Iy2+4w>LeSwb1>y=eaJFwtGZsxZXCsf{L(Q4Bsh@(bh45A z>uaxX551a*C=~yNJ`(;+14gZ)iwy5zB0UbH?KWTyU_bLLNtGF1>^L8J?iuJRp+{D^ zy4#J%)@b0D?i#Sms8^rO0J+h zgrW`Nl-^meP7Y~FkL(kRxBf^;2>(69Y;tg=U^$abM!@!j%6%~1lgWb5FGA+V1nmw} z6OjrVUH(o_Pxbv)xxwFW%kPo%+*#cdSAno40r8R~Jv8lr+x&>-a~5ooCUx-4BX9Wk zXlNWYGlO2a5C+*&?C|iLJgA&>4YX{$R*Jb>9KS$=T>eB(R{Y?~;=@K?e=&vIzWLjL zos%U|jwi!5?2$#+p#Dv4dkAx0;diGtk}ib`cq2Dnk7I{FJ^Q2gA&~XLnKHI3s$TDbW1bSBM#nNn?-?V-GEvqjDEtyfE`JgGdTk$e)ei?NScTgPYu#CV$43nq#gbz!_55%b_R9yJqa1Uxqc4}`mr~^{QA_f`_s7fe#Pg{ zTt(J=zcc=sPJhQE&%M2y%xx{+6>k}Y^Ty~;qZ4~JAm5SpomZC7xd>zUd*3(4-IfdK z4&$tePlYlKZuoT)!f}}g&~FbWbpyCkPdlI&#%n+)4P6i&=>=PO1m99qY1es2hokG| zdTXetD%MZe8!fDF?y|0s*PPTSJt}%dmoKu?KQt@T!0$W%6 zzD#z*=keHjP}aEz?T5g09?Qi|A(thkXQf<+!ehmBJ}T^r z1>qg(&*{*jQW+uWviQl4TB0$}rolxy&hlV_Ou())?9D<5Gct01H6JVuiHb^X+_@ut zw_T$Y=d$(cMF4og);fTd9SmdB`?i0Z=XWP7wC#Z}HhB<5z?TM*yEs2>P7e1zIQSzf zaafG0$;chIcB^gv%`jqpamQP(odQ$saT0d&ZB1N4QErY-$F{)=4qw@`fdLe_xTJ%y zp)^pWKmx-mEAg2T>baNih!9O)A_tgl#Pt#6NW+N=m|CO!Pzs!pnGM#}tgVPFCq+pQ z_aDM3ped+yhf}W8XBy1+thAV8`;&=B*_2$NN&t}1*TZLw(WkjXFx^MkETVt?Vh+sF zex%rhd`u)Vo?zTi2-Ef85zIwCZhUkHL>;gxS9^GQ$?VKmtL}@eH!i<$^gWi{h~9f* zrSb?%^h9TH+dwPZ{ee6MM>@e|z0g?yI~{%ZZG6jx>ZWkw7K<^FPw9Nvfa|VFg}{|Z zv0{cU)zyAk19~@LQ6G9ge2oFm{(qZ`*uAfskqlZ~akc?meggnW zu&Hbt$_=EN?K1=cpWO;PGIpe{Nsb8D^lQ|zq$;4lGl zBU&F>b?()Uu6T=#FfX<5qrDL*UOl%V@%#tufWwXyK4Flnf|r#8JQG`kOR2wEuC(%N zmg`60Gi!!h`~_$sMn`?>ovUvRk}&)YSsW4P$B=-|R=`q+-C zsibCyjY=dbFvB{4Ui;Xu;{J8VFKl4itXg^ZeSJMCrxcbQXQ(Qx*cy8s`VZ#IKxo% z=lxt!h-8UZE|_h$pG=FOEejtOimx~!9`xZpsRjk;8E5Z9oVY(M|5yIA*BYRcJL z8cw>3zJ1%$Nr}WS6;W#<)b;R!v02P53$M80pMr#pkr{@hPvu1K0m1`~H$I>PS--gL zxt5%aZf*1|uLi*XiYS*c<4dc_`9`+GKs~0wQgYB-kRNQLt08Ika-nIP}T!Yuy;`Z@I@H4T)+`oz;e5-izn++;~LV=jaWjN19TO$#ai7( z&;rukIFQ>k(W<9ORnY6@m~88Sj``rpK+^JVU`2%jR(3DgbLfpBou_`v&59tv0KY?7-&H*@ zma4baaXp@fiH-GE*S^-Z!f!8_u9n7xU=PJBRJ=+@*`7N4JyZci2^X%(U9AnUx;N3< z)|gvtbj#t!tCS^#-xt^TO2-hz%#Nh3Z^jm`0n`yfOkH#muAhJ{WhD7-)|ih{t*9Wo zQR7=ohv)H5i8bvN;~Bag0a)FDf8dmVHsFKaDSs-U7}`+Y!M@=}*IRJ$c#BmTdtqVn zbPK3j`|z$>eJoc+fb*lw^SK-Iv@dP5%45}D=vU_0)YKG*pp%wCiG`-G$fG+yWdv zsp+qn!+N9m*HdK;6CKqe!=itMwd-8-@;3fww|8$d50}_K_voa?Ldm0_x1pKh--{`V z@@^}#ldA8>*+Q&XGRB8MyZ#Y>vApe*rV@|=isTskM>Jhn5r&E0Krvn^b?i1Zt%aZB zU);5WA+%QBJ9{^m4e)=Qp5V<_n4B+Adq zhK>HLb}2(ENN}8&472Qo1s#l$W0C<4yIU{+nGn@e#4n*9|nlnI( z)DJ9&9;i1cfz#k+`G~&>O7Y7@J_C~Dy>GTQ<@gQ`%j3)lt1-!b-YMwy1jaV# zl)qa|$)?|p3|JhEOfMp3=@S#Y;?0&XlB}z28qVH$RJ#kqrtTj&m z>y?a-$159kvqeW$%$X3=yo1FCLhl+a+4dND zOXM?9zD*7(P<7(6Tj=Iae@~;%grYV`AYAo-7kiYr*xBpyXD4KT61Kkg6!BQ~YRM2) znkZ0UuC-rg+JYIX$D2hz(0C%7o?l(PT^FZ$o&%l*FP>(CSQ<#iG0iy}i$;vPCu=*lO&7Tf%dubgY|XR44D zmYn5909$?pdHsvER9%=#$uI`8O&Z7oAD6Jzi9Tl#-C*cLCZd2tP_)!_2(Sf$5Z@PT zm7?6J*V-?B)ONg!3;%sdnyqJ!VMulXN7I1#6c~eHFe;v`NY?X8K{6(>m2bBdm1AeS zfnzu98^hN_>BagZ@Skpz=B`|1W<&eqg(?@qPrfG-LXY#W8<$b=VhOb_DPHpuYH+zo zrEOscRp;EV;~cFeCz$0ShA8W_c+-@&PVol@p6dox)wyTudm4(<~h55afC zjN(7@_1u{zu*%Cxfxfd0#~5{b{q+Ufq}$C01z0S4vr$6!6{P-8ZXmxMGr=(Z0A!K$ zzigz#iY6^fMf%*ZyoLd+56-Uun~=`t*n+ihGUVc_*01+xZ9<&Q)OwPui~++dr`fQ} z?S+iR%~lKxBr$dKqiyoS=s%?TrsrL0=~1Z~YNvONPDop35k&R87;gqgu&~E*-+JLH zmq{xe3VRYI>2$cr8}+VZEJTh`BAhj+*}=|Nd9fP$tp~*E7%zD@*xtILwH^3-3bWn& z@*xaL{;Je+|1C%rs}pivOB?FE#UQHMOx)0mwdB@Kw4w`QBH-W~LvBE3ZM8jE?-}ei z95vpdFeKAEMT^bGS@CCfsIeRM&A6Ny=@45O)^snAiyQW@>M@zxcI;tVcsI0k0~-DB zPU<17vB+~OCz++O9vS;+^ zN^kgX9dU_sqdm^0^mYjgmQYf6B{bj#?+JgV0;5ztXckw}AUoqY|Fxy8-^W-_?7(c~ zGCtD96Ec{7@7gWSsiKHy2EQ4o>tT6ZJr(UEgJuCs;D0?EHJ_6}NL)wy`Q~UO+bOwD zd7{vR{qO!VX7N@KQ;&3po!=>4^XVzPvuvzNGd8cwzNIBBgXA2Vv&RnN*P0C(g;|`o6S(+Xn2{Y$f%4-UYDR+ z0Y%8P2`2F+E=f(4eMU~Ox@^(nYx!_|hoiv5hkXQxjb*wK|>M*P9qj>m{_bB{8(pRu}>YCg7PImBm zGI)!1V+iFeu|Z>kW9{ys=kqZ8Md~?GceSH@iH)_SU!z-YfmyCWVYy{-9+B7S$(LcU z4(}*`2jWnFbl^s4-k^Dw3&cY0Z!#VYP-g?lM>_n{pFS7rnXx(5+0CTp+pw{e;U=h_ z?l%(ly5T~Q=QSm`e%3`1(fQys`9``-;fZneTAaeh@&BF7BX0(yz&f>h= zF4|u3`W|V`HOo6g$AX;yN+t(b}*DaL|Mc8`=NU07+W)rJhV2j*E&Hg_vD3 zE^sJttMw#qC_lSSwN=53&fN_c!mDe(#yp!U@Dt<&1N|JD3HE(k-MoM^nR`{$In!v) z9)M}&7#;}#J(!2njE0u+iKf5)G(NDIi9{D$gOj1@I5FI#iMp-A(cOHBZr-k zamig@CU+^Y6p4`0UV&RPF@mU=3Z*nY3gV_;kGo3>wQ7zZbQQ2przRDY^ocr)Gk=2( z%d$WqDRrN&Y18`1;?S{0U@uqZecH~2U|wvz9PvGF5WIvg=#E4SiO)fOYcIDA1fN~0H# zx6DLY-@R0?w>FSqqvep}E@PWr(!ihwY;Ewjvg=aru>$_p!rX>d=LS2mH5u0tkFg_T`l-|%2_NMOx&oPN{GWogK^r(b< zCVzfClXN=E1^$K6lOktKk0?;B8yIzwLfTEK#($a=T6_|7KXI(t=I#IHG zn2pNOaBy*}Y_1JCFbOKci%C1{DvdAbSnfib|V9NT{sJ`50To<_$#dsjOzg4!e)+ocAKCa>*C`iBN^HxpzM{d*IZfXT!!Ke5 zkVX=9hkF(#IO2Nl>haZ8O`<0NoY#^dQUzD_#2`w(75xg8em%<^T7&p!Nz zsw@fTao#tjaV#ZFTpQDt%<3F%e#kj3Cj-rxmIkfw)q;g~l{wk#1jK|z2tW?)H0l4a z9mK${R)wfaMN?9q%8VlMm_JXDSMOG?K`PTR;9Plv&%b|Jd67sv&y%mJ`~m%l-XMIP zN(7<9-u!7vWTwdHop42jE$VMgxrFcqj-wKX@#}rP&c2Z@Z*3XNLv#*?I>h=I@axL* z7RnpJ`wea1=ZMMPH5k|)$p7GjFTaLQMr~J(o$u43!e}N3yJ$Vs*^Jo_-)|E}5pld$ z61t>Wvgft$cyu4WxgrH(@x-C{pE3mSV@03%===aSnEi4jr9^1uV_z%gmbhBPCdl6#1E!V(fH;Ln9!K-32E}8=A5l^8i0g3KfU@tr)H@~b!c^0f@3<%czFQWqRT;h8^Ca{k$&`3-dRl_N{y+^wvmu9ah$g!U3F z)^5v`;)M3`^ele4hq2)l*19tSh{^hScFXIwwR`hq#m78%QYZ!r3`1<3>?Nz+np_{V zPryFJ!bE^TzLUj?3vXSZq39`g>hH9+V>Um5Z%EQf8M_bU#|Q@SgkHEA+FdpS-tH@VEz8Nr;lgu!;Mq3ydnoqRXLVN`mz1 z*%~30*N#pfmY$XtG{5YzS;+6wZA~U}Lg*Vqc!8~dQ(Hh4k{zVF<+WZ^%(StFq7+(R zCAA)+O>=SkxflH51GfPwGxm0}oE*ND$H|h0>B`HC=FF&2nG)I!ewRFGbWEiRRSvHU zYLM{I1`)&%$-aRtS?bbE!Z*7l@)!DTu{-;g!I~U1-He1yE~ttTPy39G1@e32I-`bF z>e7HEXqQZsk-?MI)Od=iFRJuW47usXEDWg{3g&Y9Xxy8??`EL%l@#><*pYw$Z2F6j ze+_}6k@G@+pW>Blz!TuuNmgMhInTFV>3#X zKn0#h8e+0p%K@V}O|al>cGjmz+Qzf1Z*hT&bt$OfsvQZNv3J9cV|6N0#PiL_nEG$t zC_0KZyskSSqcpWPzK1Z|%0MYn16br|b66hIxAnxU4~tBEp}#r>_z$pY$)&57pHg8> zWNc$BgBt5ZA5vYIS91oF(vA3-?KrGu@uXE&JBaRcb(;lPRIrac<-(7dgpXZvwA(4< zsahz#pZ@awBT)UPY)CZk5{`vuy=7QcZMXidC?(R}lG2DsNjHK>!=k06ySuwlIt2;oMJ_rQjdbS% zVbR_BPVT**=h=Jz-{Y7c{NMq4&3Vl^t})K@cM3kpf3Tj%^JUcyv2DGb7GQr*Ve{X_ znzUnwJU2pG1&619i~(*a7AZM%`6f6baeIhanI)1nPV)sBcDoSLTNQzkP8}LbYyPzs zJ=hG@T{CF9k`7mi2HCMUG6D7Q?aXE9e_oXUX@NG0=OY~K)~th{#uhH}tsE^wd z7xN(xex^BoTE@M<;EVMF3H<{n^K!ruQ1l?}KJUt*_WBYRnz%bxUDy!XZ2JU2kWlKT z>JE8j0s{-}UrIN0{-!ldcJwBktpAIt@$@xOiL~?QB;%i4Z2Kz_ z^YSyW$<~ZRg8rwI9r#-h;zUW2u%x81pk9#SFHXwHbxP9nErBkmc7+?OtU`?liI4SK zeNeQ?9LcWNx|`dcp>)5}NJN`&=Pq_0fh5Y4<3?gT=>Ps$lw4IY1X^Gv?B9!v{m1d~ z&(gdl7e$-AGIX)nG@Xm^Cz&t=8z&XUUz0BoRXwUjFt;NGFMyH?U^6tWySSRMqr2J; z9gw2Kbg~57cL3vPkP7%$e|bs!KO~S(Pu725WJWUo$IO^JiDPbVrdiWtd2F^H4X{bg z279gyJ)}T?3Cw?M8V~1gdNzkgd0Rq1qnGuznf%mLWm!Ht{8V#V=6KD;=-#q!-G=`i z`2E(UyQFC`EKeG>FKqw!mjt|tKqP!wn{tr(ND8tEaQ+mU%lH=ZG`Em2|9{p-mOO!0 zbmp9az%QQQr@9u}L)IZPDtHhrR+Lm$DXHdF=|?Hywylqp)-2-**aq$c3ty)>n=ybb z;U?Q(A?=n$GV3aq6z%cjnW<;QLa3B(>FWOCY z^ECCf_hk-c+!BL@5bG0<7Etq*tB$L;FN=PwK$Y*6whg02M8aKTFr3nMCA5oG(OFYk z$4SGE-tmR$mR;)wxuv4vi?+HS-r0T4OknQ-hK% zpBezfy25_OUv_;@@dNZc6!z9s^Cpewtkj#S$w47;o)X84KIY! z?x>m{UaFv=w_v=-1Fzt4La`;q6Or)8o?xg)H@$IcrsP8@?N zTTJNdYfEom>JfIW&l}Wxwc-^$Gb;+x2$q(~b-EnqE#?O2|Ev<{|FcSB;aH}u_1!4= z6v6KpwW(8%H8*pUCWV=PbLxRno74yH5kbO8AL=*!}A?S#xgS=m`WWo@%!WJ8;66C&#LsAAu zjy=5qF>%Ks6V^TfK^;pp{;hiAeZe4Xl-nUvHS>{k3FooyOv64)`b#Agq&MPc2&x)L zb8h)Ck*mgiZa7VFp@6H;wDhoS2TPfe1k}Cu*D#e6swU&2a*?o{&%i_e!{`6*(lrRK z9B<8qBgf}pY}{!agD&h`+Sm5OyY{eHKvWopefgB~`9v)^IXO8xqKP$pxVx75e6p6+ zblCIX_=UD47U&k(2y6tm5zh$>$rAK1JX(DU7eb05;|pkTgs+9am0m#5^SC~7+vV3n^EQb=Wq3eua-gxusY~psD)YwR#?6 z_f}Us9s$FgHRxkY(B0i#m=F4^(xikE;&WqmQ^B^B3%;jRjZ5J>nKNc#TOhp|1hJ!1 zR#6$Nzm!xIu=$*JC-f9X9=}=9mLxj%8vVv%f(xwMN{1DRRTVN@#JBSeZJ|`iWZ;LW zm>9yv-ki$bA+~LD0g{@rL3n9uc|0AyP%@@7$+TD`H*1_0H-2lgm(@bei?;itCK>HQ zdse6YIpT&aGAauUvS~`2Ho{a-&g1L16G15hh|99)%xlehaKoNt{X9VS!Jtu2c2ezq zwK;JZm!2M}mwYAyP1`=eD%V- zNionkxZm6=mf3LC>`!B${zjc`HV@?0A5f`W4Mn8hEWK}my9_tyi2GKO-^75{1BV zH!kFH`S$3jNLm6Xb85cQ__bE)Kxg|{W$p%%dVM>&>!N?Mz7HzRp~-ov=8LK|g5K{q zI8U)5fRF_pvR#lKmz*4HW11ZtfzSMkio0VSs%_gE6MPC^Ph+kS1N>+dUPq!)5V(y1 zm&fOzd`lJNx+Dq9-;=u$guBMC?#8aQtP*!aoDmo6tTIQ+RQ&7DPxq5RenHq^kH{a3-w8{1}o>-~2 zbG4<(=Va2PBE(LTTKPh5v3J5wTUfch7^Mu+kdO~d^aGOb@ zd2w12e0w|!tnAaNc4^^NFLbd!<9+-$XhUWYapr$wHZ1as?V9in5SvUNg#a4YMjxQi5>|cRYle0O_!{A1m#j%^1r|}BBrT#X=N*r z=Y;3m$ZW!G$D3|>`ws8o$MmHT4g13ZellqvM+C0|5>{#$o5s zDu^s>t-w1n<-a!H%GY=Z0i3%8CuBr=_L>}M}REA=bVSwE>4h03& ztyGh{ffBO2Uu03N6dxjVA+KAl;_ynl0WSuY)G15ki5R=ln#(&|vi^|ospjU6o$V}W zm9OJO`H)~-mP4(9`>$I4sWPPoopQoXCBYZCFR9@Z#)3}e?EL%-SiD`W!VQRsTcdU4 zs*Dak(Qx>Ls?N-#RE^af&Jz`7N<+v0VMagJCs~*3OW~0%=RAn)GW^%29JYvJ8?%&x zssw16oDT@Q zI%>iCnhk^*;AImm(*vFQ+fBz!)l;C*R!522t5#Ded3+rUHE^upC>}nI!^SJFIE^0-Qo{o z!!5o2+$~#4&1xKZVnW$a6}iF?=!~k5ZBq6+az@%miJ)cdG`7+>+5lDCsR0K6pPT}K z^hE^T43=Sk^NLDu#Pf^4^HX<~Yu7W1T=0nrg8`goM9Z{l?Y8s(VZxvqoD3Bd=2`D@ zUjd+jIQK0qkBcq;fB*!m`6|u7V8m|8;}wem!23%Z<^%~Xwlb!-XqqvrcE**EP&+^Vu)X1Dp_Mb zhvR$Xq379BWcuONt|?XLP6&26gS1AliRrr&OWIC^ALqz~pIDPyFc*b!1U)iBzhpLs zx4y=9Fw#5RI&Bs$Q9*yM4nCP0SNyGRVzt{ z`)pJ#Rq^?G4uk$WqVjZft~g6Hq!keoM!S2>`Q>)Pnf$U_5ZHuo_oV&#_0IxkvdyNM zt7yt>UlMxrJXE6s8(y^fi#*tHc!js+!CZ!6J-AU0ExN^0OJE_X+2t`sjMvloU=o`> zmNfKjHd*vYfzQD^KMef1r^moIJ2!Zi;lNLBjT-U`PhEY?nL(F_7`{{beJm~cfAo`p zPKY(+O0B45!HiAEnHU%YcUBs>0@XZ)G+?N!d=g#gT$NyHyL%i2)c+f7vkEOZC*`oZ zvfEo}TZxGt2>ugFK^BgB*n(E{aTAE(uj!y&*NL&b! z&>>^o3aMuuw}wq$VPZf;Wf@0BY@brZnYdSTK{DE%k7|WFx+ypRiz?}k4!}llx~b4N z@+VZA$FRdei4*>nE$OZiZ|S46q{Ag1*<5-wyU?}RWFlH^xu15km|@*3dtGUuRJ7My zIY6_(!1y}30$sYENvY5JJqHUrEu^ajP_2N6cc z^Z!Nm1h8u1^qnVs4M1p!DYq@qW5pHDO3x1E7!jb+qv{?yh zy2;Ybm@fgl#A&yV9J?3MB}0j|z|wHg@N6ITFd`XRV%us$k8&{xuzbu+$dW>X?AhCb zlJz_oiN15o`buzuQ8M-@@GpXMQrohPU#!;Va@zCbNp&^(|MkZ#k_9{CixJ-P;5|BYe>>%r z!6mmC(iM^T_h*oIA$Gs8i`9Se@N1hz9YbH^V~Dryf3fu)+cp0MH!Pt&t?6*sJ3}c8 z)e8!?V=)Z<%KV9fK}pcC!BU7MW4caEVk+9Y(k8|wP3T7hO1T+I<1M-+@(O!NmA5QI zm3s!fM1rhVMH8Ox%=r?OClnIKl4r2qGwb-35(7>qvOiB;l$GT#xxz+Z9x0yA{SUUC zcp3)%_#JrN@Ge6Fc$O8d1QSmz z@2wEWhi8MrZx}cCRs7R2BgBxDFwEz{YQ?e`f^~bX?7ik5`<*cJdM5wu3 zGfT}Zb-3`I?%6KGF7VBt(M&RtUjDgXU!jqKjxqv}lC-&1N-i$@i@sB}o;IwW4Q!iD z=QA$q5n@mZ1W~r$W6y;;1|~Hq?HmrRSQx}+wG1~R1)J!yw~l@R>LoJISM_02(P|&v zQyZ@Ar^&45Dv9~{+^H)+N;@tF1-u9!9g&QW%kWD>-^sDOtd5IIqAoR}L55?2ahAO- z1un?@0|wIrzUcmun)yS%I{uGoQj+$5jFK7kLfuCBmf5=(7mpS9LG#BdA+1R&9b#=Im5Wgn2^LO0h$`uAYN^ zkpF)do*~anDfaeS%&QHPq5}DtE$MAo9#s+%QRB*0fYdn#QQ+Dybm0o#>l0AEhOWE& zPdH$Bp~b$eV}Nn0XO=n2j%T@@2vT&@eCw6PtstVXy=PV8{UBIhtj6K{;B`fK)1k}I zq$&xabuqVr(eo`MGI;!O>aDhY`c$sLs(gy_D_6d?Do3D&EaJE<*9#l2uUJZA9lt#2 z5Mn9Yh+Xj*@!ir*AakZm?OOor51$!yjr-;xe>j7bW)m5eIHa7vU1<0^rVM!INo;nP zv@_TT7vZg#4sS3ieFYHJw4FMMS-(hE6;w(=_0UabBwe-AwYquQUF&EBd@T_Rsp!G) zorHJOpCOdC`XseTqYH}Sl)&6RwoYk2Hei4K^x;CN&M2~PR}$+j^K;#52;!z;}k3^burB#Z_gxzBIHDXbrZ$x(lQ*1{uES_2I?ar{ca_`IjV%ueDy= zr?5&JE;r7NHh5=lG`d7aJxFpZoX+>7?= z?kMDT9MEez?5a`l>mS@WPFwS&CwgN%)r1M`#p-X`Tzncks zDCxgsgpqq$eWtwm7%Lw=N2xMY3-Xgp_lb^cZ7X_Ez`dN{skb|_BKOxG7Q8?l{&cI z2jOFo%dt?b)z)yJ+0iwB7A>6(WmQ-VO{VlEp;(H}F*0J75`nCmLH1S;JcD=jXtc`g zwoA68jvSO)w?ArTP$d-v&!~05^(JvQze~TGs-HV=S)LUUD_I3qR3O2fQqp>&piNmv^ zhdnWJS8B^(n_rUYC_mO)hEPuL;c|T0WW6I7ztQjA*~OmPpKm}D@_rON+n(+zuics_ zDi1e;p3#MUk3Xv55Pb900Qm1qY2NOS%8#K4j%CCxNzFe5vALTX5jKOz=Q;$BcM7ni z255Z{`p8Vq)v2;n1GLtzc3kuMqj5Dw2T|?MYr@N0tW-?kXW3JSFHm!ne7VHGjPYP7 zDz@|ffh^8-h!0E3f`)rrbi35r$a;kuoLH>L(Dr*(9J`%_-0Js-yJ}fhyq8ZIH- zGPtz6;(P_ZPEU9KU)L8K1`i+&v)lKDc38A)e(ZrAoLL9?^l~rec)rLqIHiwOmXAr} zH7nILfqvP7oKndT!<7IQG_4YhRSs2oY|bJj3$f1|1ht2sZ(gF4@O18bvtc(p+>d-T zz-A_Vd%>iyMzQ~KxZ11BICS}~kIPdGeQ{*?Ecy4q&8u@dB zG-JEhs*_o*vk*9GlhAR} zZbmZ3pC*F)mT)d%wJK0WM@pBLObpoQi~zIhDM*4r7jJ=jnn1)rtMP;%x#=wf6QQ(8 zQ6rNO8k***o`I$-lgCjtC*@nomA4SY;*IPXyTDS&Qs2lx&;pafdz)V$OY~h_xJkIJ z%vS1r0Ogs%Hc46@QcyG2N!H3=lV$Et2F|W|pJkZ_3O$Zz652+~&r_+T_7?Sh!~=T@ zskrD+r`jP z%F%gNy%oExkd)D07Mt(tf9?5*&~L9i8-I~HrZyTVDnl3ue>^YSR+k^orHE~#NYll% z#HjyU9*D+FV?1Kd2zSH5dkWtPQ^^b?0u$InBTRUBVqZ0L?P$7as|Q8wm|V8-P`FQj z$usLgk1Mnf=dE~eXjW;E0w^O@ou}npJF499&5a2^I&BA->ffR;KWO=Pyr>At$O)@5 zmh3__b?vld7kJ@n!RMBsJX)&BX$5M+m=+P6sYOxZU1Y7?LhBEH=J~1Ai=E%K&pFxf zLq&AxPjIC|$ee+$0bDYR$nx)t#c&ZyDcP`nW;Bt9m6E@kiilzz=iuLXV%^@_^cG9)Ifo~e zIF!ZFQT*|WD6j)k1e;4o;!P1R@#@|MI{P$w<$cf$&6{hxVe z?UQ7VU{6)*-_A>1O44fTl6<0adZ#U#U+k9Oc!hLmBE!8by*4PBw=Mxl)moe67y)?E z*(QLru~>5P78=ceM_(Md;7A+1Xm$OjeR#lco!%Hz-D&-s$DB!WN??*-SjEk1pC^<< zZ9qJ6hwFH6LV@T&{yJJi8A=}AYa$3hk*u(|NqqX5S+wS?N#L1|F{}q#flYuijM&8D zIapo+h}DF3x$jHd<4N|crrQROKySoUB(oPiCPc1GlGqmw`{+Y#ta83*q(vYfwkhCs zAimLcKpRWEB&-n4UTzsJaTUg%PVp_)k3E_+ z6c~uEepe9c$Y^jJUGH3;1zVc_4ezwfM$#Pmy&d0c%ot))L_cu7H^B(l50U8QvsgCY z1&5Jt%uU$6E{YtrN+U2|#9qd*Tqsha0i?2c0GyPL!fo|AExkL3#bi+toOqOQzYP7me|f6B?5=Y z>E)=mG|o1Ak$YB95^CUhVJQ~+c&n!nG*elI$QK!x>8-QHN|tKod7(b%pog(>zZo|R zc{YTMW99b-HC_4O%dB<5Opv9q94&?|kXTR7V*!ug&rr58A#n|-@(H=RO<(o<=dg@4m0T^KznFW0^>qsEAfAU zA7q`dv$WVJ1F97@wy_6(lGzJng;K*crRGP^5-84|Va;?QJT6T}ukfvR=T*l=hasv+ z2p#w0CGTAjF^8|K816shB0n84{<*>)1*-V63D4t|#{+Kj${rGY;;}F8m<7zk zc{54yF8sCqA{su)MNI`>hoMI`C@+jpxeE*>s0DBoGHAwjO?y$+-$Wm~S$*~$Ff3O! z){cT38es1#!yNHN?EA$^|5_^z%_$oEF22H!yDN3~dQb|`KCNsda%lhvh3+Tu>;0m7 zV%#S!e4UN6xJL;7FDKL`a09GGP*F-$<-*iHAhIoCIk^ip@2w=?A+ht%=Beb&CM>t z`))-{KqOUL>BW+rpzV|tZj%!jCyC~!a}W1lHyD7hhpw(IZ}pBGxDXsPz$u;xY{HKM z$a>J=(mku&tMjKxnD-4e4b2N6wE$2WbXAWC#L{S{8Xg?)66^m|k0$ASmuuf8MFb=~ z9_#qPD`6>|m+-GZs6f?e3co8`mgj>ckh>v4=DP>(RV=h&eND!T^;|8qcfIp1r7z3C z%?c!0e3K{B8t1TUEhvfkI}%`je^P;1UDk$B$-NVZv-0Iu#1<)&svWJeb_A>k9m%_0 zf1>$0eE+a_GSvYR`>pX3&R0LY%*vnHH0%>Vy!4C@{&WHbI{N7+$|(!AUOw+XorIjN z5;I?P>8`K9i6fk<724?gO*P^U4)3{bmT5P=xm*T1=i2ZqRQ@?V0O@O3mLMMs z85udT%Rvv~lJLFTdTepnR{c8`Z8}G2D!7y-mo1+9^OchiHi3?kjusLkj%IfRD@$36 zaKS9D$HVV6tsif`&~oUGg=kty-IAhV3tuB3gBhsLc$2l%82!97o}bJai9Ek!$qZ1{ zc#nf%6Q31X0O(^H)*uR{pXF5&;&MDCg_u0;`Vvo}91kO`4RJkxE^kcCbW)tUgR!?Y zecVf%1*!#tHIl4Cjt!+G3-_dSQ~Wz|o7>uHv^2>WV!7t3spnPK&KM9Opy!KAI!){zMaGAN2^KgY zb#HU)@!m`|eYf#F)^1|80~k})+g)aMDJm-Z0i5Px9Xt#GW)CDM$p7^|8pWpli~q9a zAr)|SpET+|rFf6#Kc#pZUZ~qC)e%&&Un)}QT(f~I1rF`}tjMBF>ZdS$E9MZXTY{y|g)jD-` z)-(b(n!)%*pDbEoBRIMrRW73{*jenTazXIp(EjA8TCc@j07ag*vJjYhrj9z)lcL$lmc& z-AIo?4M`2A25v^G&Z3doL>w8feQh5xiPbFDb|TBM;PW=~!=7Dlv%RBUgkr2tr`zC8 zLuEOv3^Us&z5Giu5ryZ>=5+9OG*YfNM(BEchh80oP# z`cvF(BT*x${Tkrft>vP5&R-uF_Rr}13WAUpjysUQ(->BtY;-kZgIUCaf@v^eohei6 zUH#vWj`H$`;zULC^s=x_S9}izU%tA7?5)Y!U2|!q=7Nu3H`}jE=}b0NxIJ#BuVYt; zezIQ`DsIhqpEiKJkc8?$3f>@YnC- zl}FCpRb$96@C5S@qdyu5r+u&q@X{9|xW6F9@kRwzxuIHQXp%Fz{vPt?>0;gKU$6j2 zCmybyzWo;V3kB1>CMW%AAmNW^Nx#6y@Nt^w9sG^-Yvb8y?M)8soE{;@mzK|PTd7&5 zf3-RKnhAhn@VMVN^u%CkQ?TewP?vq{8lTJO)1f5IXTEqQ@z*462XW-P{Q5#(wG&O4 z3c6g-1~bFyUxeeM#6F}0Ae6_%ec-VUhmK}CY||-|dG~0LsSdu^Uj02wy6Mlpy~zr5 zch}ezv2B+&>+}7=dThGZyE#h+jeIhxvsN_idaGV-eFYJRebtP3r;ix$>d@9$eJCGc zMl`qc!s?~IZ||d!2Wl*aC!U%vKJ2&&`hq4lJE7$7dTf~-=tcv$^~gHQx5{Kg0zsH? zuK;J{8H33UOtj0tbK!h_pZdhxwYI29r<=t-Pt#6TC=(`MLOv9jk<{cw-}CoL8rbyT zc0^$Yzww&!;!FB|0)iC-T^=Eh^JoVBaup zr@10F%QYw8?~!PkUmXj&d)oIxn(P7M_)l*SOUNrct)hQts@NCj5a$-0FPe8FI`h_5 ztyO(AteP%TdO^x+pn{g{2U%<6SGw&Xc>bY z&YWIp#{y*D<*_pc-F?X2%!XMX_O5xZ$jBFajanmk%MyqQ#X@JU1xl(^s4dw_ntYmsfE0@#GooT4kxZ~Uiou>WTr>+IiAx{`!h?2hLnvu z8{X2etb_RZZ|cSG{QZFeLsq|%`I}9%>ZQr|L0H1Yy&{2m(U`!Cm9qQiZRNT_1p6c1J=UaP=SS`{%6HG4l}KyT zr+WhS+t!+d+oYG+oH-6-uNn0D$l+YVl-qlEX@tgK4{i`akZYc^lBGdCDrOxb8p>>+ zOX4&h+Zg6X-9O5OZxs3bYLGVGT?Ctg&4#Hz*iIt2XCeIcIE@?r-T`Zp0XyJZJzk1$-rL{3-;uo*pX}llZjZ5Gu)B>b_`QO-~F*>~83B-neL`7^a~hLJMME7m=8>4xbW0cIW+(5!@B7*@=@Jx$6Ml zv;22cF6=w1{8mnfJ2%|x=S%Ugf7J_ne2ij-Z-(>%{9-2nPYZJGX^RHLb+EqpE?)Rv z_3<4VdpFAjcjf9ScIh6|Hws*OB5KRcpIyA^@Z&Z@@ew0u^ZA^j`Xvp5$z5f$u}&lLRRoO!lP(4p#36e&SF^X;P!edajxWe64?l*tV7M( zOAJqa99i0}DV;z-FP;&X{1K|H5!Z#`rekAgJ~8sJFH}itf;;#V=N29ux<96v@4!_$ z*GMAtvJ&2|H_4Ffk7sA8eDSRFk6cAcjP9+gc!hQOw28X|%(1BseF4*^cdDgajPHkJ z?q-MrMh=@ruKDo9@rFBoSE`*jn|?Pkr$&==kFPDTnzu zZL{m68!e=HHt>bpV<;xr>RK7L1Mxm}fk4)w3>Cjn2d56}@1lMS%ZqQqK2OS2_?NIl z$ZDBpdS)3%zE1t&60>UAnA1K!$stq}t}O?NGU*Tw7FVX-AH_0W!A2;R;t|@(vh?kN=&LxKVXAkHXRd#5PZaBIbaloqUm05q2PD63 ziCC<1cLO4Pz^(HZubZy8-4H@tpZ}*Co|H)F%)U0J`Ap(Dy$mF9Y1NvUv(!v;$-Gwt zmPv0l(_G@Qg`>rdZhgmJ}*3(n#)T{(fOPco_OeW%f{0IU-DYMI`x!7`dHPE5T zYxS;xq;p-74*&hoO5a|pyJ{y^8zKk60ae*9*l?;{2OWWaNiii(>^_sWP4KV~X8Pa< z4_QwAj_Ywdn1IZzKoJeICeNm#iJI7S2ThSy{d-jnzbWWjF{VU0_DI1dSAGFs$zJzG z#)F;%_NV_l?}K7o3$Bz>`FGTSu$!DtLY_*0=ivx0JNPL7(H%egYO1S#d&`SM(RI9a z12g9fErH&PjxvMUVb`33CM3-FBqvzyP^sX1;^{ISl|9U&=Xeay_l6fDGk0BAGuYr^ zPO0viM*|ecyq&stNB+$-yHHrhXS=39G!3Z?*;*Jj7bAHdIgER-;dN0L8y7dIea>nn z7EeimnpA=X=t$NE6*YlSZ9ugfNPN)DdVcIzvvGe7F?=_Ds1wQqn*Yc%DN=Sef8_z?Non~jt>Zm6RRn6K{^oVd zwe0g~EnwFrv*@~l@|sc@ClCp}v*FW{#3JE;9@(}qmQpU$p%j09n>KtvyvgQsWsmWP zZ+bv(~X6F3|t4S0M9e4c80J4BT6lXlP3lRW}-C%xZBHo>r z)Q9Vt?vw-t#YNP}DX1=LW32M0-TrRSVbkqFQ!sRI$Jr7n8O#Ju`e<^8$QN8uMY;4! zrjaS!o&jiEznIKMDo+00l(VF7jS}Umzp0C-N(1Dzd4A^PJOjjl{sg0Te-#VHFc%Gz z?+1;ubt3`*BH!1;kQdZe8Rf4WK~y~sN-AT<6IT*0Ka7T)PlET0$*^*Z==ByklW05Z zJ|(SMv_*|1*twRXVCwA7l=h~Q|B+azvjzj_EA^3i8Cm&|R%RbAE-Bi78e+Uuz;FLfQln^X#=ar1$78e7TDf`Z_kCn+(XDS?5weQj^hI0@D2`<|xgOJ@wXy20{|zMcBI<55?IFzhUf9&%R`86RO>V>v^H zXjyG<+>1R~s)xJX>cG%9BJOU-F=tePbIvSeOpRPk@Zg8GkQIB%>>Qe$Uo=xf5E9!& z*hYDS%R0anh@)sXZJs&WbQ3IN$VhT&73Mj6Ea~|mQaG@`{{WlrLUVbHAotgWY%iv3 zt~4q&Xwk7$91{|hUBB2SXEvD5)!Z5{WByY-t_GsiXsUcquaFfLgh0qNuq|`qt|TRE zvM=MmaQNQGbQ>1&uesjZueV-49OD5>kylQu?avn-M+6~&;yuXk8aSiD;O?ih0H8G_ zvPePj9Iq#e_=Tj>lPdFb#+}eX<8Jxah5~qL{W3)?(6B;S8A=HUaw|iYoH2cF+%f8` zmSWy!dEgjjQ_uu;FEzPF!c`A;Qo0B&PVa;>Z}@z!z2htW?k}3=w&rV@dt%5VA9yf= z%6~(5(WDpDRJvKqB~7oq*lY{g75xT?6GtH?FBoi|lt=;D--i5?es|c#5B z*8FwBI&X|RwQ(gpc=#1eHa;yzSAF{nCx8_Q#Bou+a=5#4pB8Z18K)@wV(X6%$R&3- zhexOSuBT;K&_M6Ko(!?Fm_SNULI*Y=Aix&Na6w#@NyPi`$vO%ac-Z53*z8BR@{mnc zGk_yi=`Al19lxXSUR|5o%)s&yKt(o`QsrDpzMlBq_POi8yXp8UE;KmZ=-{Ep&buU7 z;AGCm1@fzSw*c_@2jX$5e_)@ZYk~$wR(hByC@l_fKkFL)B63M;Z{XMiMytJbT0*tZW{Ksv6;2p535Y&>}@a{!0;oA$uccWa~5Qq;}VGR zuSE99;oG`g_Xf`3oQ#5BZ-O_nbMrgbPr}w0Fc1Co_^y8Soi2FUp;b+h3EZ!#l(HPW zZ+upY1DyCIgqU}~Za&3hRwm%Y;mP<7YU9%&uQ1=_Y?Yc3q8wq*1BQfv;VzXc+#)7pc&i<8p>Mby*K%f z-V^Z5#2`Iv4Y5iz9DO|9CGJ#724$4@+aWiLViSb;_3t*`U2cGGdX){vn08-zt||C| z)XOaa?$+C7_j~dS;{$BR*8+lszpsVaeA=$()9WNlQ;bH94fcz-dIEMRTW>$@!6Vm$ zwYQoPKD%98E4}3@K_hgkw_XmwtSiHH9tU}Q>Uv?j>@VYwm%H62KJCyiA2@rD+q&*)6gvFnvjC#!p-q4sT+tcymPHYn!@g+Csd~s5Bofg8)UMF zI}_D8@x3Ra3v-zlCt3iVhQ)y9#H;5T<1&vC-FkKELP+ zJ1cI2@Jl!5d$RTYCjWl87^R}&Vnn}w@B%!=Hf~;s&uQQ0ha~mtZ6pkYBWK=AW8_F# zpo2Yr2>-ITcRqx9GbAgkcN@oJdCk7hSEBW3;oahA_^Yv=f*mROO_+Y!kJNS9j&rAk zD?TGT%rBQ}bvzEmRU&&J#LH@vG*91&CZCfcy7DaAJ{oH?rf)-S_41Z_qH2#RU%i_q zIab0!G#4xG`h}*ZMp|Wx8rdZ=4O=X}CqIN_-mJ++=h3|0{FHK z*e)#_jwjo7T6v>{(&nYqXz^-$uUh{iPxSlEth3qMs;Vl*CMj!U)q(bkY9F-J?3m?x z*lE*W&8N^G-eWuf{#**+1bCG|ffklW6c0yKNO#ZQ;D0hxF2JTBvTMWote{{WMC+S- z{|;M-cP1*+P&8cO7*Dy5~vqQUr>lh?XvCdef7q_a7?FCla#uC&7#wab^d;* z>+l$jut$4pAJk%iZqs^2uO1*h$T=i)N42i-*JM626p?)^h(r=0$7{sBLwj1q`Rx`EiRIItD&hyTHv(4-YOIf8By0y zI(YK&35sAVI;;5>Y|}}>z;m@{w=a<6yjR~@s1;F_248on)upkXz?1qGGmqo%LzQZ+ z@MWm>la=yx&{>2kQ2^6-QJktM*+!>h?xu<928|#c5D2~}qSt`1WrL1hdR#g>~4%giNTN&JPA!f(u zl#ROt;zB0}Pw<{HVT^4G7;fm}NO*E}X@Rg4tf!AiDqDytd1)a7~d{Q`&-9t};++=zKtj~VT}#Eufx>koRMJ7iA4WS`v4WZO5v%|ZaWzfLE@O zg0@O8xU9}%S+*U=(CJ1StIbk$1S9^~$r-_w!zMuc+XGLz3=qF+74-&O*1;!mrWx5|7-8NqnhZR{t;}5G%+9@K|z{`^iU#F6lo&8OYb0E zx^zT}N|P=iorK=2RH>mQbRso@&_hCpH|p2t`M!Vu&hNi>&YnHzCcArgXYQSuyF2q4 zh(pNrl*F_!d~IP5s_V{NL^?EN9OHJv0eMC-CJY5DA-`+W{v-Xs~=@me3*i^leOLJLF{_%aw zqjL0Z5Kbc3Tz8GyNlNez?Srsli@`ElYD~FGxMiMZ*?nJ<(hs56hkPM2Blk|v$C!2Y zF{aPN)ABYr;k>MPQMZI&HI@XN_?glF&4T^mERgW(cK(2-l?(QS^!G< zCiFCDRW3AS?~eE8;GsGEHqRF!W}$wopD}h$GGg&m{4aUxLTyZ0F7evcw1GutgKS(| z+7_?0vfbw=1`pW>ayJ-6-owS}|Hxa-&6epPD^Y&T-gG3qFZ;8Cgs+2^HlKv%hlh`j zxWyb7G{-lE}?*^pcTBiFB9u9p57swfQt!P_{=XKHYQprG`?$ zy%M{|n)N7z@OeyZcfh;EVCa+jPC+v)S23TH@qs1MH1mQbPc=0rXqHfL$T~v2w2>4K zMKMdvaomiWs^-vosQcaq-6FeEb)#-`FYCD#C*jLYIew-G8&QwzPL3NNtu>PG94nT7 zN8_^nf@_{Dya-H%<@O{a*+AUnV<0n&Bh1KGX`)%TC5m!7IJeiEUfr{uvozg9cU8c6 zW7+ENq2AvpAG~k)Q77xeB7oED=GdE3{qjD}>1Qt2iQN`)$#y&gXO*FE9AMi;M!SM=&mxWw=YpL5&6_FU|@h%bnglsUxqMC3d|#iav3Ko ztaPiACo7{>sR^5=?mywpG~-eL1d?I#jU zN);IqXno^QJ3V9CQ7L~ZPsP(iY|f-s__P5{x$|48hjU%)+t1i5WN#WJWw#OWef|{D z-AlT&yb>EBO#8_K0Ear3EkD=pGkwy79%)^w#tEc+Fb;pGU$qprs=aW$s@-1ftvhqi z`S+yxt@WK*BSdhk<;i?E(uuC$Dt*3o`|5{VcXF6LYt&;Nh+AjrPs2KXpkIEu?<|-d zS^w&^hh#~I{^BD)-s``tM)#mbZlj!<2me}DffHEu)ALqWP?O0IyD?2XD%_J!^?mu$SDnQ?1r>5L*S~ZKnU7IgbxT9PCWP zlHGu0ao5Kr5zuq)P(yFwtlt~PSTc#M%WxN(;-jZKdwa^=w$&?Kmh@={%!kcUzA2AU(k|1MgxHeT}0A%RiAiI7IA8WD{r`onD@+8Z=c zaz^ITe`|5eK7cpf9BfSE`I4~_=|-vq_4{p9Pir~VqJDuNEh6sTTfKR5aOkI6(POCG zsiAqwaULj~=#!7faWI<=z91@fPe~-KMes7U)}NSGnEw-#D0=i>M2h>jh3*VU{_=FA z73nSNeRFzri7@#WFp#LxZQgHsChegjt5KArUUwf3W7AarE2I&x$8?K^8vh*k+r-> zqb31$6!Y66WK%Fa-FP24r9BI9NenFlG-6RN$ZMOT8RPDayBug37uvk%WN4AxP3*-( z!Lb}~mtnHtksWmBUwtGDo&rRmI|#YmnM-W!JWk0YMWYs7e?Px=wnkZqaenGr?hWaL z%zV5X{HFio_sKeEHhIY-Q7wobX*p2Td?*6)hl3$h6%o<&bkA>Hb~cRwgu{Awbpm}$ z_B1#h|6Ot^Ktu2)o-1QP?UUtZ{ACveQFYr!sE<>ajP?vo7?q*qZ{9?_dXF#o&Ld?p zR;2L3h8Q1)Ikg||#ls_$ek3gnZjLN#lhC40`OHMuxTsO<7ENOX+Of-F(+bC&k8Ny6 z-MpXoQkG(b0zsqxO`J(9orA_Cv5cR0Ur}TMt93p$5&s`U8bR^n7VcY!Pu>y~{j9>J z%Wma)RA^&gO{d>!iY#OJJ6wR>LLxvpw^%QaE0G#nk8)dQpDd?#eN z<@JKDn|6vNbgH%m=>Lk?{m$-E5&E^@!c;Yt0nHgWfsFyL;gCP&c1gq)$A`FoGf6ybC||> z!acp$y#4>&o~YVw8%p{$E)8J=;{N-9ts8tfRSoRC(eA)RhO_Er1j&+NubkmxYYe7Q zT`zzn;w{Uizq%me^Y=`4lx~?@B_1C^ut1+2pDEh02jKqtdG$9y_+`pCJeHnyFv5(t zO055SORUtVfDIlKv+>-BNkC#KU1?9@gWFwWdD){oOGAh4*`+FAN7fjkg;wtTX@r)^ zZR|l-GId_A^8C$G@~Q8Ci=tBUEkMa}0(TFtf73tj6C9?JUo#uYmOI>P+}`>A?ZEkg z0F_~bd$r_1uL;IOnq(@V{_`KdJ2#}xw9`c-uBc?%v`wu8!*A#inGlmQaMk?Az2<#V z$bHI7!HqmM4Sfp@N4IZB%HDjPZcL+mGf@cc*s_v&KfJ3reg3wtd;cq-5+`WBUCjA$ z$Hn}Vh|;T{6NEnzti7}07X!lHnITRVEeV1ckqW`_#*f~tv;A%*@8hV9jsH8p5ub*~ zVcCyNHb3_@10mmmZdF(jo2dkWIBw-8;WlS}RcdY{6`6f!!L6N!TK^-)cw77+u_LhW zI;@nPBorgRbU5`AvoUx(>h~lHN#&GIERO;!f4WhXa>DRS|3k zAqjG+7MJg_;X(`8s=n|us$Qu>aiRV1Mjl!4X~5-^hDtT9nWyl}Crhqvc}g}dk;Sg* z@+Zp?r^JgFn{&ip7%b9zc8xW${K>t`outpM2Qp9)F5?Kqx-{EwFfnAu^6-Bydm=h_ zrKxV*ibk6>gyM?ycey0Z@zDE=fp3HFI9gax-J(=}LKJ7t?&_`$gP&+=8oRO!_TKXa zi8iSlPRzi*=a04?x0afmkMVyul$v1y03Ghoho3syY;GZN{+hP>7z^3coxY(hrwi~H zTDEn~J0as;OlHLV(OK@lG$#g*J3q_YKS=xH)h52=y^EmY)xP7Mr<9y6gody0=4WVF zx00-%xMts?CLm~<$Ldq2hL!P#CSfrM$pUvi$^4itj}vbP3bB`ure?e`(`B=77$0pB zFuMP6rc0;ghMHG3Yjx+9Vs9#;p>C{YG7AzF>-=+P=@;V5TM7JI7~kzw6&Q{pw?>dF)K*c$kg(y`Hhs3l|ikxlV4*&<8ha#tp-PH6hQ^t?Xb zq@+Ph*wmDbj1GMsM(h#+!9J&Y%L3^}Ye(clxbpRxO%IfnD(EYRv9D67Qh1Qbio+(( zQdDnZZdJ@|jJ^)MKwS-S`o#HF!ek!wpwwh9s4sQr@SyafM?}!?*ljSy3lC4w!%Z5h zFK%Unb-|u+*E-&Mkcd#U2{;;=b_DpYosgftpgHf?q!V{}yPv`w9=}R9hW&hC&>TSb z3P+;H87>qNJy$5IW zwKLuAqyz--<_uc_<(4XwPxqm&^zed@n=iE?IaQF`qF0keDZqC|C#9-%^l+Qal~iw? zL%~Sx(B?uRK1dC+ul~py;qnzVne}n=W-hMB(?GoByN5Ze+7*;5RKhocJNAzu@v%&+w`4BlXeXgGjd(0yIw z422{|qu64HPnYJiGfz?B$6X!8%d08bW~=+3t}qXGO77`3A7AW`J-a9vDzM|_gQzPi zewg>`CLGKVGfYMul7seL&wg^W##eNVPcfoO1fatq&bo8{CllC#ZMgN(-U@IPT$r$ZR;7-_Ge0kVxl~$;5vdz(2EF;3(DDAT)uB!^dhEV(MXh>ox3O_V28xlP~SY$OSb)xj?EQb zaWx*t!0pT-_@T4%2oiw$)RPefL7u5y$j}q!pFzXY^)8S!Ri`!L2~hKD=ne`KIqj$X z)Tc-hy0knOeO4q0b_~dHV24EGBC*`bc7urL;b2n3g2Td5E z$HwEv=T$18w?Cx~eW!Y0@g&tYim6hbOS}!JAdC=@8(+UfrKbsVFWrBdvk#z;9amSi z!5$9+k*BVLT&Qur9E|U=z{J`qNuu@oiK?cLPa{O%J2g)kB+EwM=b0z{6MwJ%GR&e& zXLKU|K@A;IEp@Ex+nL6r%YO3|+AOKN0Ub}k4h~$mi448ha%)(zdDe~kt;a%!P0JFb z^z+}WG7O6w&oQYD8||UTV>3Zbuw0&&Ciz(=Jk(az{ktg6ShIV z`di*7jGpU1ImfDY9C`{BV_ND?sg+YCbk%Wm>E||g_L+gdrAx4(=h$*&Ik&fxy18vR zP;D++%O$<{x&NIF{%@@dY5Vp{*XV}boemWVU&zrMv7|+C8l#rmR3G1T&(fYr4ylDx z15#d}aVC9jSQ%jq`Ji%G;CMbi=n{r)QV{6D7)&tr3|Xvqs!k_!pndh<)3>6oO1o`1 ziig{@WT`!C!WLMIl}V$Qd*g&`_Rf2EIpu2a`alqBjw~k^Dz17Ue1Wb)Nm1N zB{QQraO(JMymr=bE{)M!qZ1|IbF&^{^k_hf;51k+(6~xwCuKkYO;fd=WxU4Q)1oYG zrsdB+T(C+B^qp6d+(oegamtt~~!9RGgWY{O{U z39&nJoJ~i+H*IEaWT(QEZ@rlY6NiS3&hKf{go)hO?Y1NV1iVo`%mK0cZkv2T}nBwN;N zfrszT#)l=Q!^)-#w)cDo#rB`9w1aJDM)ODm97cjFsi){SQ7-G?XUw_z{ukoP^Cv&{ zuGuiVF00W0mfma6(VPm{TJv}U0p1(=s&^q>LTcpWCB_I8wQXWCD>j~EAtO|_3z~FQ z0X`F%_iQH2|8iRqCwaMo6mW84p5!PUijo0Y>pE5nrv zQiWK9ne8-Uo8X7_G4)pi9io-N=?w?-jj+)MIXRA;YQCm#>MCN@`}1bIP4%lfYcRY7 zI5MFLLrzcEqv|bNN(aFXDWV?GqVrfrp%(#O&N6Np!qIdB!cra-2Z$ju`eqvS`jhss z=m!6(8y0CQ;@eL8{NuAz0&z#Sct;0MWsPP9CU|A%AW#rF8NU?PtaA2I(`azhtO`{n zcf42Y^qK0+b!N_3lRY&lb#49Pi$cL2c94p}@bosWItxr}7jT4rR_!sD>5k99;06{> z*Ltia0u&W{AhU>1>eGB0!F1tPZpUj@lPlGB@HKp_^7wsbAy}r7Z?KIxprF29ftOyt z>f)Q<=9?cG^wwsIid7dsM5|Q7knZ-Hj^0*!WL(8;{RK368p4c;Q#VC64o6gL8*T(9 zLh1~efv$nrXPSWlOd%iwb9KsrlGG{aJ*j>vnc(kTC7M-hArvfn8<3Cv14t zH}67uaL|@6QCnX>HPX^<$+1;kKRsUPqYSbd}99%8NPCs#NZ7!$YHeDZj$voYN{Z zgKmijYtcRdnL@wWMGzL#mlf`F5<`Y>!fH4pHYwi$18*2fUvXdC7?UzZzKu>I(K~Fp z!X>{s%IrVyXy-2?0|?K1S`)j3?b0w5UY4k?b$stH;0g`%`0=xEL1o}g6BUO_Jlfp; z?I9k`-Ws2#Qj-GTSo#qgE>(k63e$;!ml!r`9e|s(k$m;TCF6KJ(%h%tV?R7~;D|HM zUK&UfSM@xF2B!#|lv(q?M8iGKRzGPryy=yLZFhZ(f>;%sFFg?O8kBF79^EdB*PVVd zsX#RLa&B&J?{RVg-;(|PE%*@GiT`{(G6SI9waI@YYAtxFJInb?i?Mi7WzN&_p%j5< zp7SSPR|kQeN;}0hU`Fp{kz%*+S7@wdr8N4v17O|J^g@mscM2)ilVNBY_{N& zvKZLQ1574iolTlfVrba+R>j6rM?bZ{S>!h>+Z_9SH{-*~L(1??BKk)W$|O&HmV8=( zco^OGvxzA&^XNA^U-?ddHXM3#l+D}`{y11%Wglz!=^n6%=}P@xA&_d1(eF@#x6vk) zDd6}3h$$gY+EeSei9Cr}iXF!)It`OEAC{+t#lq9PoL<$G>!!;=$rSCwP(mUf9A8!2 zgk2KuxT2a;7mI%lU4pOcDQ^VfoR%6_;AnkTZ?Db}AwLr`64R18;UAZ`p8m`o6$bTv z0ZFQc<>Y60r^rWHe+G&#ox(KgrcGi8Pir7_PmQNoTE??z+jXzhucxnBjX>>mWK>+q z(zETPyz0Y*MC=9-(PQyM`L<{{93^@A1)HHbtEr5Q^XgizBgOatCw@02~lGU<3`q^gYz>XD-(RMGT$6|Az|zN462pOdGoqJ^ce>l zkF`4H+1e=QKMq|T@v;66@_!y}#I8rS#Bq1gy9(C~I!qcqs!>OEUB>m&`0&!e+Z;EC+}2dTPVV zSG?*C!@4Dg*3kcG1(!1UQDnWNaUNq-?Gr7`Af)-t40k7Ak*}Vm#hKJs43dwBW#u3u zy|xPw!3@ZwRV5ZSfhOnTpE07nNXmPFhTXQ#44UTH4}4KW$VrnA%C!hlD@PZ@C3O0r zrF&P8)Tf&r2E$DS+pboO)QpwiaiBMgS@DBH$yFDZ8gT2y8_Xm@0`%ta)1`jC%4qClQ z1xrii4(-Z~gl=lG7p^(ohAs}TZH7{#hsA=tb?g%yo9Y$H z4dCWIg$o7_!|ix@KA;PCRc!8I19eRXobv!tBD_#lIM0hvyygL_Oyi$mjR+4SQw+r8 zq!Wtgm!w`^TzlQ6iQJYmPN?|v_;>WKo}7^)4)84vbStZ{TLN(<&=$vk!{2y(x)rSD z^Q@6{ex08@q+ZN!K-hNlkQ3-~Jc2WD)M4CA413@qq0edis_Rgwv8E|KRUzof)cVWD zTvEGc^M{(46y{Q)KuFxp+_cNZkR>g9vu}ZKbRzL};JJCl5;_RQVP|Av6<`I$c}M^#UnV)ET& z6E!=rBiz951pZiPyOF^!>D4 zZxdRm?Ow9q+cG*^w1z zJsJ~Q&CnDyNGG8K7NT+_Awf(0o9$&jiziHYqi&0z*kQzE0xd_qWMwP3E9He%z^cc- z8`U5`jgMLo^3PoG&BDG7UIyh8ZER!Q-AHXaU>VzNq#Ef(cm0;W2iTp^KH!OdVL*GF zSX)fmK33CUTALl*ds1YC5C~BYf5|j^zEYi)UM@VJcM4RM$1k(-v{7W+iYPiag1CfK zwUraW@zQC6kg`T>rpIZ7z>6PO)-1Yyb-1>iSgdKx-Fz*+d|C?U0X7<}E(RMgDrs)w zrZbjaX_EZD7Jm0`qEh-YUI*DLVuh{Naa>S2pbpW^K!%W_jn4u<23*T#k}*z-@H%i7 z`kiYFI|rX;#k9+8pX*J*d^`N0560RaT>E{kHc0Np_7YbsM>ZYaLmk(~J}+xVB;AaU z-h4Y_Ncm5H1OzBX=|AhuU#GphTY^`)D9u(}7z_TizqB6p|Js`D3N?Aqjue93mE=|plERf3f`%WTRD%9)7cuj#NEe3o6dx8<9Ng;q-H0oH z9?yf#MlRxrzcoB7v&0K)>{c=GxSyx;*Nz-!fgwmCHa%gcH1A>k_jnUI&gZ9%`juDy zb6n-X-d4Rt$YY%k|HY;@$L|sm!Wu=U#HRnuSb`1euou_uC_4X)onQS=THMSnnmGU8 zKW*H&TOv*IUqW)1|G(BBY5gB3{_uf-;QtFJa2Gt_r0&pilAK@ND9EYELY}@3`X4&A BkSqWI literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..dbde8d72 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,14 @@ +omega|ml +======== + +*omega|ml is the production and integration platform for data science +that scales from laptop to teams to enterprise. Batteries included.* + +.. toctree:: + :maxdepth: 2 + + admin/index + guide/index + devguide/index + reference/index + screenshots diff --git a/docs/source/nb/omegaml sample code.ipynb b/docs/source/nb/omegaml sample code.ipynb new file mode 100644 index 00000000..c69d6ff9 --- /dev/null +++ b/docs/source/nb/omegaml sample code.ipynb @@ -0,0 +1,2122 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "utf-8\n" + ] + }, + { + "data": { + "text/plain": [ + "'testing3'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys;\n", + "print(sys.getdefaultencoding())\n", + "sys.path.insert(0, '/home/patrick/projects/omegaml')\n", + "import omegaml as om\n", + "om.defaults.OMEGA_USERID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "om.datasets.list()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + ",objid=None,created=2018-01-16 20:07:33.481106,bucket=store,s3file={},collection=store.data_.sample.datastore,name=sample,id=5a5e5b85de39d13b8b0a42a6,kind=pandas.dfrows,uri=None,kind_meta={'columns': {'_idx#0_0': '_idx#0_0', 'x': 'x'}, 'idx_meta': {'names': [None]}, 'dtypes': {'_idx#0_0': 'int64', 'x': 'int64'}},prefix=data/)>" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.DataFrame({'x': range(10)})\n", + "om.datasets.put(df, 'sample')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x y\n", + "2 2 2\n", + "3 3 3\n", + "4 4 4\n", + "5 5 5\n" + ] + } + ], + "source": [ + "print(om.datasets.get('dfx', lazy=True).loc[2:5].value.drop_duplicates())" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'explain': 'specify explain=True',\n", + " 'projection': ['x', 'y'],\n", + " 'query': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}}" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.datasets.get('dfx', lazy=True).query(x__gt=2, x__lt=5).inspect()" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'explain': {'executionStats': {'allPlansExecution': [],\n", + " 'executionStages': {'advanced': 4,\n", + " 'executionTimeMillisEstimate': 0,\n", + " 'inputStage': {'advanced': 4,\n", + " 'direction': 'forward',\n", + " 'docsExamined': 1100,\n", + " 'executionTimeMillisEstimate': 0,\n", + " 'filter': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]},\n", + " 'invalidates': 0,\n", + " 'isEOF': 1,\n", + " 'nReturned': 4,\n", + " 'needTime': 1097,\n", + " 'needYield': 0,\n", + " 'restoreState': 8,\n", + " 'saveState': 8,\n", + " 'stage': 'COLLSCAN',\n", + " 'works': 1102},\n", + " 'invalidates': 0,\n", + " 'isEOF': 1,\n", + " 'nReturned': 4,\n", + " 'needTime': 1097,\n", + " 'needYield': 0,\n", + " 'restoreState': 8,\n", + " 'saveState': 8,\n", + " 'stage': 'PROJECTION',\n", + " 'transformBy': {'_idx#0_0': 1, 'x': 1, 'y': 1},\n", + " 'works': 1102},\n", + " 'executionSuccess': True,\n", + " 'executionTimeMillis': 1,\n", + " 'nReturned': 4,\n", + " 'totalDocsExamined': 1100,\n", + " 'totalKeysExamined': 0},\n", + " 'ok': 1.0,\n", + " 'queryPlanner': {'indexFilterSet': False,\n", + " 'namespace': 'testing3.omegaml.data_.dfx.datastore',\n", + " 'parsedQuery': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]},\n", + " 'plannerVersion': 1,\n", + " 'rejectedPlans': [],\n", + " 'winningPlan': {'inputStage': {'direction': 'forward',\n", + " 'filter': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]},\n", + " 'stage': 'COLLSCAN'},\n", + " 'stage': 'PROJECTION',\n", + " 'transformBy': {'_idx#0_0': 1, 'x': 1, 'y': 1}}},\n", + " 'serverInfo': {'gitVersion': '22ec9e93b40c85fc7cae7d56e7d6a02fd811088c',\n", + " 'host': 'c24ade3fa980',\n", + " 'port': 27017,\n", + " 'version': '3.2.9'}},\n", + " 'projection': ['x', 'y'],\n", + " 'query': {'$and': [{'x': {'$lt': 5}}, {'x': {'$gt': 2}}]}}" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.datasets.get('dfx', lazy=True).query(x__gt=2, x__lt=5).inspect(explain=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x y\n", + "999 999 999\n", + "998 998 998\n", + "997 997 997\n", + "996 996 996\n", + "995 995 995\n" + ] + } + ], + "source": [ + "print(om.datasets.get('dfx', lazy=True).sort(['-x', '+y']).head(5).value)" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x y\n", + "5 5 5\n", + "6 6 6\n", + "7 7 7\n" + ] + } + ], + "source": [ + "print(om.datasets.get('dfx', lazy=True).skip(5).head(3).value)" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "140341971523792\n", + "140341971587648\n", + "140341971523792\n", + " x y\n", + "3 3 3\n", + "4 4 4\n", + "3 3 3\n", + "4 4 4\n" + ] + } + ], + "source": [ + "mdf = om.datasets.get('dfx', lazy=True)\n", + "print(id(mdf))\n", + "print(id(mdf.query(x__gt=2, x__lt=5)))\n", + "print(id(mdf.query_inplace(x__gt=2, x__lt=5)))\n", + "print(mdf.value)" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x y\n", + "0 0 0\n", + "1 1 1\n", + "2 2 2\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "dfl = pd.DataFrame({'x': range(3)})\n", + "dfr = pd.DataFrame({'x': range(3), 'y': range(3)})\n", + "om.datasets.put(dfl, 'dfxl', append=False)\n", + "om.datasets.put(dfr, 'dfxr', append=False)\n", + "mdfl = om.datasets.get('dfxl', lazy=True)\n", + "mdfr = om.datasets.get('dfxr', lazy=True)\n", + "print(mdfl.merge(mdfr, on='x').value)" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'x' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mmdf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mom\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatasets\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgetl\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'dfx'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmdf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgroupby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'x'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0magg\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msum\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mhead\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m5\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'x' is not defined" + ] + } + ], + "source": [ + "mdf = om.datasets.getl('dfx')\n", + "print(mdf.groupby('x').x.mean().head(5))" + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x_sum y_mean\n", + "x \n", + "0 0 0.0\n", + "1 2 1.0\n", + "2 4 2.0\n", + "3 6 3.0\n", + "4 8 4.0\n" + ] + } + ], + "source": [ + "mdf = om.datasets.getl('dfx')\n", + "print(mdf.groupby('x').agg(dict(x='sum', y='mean')).head(5))" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x_std y_avg\n", + "x \n", + "0 0.0 0.0\n", + "1 0.0 1.0\n", + "2 0.0 2.0\n", + "3 0.0 3.0\n", + "4 0.0 4.0\n" + ] + } + ], + "source": [ + "mdf = om.datasets.getl('dfx')\n", + "print(mdf.groupby('x').agg(dict(x='std', y='avg')).head(5))" + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " x_first y_avg\n", + "x \n", + "0 0 0.0\n", + "1 1 1.0\n", + "2 2 2.0\n", + "3 3 3.0\n", + "4 4 4.0\n" + ] + } + ], + "source": [ + "mdf = om.datasets.getl('dfx')\n", + "print(mdf.groupby('x').agg(dict(x='first', y='avg')).head(5))" + ] + }, + { + "cell_type": "code", + "execution_count": 143, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2 Bern\n", + "3 Zurich\n", + "1 Geneva\n", + "0 New York\n", + "Name: place, dtype: object\n" + ] + } + ], + "source": [ + "from omegaml.store.queryops import GeoJSON\n", + "locations = [{'location': {'coordinates': [-74.0059413, 40.7127837],\n", + " 'type': 'Point'},\n", + " 'place': 'New York'},\n", + " {'location': {'coordinates': [6.1431577, 46.2043907],\n", + " 'type': 'Point'},\n", + " 'place': 'Geneva'},\n", + " {'location': {'coordinates': [7.4474468, 46.9479739],\n", + " 'type': 'Point'},\n", + " 'place': 'Bern'},\n", + " {'location': {'coordinates': [8.541694, 47.3768866],\n", + " 'type': 'Point'},\n", + " 'place': 'Zurich'}]\n", + "geodf = pd.DataFrame(locations)\n", + "geodf['location'] = geodf.location.apply(lambda v: GeoJSON(v))\n", + "om.datasets.put(geodf, 'geosample', append=False, index='@location')\n", + "r = om.datasets.getl('geosample', location__near=dict(location=(7.4474468, 46.9479739))).value\n", + "print(r['place'])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + ",kind_meta={},prefix=models/,collection=None,objid=None,bucket=omegaml)>" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sklearn.linear_model import LinearRegression\n", + "import pandas as pd\n", + "import omegaml as om\n", + "\n", + "df = pd.DataFrame(dict(x=range(10), y=range(20,30)))\n", + "clf = LinearRegression()\n", + "clf.fit(df[['x']], df[['y']])\n", + "clf.predict(df[['x']])\n", + "\n", + "om.models.put(clf, 'lrmodel')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.models.get('lrmodel')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "> /home/patrick/projects/omegaml/omegaml/runtime/runtime.py(69)auth()\n", + "-> if self._auth is None:\n", + "(Pdb) self._auth\n", + "(Pdb) q\n" + ] + }, + { + "ename": "BdbQuit", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mBdbQuit\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mom\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mruntime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'lrmodel'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpredict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'x'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;31m#result.get()\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/projects/omegaml/omegaml/runtime/proxy.py\u001b[0m in \u001b[0;36mpredict\u001b[0;34m(self, Xpath_or_data, rName, **kwargs)\u001b[0m\n\u001b[1;32m 159\u001b[0m \u001b[0mXname\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_ensure_data_is_stored\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mXpath_or_data\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 160\u001b[0m return omega_predict.delay(self.modelname, Xname, rName=rName,\n\u001b[0;32m--> 161\u001b[0;31m \u001b[0mauth\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mruntime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mauth\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 162\u001b[0m pure_python=self.pure_python, **kwargs)\n\u001b[1;32m 163\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/projects/omegaml/omegaml/runtime/runtime.py\u001b[0m in \u001b[0;36mauth\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 67\u001b[0m \"\"\"\n\u001b[1;32m 68\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m;\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_trace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 69\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_auth\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 70\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 71\u001b[0m kwargs = dict(userid=getattr(defaults, 'OMEGA_USERID'),\n", + "\u001b[0;32m~/projects/omegaml/omegaml/runtime/runtime.py\u001b[0m in \u001b[0;36mauth\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 67\u001b[0m \"\"\"\n\u001b[1;32m 68\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m;\u001b[0m \u001b[0mpdb\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_trace\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 69\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_auth\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 70\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 71\u001b[0m kwargs = dict(userid=getattr(defaults, 'OMEGA_USERID'),\n", + "\u001b[0;32m/usr/local/anaconda/envs/omegamlpy3/lib/python3.5/bdb.py\u001b[0m in \u001b[0;36mtrace_dispatch\u001b[0;34m(self, frame, event, arg)\u001b[0m\n\u001b[1;32m 46\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;31m# None\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 47\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mevent\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'line'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 48\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdispatch_line\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 49\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mevent\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'call'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 50\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdispatch_call\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/anaconda/envs/omegamlpy3/lib/python3.5/bdb.py\u001b[0m in \u001b[0;36mdispatch_line\u001b[0;34m(self, frame)\u001b[0m\n\u001b[1;32m 65\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstop_here\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbreak_here\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 66\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muser_line\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 67\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mquitting\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mBdbQuit\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 68\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrace_dispatch\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 69\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mBdbQuit\u001b[0m: " + ] + } + ], + "source": [ + "result = om.runtime.model('lrmodel').predict(df[['x']])\n", + "#result.get()\n", + "result.get()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['lrmodel']" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.models.list()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/patrick/projects/omegaml/omegaml/store/base.py:342: UserWarning: lrmodelX already exists, will append rows\n", + " warn('%s already exists, will append rows' % name)\n" + ] + }, + { + "data": { + "text/plain": [ + "array([[ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.],\n", + " [ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.]])" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys;\n", + "sys.path.insert(0, '/home/patrick/projects/omegaml')\n", + "\n", + "\n", + "import omegaml as om \n", + "import pandas as pd \n", + "\n", + "om.models.get('lrmodel')\n", + "\n", + "df = pd.DataFrame(dict(x=range(70,80)))\n", + "om.datasets.put(df, 'lrmodelX')\n", + "\n", + "result = om.runtime.model('lrmodel').predict('lrmodelX')\n", + "result.get()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.runtime.model('lrmodel').pure_python" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'error': 'settings dump is disabled'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import sys;\n", + "sys.path.insert(0, '/home/patrick/projects/omegaml')\n", + "\n", + "\n", + "import omegaml as om \n", + "import pandas as pd \n", + "om.runtime.settings()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "ename": "AttributeError", + "evalue": "module 'omegaml.defaults' has no attribute 'OMEGA_APIKIEY'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m kwargs = dict(userid=getattr(om.defaults, 'OMEGA_USERID'),\n\u001b[0;32m----> 3\u001b[0;31m apikey=getattr(om.defaults, 'OMEGA_APIKIEY'))\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0mOmegaRuntimeAuthentication\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: module 'omegaml.defaults' has no attribute 'OMEGA_APIKIEY'" + ] + } + ], + "source": [ + "\n", + "kwargs = dict(userid=getattr(om.defaults, 'OMEGA_USERID'),\n", + " apikey=getattr(om.defaults, 'OMEGA_APIKIEY'))\n", + "OmegaRuntimeAuthentication(**kwargs)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 161, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)" + ] + }, + "execution_count": 161, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.models.get('lrmodel')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[ 90.],\n", + " [ 91.],\n", + " [ 92.],\n", + " [ 93.],\n", + " [ 94.],\n", + " [ 95.],\n", + " [ 96.],\n", + " [ 97.],\n", + " [ 98.],\n", + " [ 99.]])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "clf = om.models.get('lrmodel')\n", + "df = pd.DataFrame(dict(x=range(70,80)))\n", + "clf.predict(df[['x']])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/patrick/projects/omegaml/omegaml/store/base.py:342: UserWarning: largeX already exists, will append rows\n", + " warn('%s already exists, will append rows' % name)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 575 ms, sys: 217 ms, total: 792 ms\n", + "Wall time: 33.6 s\n" + ] + }, + { + "data": { + "text/plain": [ + ",kind_meta={'columns': {'y': 'y', 'x': 'x', '_idx#0_0': '_idx#0_0'}, 'dtypes': {'y': 'int64', 'x': 'int64', '_idx#0_0': 'int64'}, 'idx_meta': {'names': [None]}},objid=None)>" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.DataFrame({'x': range(int(1e6))})\n", + "df['y'] = df['x']\n", + "\n", + "%time om.datasets.put(df, 'largeX')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1.57 s, sys: 118 ms, total: 1.69 s\n", + "Wall time: 1.7 s\n" + ] + }, + { + "data": { + "text/html": [ + "

\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
000
111
222
333
444
555
666
777
888
999
101010
111111
121212
131313
141414
151515
161616
171717
181818
191919
202020
212121
222222
232323
242424
252525
262626
272727
282828
292929
.........
999970999970999970
999971999971999971
999972999972999972
999973999973999973
999974999974999974
999975999975999975
999976999976999976
999977999977999977
999978999978999978
999979999979999979
999980999980999980
999981999981999981
999982999982999982
999983999983999983
999984999984999984
999985999985999985
999986999986999986
999987999987999987
999988999988999988
999989999989999989
999990999990999990
999991999991999991
999992999992999992
999993999993999993
999994999994999994
999995999995999995
999996999996999996
999997999997999997
999998999998999998
999999999999999999
\n", + "

1000000 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " x y\n", + "0 0 0\n", + "1 1 1\n", + "2 2 2\n", + "3 3 3\n", + "4 4 4\n", + "5 5 5\n", + "6 6 6\n", + "7 7 7\n", + "8 8 8\n", + "9 9 9\n", + "10 10 10\n", + "11 11 11\n", + "12 12 12\n", + "13 13 13\n", + "14 14 14\n", + "15 15 15\n", + "16 16 16\n", + "17 17 17\n", + "18 18 18\n", + "19 19 19\n", + "20 20 20\n", + "21 21 21\n", + "22 22 22\n", + "23 23 23\n", + "24 24 24\n", + "25 25 25\n", + "26 26 26\n", + "27 27 27\n", + "28 28 28\n", + "29 29 29\n", + "... ... ...\n", + "999970 999970 999970\n", + "999971 999971 999971\n", + "999972 999972 999972\n", + "999973 999973 999973\n", + "999974 999974 999974\n", + "999975 999975 999975\n", + "999976 999976 999976\n", + "999977 999977 999977\n", + "999978 999978 999978\n", + "999979 999979 999979\n", + "999980 999980 999980\n", + "999981 999981 999981\n", + "999982 999982 999982\n", + "999983 999983 999983\n", + "999984 999984 999984\n", + "999985 999985 999985\n", + "999986 999986 999986\n", + "999987 999987 999987\n", + "999988 999988 999988\n", + "999989 999989 999989\n", + "999990 999990 999990\n", + "999991 999991 999991\n", + "999992 999992 999992\n", + "999993 999993 999993\n", + "999994 999994 999994\n", + "999995 999995 999995\n", + "999996 999996 999996\n", + "999997 999997 999997\n", + "999998 999998 999998\n", + "999999 999999 999999\n", + "\n", + "[1000000 rows x 2 columns]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time df.from_csv('/tmp/large.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 10.4 s, sys: 466 ms, total: 10.9 s\n", + "Wall time: 11.8 s\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
000
111
222
333
444
555
666
777
888
999
101010
111111
121212
131313
141414
151515
161616
171717
181818
191919
202020
212121
222222
232323
242424
252525
262626
272727
282828
292929
.........
999970999970999970
999971999971999971
999972999972999972
999973999973999973
999974999974999974
999975999975999975
999976999976999976
999977999977999977
999978999978999978
999979999979999979
999980999980999980
999981999981999981
999982999982999982
999983999983999983
999984999984999984
999985999985999985
999986999986999986
999987999987999987
999988999988999988
999989999989999989
999990999990999990
999991999991999991
999992999992999992
999993999993999993
999994999994999994
999995999995999995
999996999996999996
999997999997999997
999998999998999998
999999999999999999
\n", + "

1000000 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " x y\n", + "0 0 0\n", + "1 1 1\n", + "2 2 2\n", + "3 3 3\n", + "4 4 4\n", + "5 5 5\n", + "6 6 6\n", + "7 7 7\n", + "8 8 8\n", + "9 9 9\n", + "10 10 10\n", + "11 11 11\n", + "12 12 12\n", + "13 13 13\n", + "14 14 14\n", + "15 15 15\n", + "16 16 16\n", + "17 17 17\n", + "18 18 18\n", + "19 19 19\n", + "20 20 20\n", + "21 21 21\n", + "22 22 22\n", + "23 23 23\n", + "24 24 24\n", + "25 25 25\n", + "26 26 26\n", + "27 27 27\n", + "28 28 28\n", + "29 29 29\n", + "... ... ...\n", + "999970 999970 999970\n", + "999971 999971 999971\n", + "999972 999972 999972\n", + "999973 999973 999973\n", + "999974 999974 999974\n", + "999975 999975 999975\n", + "999976 999976 999976\n", + "999977 999977 999977\n", + "999978 999978 999978\n", + "999979 999979 999979\n", + "999980 999980 999980\n", + "999981 999981 999981\n", + "999982 999982 999982\n", + "999983 999983 999983\n", + "999984 999984 999984\n", + "999985 999985 999985\n", + "999986 999986 999986\n", + "999987 999987 999987\n", + "999988 999988 999988\n", + "999989 999989 999989\n", + "999990 999990 999990\n", + "999991 999991 999991\n", + "999992 999992 999992\n", + "999993 999993 999993\n", + "999994 999994 999994\n", + "999995 999995 999995\n", + "999996 999996 999996\n", + "999997 999997 999997\n", + "999998 999998 999998\n", + "999999 999999 999999\n", + "\n", + "[1000000 rows x 2 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time om.datasets.getl('largeX').head(int(1000000)).value" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/patrick/projects/omegaml/omegaml/store/base.py:342: UserWarning: largeX already exists, will append rows\n", + " warn('%s already exists, will append rows' % name)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 37.4 s, sys: 567 ms, total: 37.9 s\n", + "Wall time: 55 s\n" + ] + }, + { + "data": { + "text/plain": [ + ",kind_meta={'dtypes': {'x': 'int64', '_idx#0_0': 'int64', 'y': 'int64'}, 'columns': {'x': 'x', '_idx#0_0': '_idx#0_0', 'y': 'y'}, 'idx_meta': {'names': [None]}},prefix=data/,collection=omegaml.data_.largeX.datastore,objid=None,bucket=omegaml)>" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.DataFrame({'x': range(int(1e6))})\n", + "df['y'] = df['x']\n", + "\n", + "%time om.datasets.put(df, 'largeX')" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 33.7 s, sys: 639 ms, total: 34.3 s\n", + "Wall time: 52.5 s\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection = om.datasets.collection('largeX')\n", + "%time collection.insert_many(df.astype('O').to_dict(orient='records'))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 16.9 s, sys: 118 ms, total: 17.1 s\n", + "Wall time: 25 s\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "collection = om.datasets.collection('largeX')\n", + "\n", + "df = pd.DataFrame({'x': range(int(500000))})\n", + "df['y'] = df['x']\n", + "\n", + "def dfchunker(df, size=10000):\n", + " return (df[pos:pos + size] for pos in range(0, len(df), size))\n", + "\n", + "def insert_as_chunks(df):\n", + " for sdf in dfchunker(df):\n", + " sdf = sdf.astype('O')\n", + " collection.insert_many(sdf.to_dict(orient='records'))\n", + " \n", + "%time insert_as_chunks(df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 15s, sys: 11.5 s, total: 1min 27s\n", + "Wall time: 1min 20s\n" + ] + } + ], + "source": [ + "from multiprocessing.dummy import Pool\n", + "\n", + "def dfchunker(df, size=1000):\n", + " return (df[pos:pos + size].copy() for pos in range(0, len(df), size))\n", + "\n", + "def insert_chunk(sdf):\n", + " collection.insert_many(sdf.to_dict(orient='records'))\n", + " \n", + "def insert_threaded(df):\n", + " pool = Pool(5)\n", + " pool.map(insert_chunk, dfchunker(df))\n", + " \n", + "%time insert_threaded(df.astype('O'))" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2min 22s, sys: 748 ms, total: 2min 23s\n", + "Wall time: 2min 41s\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time collection.insert_many((row.to_dict() for i, row in df.astype('O').iterrows()))" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "utf-8\n", + "CPU times: user 1.51 s, sys: 618 ms, total: 2.12 s\n", + "Wall time: 2min 47s\n" + ] + } + ], + "source": [ + "import sys;\n", + "print(sys.getdefaultencoding())\n", + "sys.path.insert(0, '/home/patrick/projects/omegaml')\n", + "\n", + "import omegaml as om \n", + "import pandas as pd\n", + "\n", + "from multiprocessing import Pool\n", + "from itertools import repeat\n", + "from pymongo import MongoClient\n", + "\n", + "def dfchunker(df, size=10000):\n", + " return (df[pos:pos + size].copy() for pos in range(0, len(df), size))\n", + "\n", + "def insert_chunk(job):\n", + " sdf, mongo_url, collection_name = job\n", + " collection = MongoClient(mongo_url).get_database()[collection_name]\n", + " collection.insert_many(sdf.to_dict(orient='records'))\n", + " \n", + "def fast_insert(df, omstore, name):\n", + " \"\"\"\n", + " fast insert of dataframe to mongodb, depending on size use single-process or multiprocessing\n", + " \n", + " Note this can max out \n", + " \n", + " :param df: the dataframe to store\n", + " :param omstore: the OmegaStore to use. will be used to get the mongo_url\n", + " :param name: the name in OmegaStore to use. will be used to get the collection name\n", + " \"\"\"\n", + " mongo_url = omstore.mongo_url\n", + " collection_name = omstore.collection(name).full_name\n", + " if len(df) * len(df.columns) > 20000:\n", + " pool = Pool(5)\n", + " jobs = zip(dfchunker(df), repeat(mongo_url), repeat(collection_name))\n", + " pool.map(insert_chunk, (job for job in jobs))\n", + " else:\n", + " collection = MongoClient(mongo_url).get_database()[collection_name]\n", + " collection.insert_many(df.to_dict(orient='records'))\n", + " \n", + " \n", + "if __name__ == '__main__':\n", + " df = pd.DataFrame({'x': range(int(5000000))})\n", + " %time fast_insert(df.astype('O'), om.datasets, 'superLargeX')\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'testing3.omegaml.data_.largeX.datastore'" + ] + }, + "execution_count": 107, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection.full_name" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'mongodb://9BQ9q33UzLfrxSP6nRcsAHDfQycCMxFQxE76:azpyAq44mPdyhTAYdfA8UnaJS7pXjdWRDmer@dokku.me:27017/testing3'" + ] + }, + "execution_count": 98, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "om.datasets.mongo_url" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.4" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/source/reference/developer.rst b/docs/source/reference/developer.rst new file mode 100644 index 00000000..db0c953a --- /dev/null +++ b/docs/source/reference/developer.rst @@ -0,0 +1,73 @@ +Developer API +============= + +omega|ml +-------- + +.. automodule:: omegaml + :members: Omega + + +omegaml.store +------------- + +.. automodule:: omegaml.store.base + +.. autoclass:: omegaml.store.base.OmegaStore + + +omegaml.backends +---------------- + +.. autoclass:: omegaml.backends.basedata.BaseDataBackend + +.. autoclass:: omegaml.backends.basemodel.BaseModelBackend + +.. autoclass:: omegaml.documents.Metadata + + +omegaml.mixins +----------------------- + +.. autoclass:: omegaml.mixins.store.ProjectedMixin +.. autoclass:: omegaml.mixins.mdf.FilterOpsMixin +.. autoclass:: omegaml.mixins.mdf.ApplyMixin +.. autoclass:: omegaml.mixins.mdf.ApplyArithmetics + :special-members: __mul__ + :private-members: __mul__ + +.. autoclass:: omegaml.mixins.mdf.ApplyDateTime +.. autoclass:: omegaml.mixins.mdf.ApplyString +.. autoclass:: omegaml.mixins.mdf.ApplyAccumulators + + +omegaml.runtime +--------------- + +.. autoclass:: omegaml.runtime.OmegaRuntime + +.. autoclass:: omegaml.runtime.OmegaModelProxy + +.. autoclass:: omegaml.runtime.OmegaJobProxy + +.. autoclass:: omegaml.runtime.OmegaRuntimeDask + + +omegaml.documents +----------------- + +.. autoclass:: omegaml.documents.Metadata + + +omegaml.jobs +------------ + +.. autoclass:: omegaml.jobs.OmegaJobs + + +omegajobs +--------- + +.. autoclass:: omegaml.notebook.omegacontentsmgr.OmegaStoreContentsManager + + diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst new file mode 100644 index 00000000..00395896 --- /dev/null +++ b/docs/source/reference/index.rst @@ -0,0 +1,10 @@ +Reference +========= + +.. toctree:: + :maxdepth: 3 + + public + developer + + diff --git a/docs/source/reference/public.rst b/docs/source/reference/public.rst new file mode 100644 index 00000000..9ba4c09b --- /dev/null +++ b/docs/source/reference/public.rst @@ -0,0 +1,109 @@ +Public API +========== + +Python API (overview) +--------------------- + +.. autosummary:: + + omegaml.datasets + omegaml.models + omegaml.runtime + omegaml.jobs + omegaml.scripts + + omegaml.store.base.OmegaStore + omegaml.runtime.OmegaRuntime + omegaml.runtime.OmegaRuntimeDask + omegaml.runtime.OmegaModelProxy + omegaml.runtime.OmegaJobProxy + omegaml.jobs.OmegaJobs + + omegaml.mdataframe.MDataFrame + omegaml.mdataframe.MGrouper + omegaml.mdataframe.MLocIndexer + omegaml.mdataframe.MPosIndexer + +Python API +---------- + +omega|ml ++++++++ + +.. autodata:: omegaml.datasets + :annotation: - storage for data + +.. autodata:: omegaml.models + :annotation: - storage for models + +.. autodata:: omegaml.runtime + :annotation: - the cluster runtime API + +.. autodata:: omegaml.jobs + :annotation: - the lambda compute service + + +omegaml.store ++++++++++++++ + +.. autoclass:: omegaml.store.base.OmegaStore + :members: list,get,getl,put,drop + :noindex: + + +omegaml.runtime ++++++++++++++++ + +.. autoclass:: omegaml.runtime.OmegaRuntime + :members: model + +.. autoclass:: omegaml.runtime.OmegaRuntimeDask + :members: model + +.. autoclass:: omegaml.runtime.OmegaModelProxy + :members: + +.. autoclass:: omegaml.runtime.OmegaJobProxy + :members: + + +omegaml.jobs +++++++++++++ + +.. autoclass:: omegaml.jobs.OmegaJobs + :members: run, run_notebook, schedule + + +omegaml.mdataframe +++++++++++++++++++ + +.. autoclass:: omegaml.mdataframe.MDataFrame + :members: groupby, inspect, __len__, value, sort, head, skip, merge, query, query_inplace, create_index, loc + :special-members: __len__ + +.. autoclass:: omegaml.mdataframe.MSeries + :inherited-members: groupby, inspect, value, sort, head, skip, merge, query, query_inplace, create_index, loc + :special-members: __len__ + + +.. autoclass:: omegaml.mdataframe.MGrouper + :members: agg, aggregate, count + +.. autoclass:: omegaml.mdataframe.MLocIndexer + :special-members: __getitem__ + +.. autoclass:: omegaml.mdataframe.MPosIndexer + :special-members: __getitem__ + +.. autoclass:: omegaml.mixins.mdf.ApplyContext + +.. autoclass:: omegaml.mixins.mdf.ApplyArithmetics + :special-members: __mul__, __add__, + +.. autoclass:: omegaml.mixins.mdf.ApplyDateTime + +.. autoclass:: omegaml.mixins.mdf.ApplyString + +.. autoclass:: omegaml.mixins.mdf.ApplyAccumulators + + diff --git a/docs/source/screenshots.rst b/docs/source/screenshots.rst new file mode 100644 index 00000000..0e359683 --- /dev/null +++ b/docs/source/screenshots.rst @@ -0,0 +1,28 @@ +Screenshots +=========== + +Screenshots from the omega|ml Enterprise Edition + +Dashboard +--------- + +.. image:: images/screenshots/dashboard.png + :class: om-box + +Notebook Online Editor & Report publishing +------------------------------------------ + +.. image:: images/screenshots/notebook.png + :class: om-box + +Integrated Online Help +---------------------- + +.. image:: images/screenshots/help.png + :class: om-box + +REST API documentation +---------------------- + +.. image:: images/screenshots/restapi.png + :class: om-box \ No newline at end of file diff --git a/omegaml/__init__.py b/omegaml/__init__.py new file mode 100644 index 00000000..c9024e3d --- /dev/null +++ b/omegaml/__init__.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import + +import logging + +from omegaml.util import load_class, settings + +logger = logging.getLogger(__file__) + +try: + from omegaee import omega as _omega +except Exception as e: + from omegaml import omega as _omega + +# link implementation +setup = _omega.setup +version = _omega.version +get_omega_for_task = _omega.get_omega_for_task +Omega = _omega.Omega +# setup API +#: the OmegaStore for datasets +datasets = _omega.OmegaDeferredInstance(_omega._om, 'datasets') +#: the OmegaStore for models +models = _omega.OmegaDeferredInstance(_omega._om, 'models') +#: the jobs API +jobs = _omega.OmegaDeferredInstance(_omega._om, 'jobs') +#: the OmegaStore for lambda scripts +scripts = _omega.OmegaDeferredInstance(_omega._om, 'scripts') +#: the OmegaRuntime for cluster execution +runtime = _omega.OmegaDeferredInstance(_omega._om, 'runtime') +#: the settings object +config = _omega.OmegaDeferredInstance(_omega._om, 'defaults') diff --git a/omegaml/_version.py b/omegaml/_version.py new file mode 100644 index 00000000..0708834c --- /dev/null +++ b/omegaml/_version.py @@ -0,0 +1 @@ +version = '0.10.0-beta' \ No newline at end of file diff --git a/omegaml/backends/__init__.py b/omegaml/backends/__init__.py new file mode 100644 index 00000000..515216a5 --- /dev/null +++ b/omegaml/backends/__init__.py @@ -0,0 +1,3 @@ +from __future__ import absolute_import +from omegaml.backends.basemodel import BaseModelBackend +from .scikitlearn import ScikitLearnBackend diff --git a/omegaml/backends/basedata.py b/omegaml/backends/basedata.py new file mode 100644 index 00000000..4cbda6b9 --- /dev/null +++ b/omegaml/backends/basedata.py @@ -0,0 +1,58 @@ + + +class BaseDataBackend(object): + + """ + OmegaML BaseDataBackend to be subclassed by other arbitrary backends + + This provides the abstract interface for any data backend to be implemented + """ + def __init__(self, model_store=None, data_store=None, **kwargs): + assert model_store, "Need a model store" + assert data_store, "Need a data store" + self.model_store = model_store + self.data_store = data_store + + @classmethod + def supports(self, obj, name, **kwargs): + """ + test if this backend supports this obj + """ + return False + + def put(self, obj, name, attributes=None, **kwargs): + """ + put an obj + + :param obj: the object to store (object) + :param name: the name of the object (str) + :param attributes: the attributes dict (dict, optional) + :param kwargs: other kwargs to be passed to the Metadata object + :return: the Metadata object + """ + raise NotImplementedError + + def get(self, name, version=-1, force_python=False, lazy=False, **kwargs): + """ + get an obj + + :param name: the name of the object (str) + :return: the object as it was originally stored + """ + raise NotImplementedError + + def getl(self, *args, **kwargs): + """ + get an lazy implementation to access the obj + + A lazy implementation is a proxy to the object that can be + evaluated using the :code:`.value` property. The proxy should + ensure that any operations applied on the object are delayed until + the .value property is accessed. Typically this is to ensure that + the actual computation is executed on the cluster, not on the local + machine. + + :param name: the name of the object (str) + :return: the proxy to the object as it was originally stored + """ + return self.get(*args, lazy=True, **kwargs) diff --git a/omegaml/backends/basemodel.py b/omegaml/backends/basemodel.py new file mode 100644 index 00000000..f3abd75d --- /dev/null +++ b/omegaml/backends/basemodel.py @@ -0,0 +1,167 @@ + + +class BaseModelBackend(object): + + """ + OmegaML BaseModelBackend to be subclassed by other arbitrary backends + + This provides the abstract interface for any model backend to be implemented + """ + def __init__(self, model_store=None, data_store=None, **kwargs): + assert model_store, "Need a model store" + assert data_store, "Need a data store" + self.model_store = model_store + self.data_store = data_store + + @classmethod + def supports(self, obj, name, **kwargs): + """ + test if this backend supports this obj + """ + return False + + def get(self, name, **kwargs): + """ + retrieve a model + + :param name: the name of the object + :param version: the version of the object (not supported) + """ + # support new backend architecture while keeping back compatibility + return self.get_model(name) + + def put(self, obj, name, **kwargs): + """ + store a model + + :param obj: the model object to be stored + :param name: the name of the object + :param attributes: attributes for meta data + """ + # support new backend architecture while keeping back compatibility + return self.put_model(obj, name, **kwargs) + + def put_model(self, obj, name, attributes=None): + """ + store a model + + :param obj: the model object to be stored + :param name: the name of the object + :param attributes: attributes for meta data + """ + raise NotImplementedError + + def get_model(self, name, version=-1): + """ + retrieve a model + + :param name: the name of the object + :param version: the version of the object (not supported) + """ + raise NotImplementedError + + def predict( + self, modelname, Xname, rName=None, pure_python=True, **kwargs): + """ + predict using data stored in Xname + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param rName: the name of the result data object or None + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's predict method + :return: return the predicted outcome + """ + raise NotImplementedError + + def predict_proba( + self, modelname, Xname, rName=None, pure_python=True, **kwargs): + """ + predict the probability using data stored in Xname + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param rName: the name of the result data object or None + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's predict method + :return: return the predicted outcome + """ + raise NotImplementedError + + def fit(self, modelname, Xname, Yname=None, pure_python=True, **kwargs): + """ + fit the model with data + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param Yname: the name of the Y data set + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's predict method + :return: return the meta data object of the model + """ + raise NotImplementedError + + def partial_fit( + self, modelname, Xname, Yname=None, pure_python=True, **kwargs): + """ + partially fit the model with data (online) + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param Yname: the name of the Y data set + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's predict method + :return: return the meta data object of the model + """ + + raise NotImplementedError + + def fit_transform( + self, modelname, Xname, Yname=None, rName=None, pure_python=True, + **kwargs): + """ + fit and transform using data + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param Yname: the name of the Y data set + :param rName: the name of the transforms's result data object or None + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's transform method + :return: return the meta data object of the model + """ + raise NotImplementedError + + def transform(self, modelname, Xname, rName=None, **kwargs): + """ + transform using data + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param rName: the name of the transforms's result data object or None + :param kwargs: kwargs passed to the model's transform method + :return: return the transform data of the model + """ + raise NotImplementedError + + def score( + self, modelname, Xname, Yname, rName=True, pure_python=True, + **kwargs): + """ + score using data + + :param modelname: the name of the model object + :param Xname: the name of the X data set + :param Yname: the name of the Y data set + :param rName: the name of the transforms's result data object or None + :param pure_python: if True return a python object. If False return + a dataframe. Defaults to True to support any client. + :param kwargs: kwargs passed to the model's predict method + :return: return the score result + """ + raise NotImplementedError diff --git a/omegaml/backends/scikitlearn.py b/omegaml/backends/scikitlearn.py new file mode 100644 index 00000000..94954a39 --- /dev/null +++ b/omegaml/backends/scikitlearn.py @@ -0,0 +1,247 @@ +from __future__ import absolute_import + +import glob +import os +from shutil import rmtree +import tempfile +from zipfile import ZipFile, ZIP_DEFLATED + +import datetime +from mongoengine.fields import GridFSProxy +from sklearn.model_selection import GridSearchCV + +from omegaml.util import reshaped, gsreshaped + +from omegaml.backends.basemodel import BaseModelBackend + + +class ScikitLearnBackend(BaseModelBackend): + """ + OmegaML backend to use with ScikitLearn + """ + + def _package_model(self, model, filename): + """ + Dumps a model using joblib and packages all of joblib files into a zip + file + """ + import joblib + lpath = tempfile.mkdtemp() + fname = os.path.basename(filename) + mklfname = os.path.join(lpath, fname) + zipfname = os.path.join(self.model_store.tmppath, fname) + joblib.dump(model, mklfname, protocol=2) + with ZipFile(zipfname, 'w', compression=ZIP_DEFLATED) as zipf: + for part in glob.glob(os.path.join(lpath, '*')): + zipf.write(part, os.path.basename(part)) + rmtree(lpath) + return zipfname + + def _extract_model(self, packagefname): + """ + Loads a model using joblib from a zip file created with _package_model + """ + import joblib + lpath = tempfile.mkdtemp() + fname = os.path.basename(packagefname) + mklfname = os.path.join(lpath, fname) + with ZipFile(packagefname) as zipf: + zipf.extractall(lpath) + model = joblib.load(mklfname) + rmtree(lpath) + return model + + def get_model(self, name, version=-1): + """ + Retrieves a pre-stored model + """ + filename = self.model_store._get_obj_store_key(name, '.omm') + packagefname = os.path.join(self.model_store.tmppath, name) + dirname = os.path.dirname(packagefname) + try: + os.makedirs(dirname) + except OSError: + # OSError is raised if path exists already + pass + outf = self.model_store.fs.get_version(filename, version=version) + with open(packagefname, 'wb') as zipf: + zipf.write(outf.read()) + model = self._extract_model(packagefname) + return model + + def put_model(self, obj, name, attributes=None): + """ + Packages a model using joblib and stores in GridFS + """ + from ..documents import Metadata + zipfname = self._package_model(obj, name) + with open(zipfname, 'rb') as fzip: + fileid = self.model_store.fs.put( + fzip, filename=self.model_store._get_obj_store_key(name, 'omm')) + gridfile = GridFSProxy(grid_id=fileid, + db_alias='omega', + collection_name=self.model_store.bucket) + return self.model_store._make_metadata( + name=name, + prefix=self.model_store.prefix, + bucket=self.model_store.bucket, + kind=Metadata.SKLEARN_JOBLIB, + attributes=attributes, + gridfile=gridfile).save() + + def predict( + self, modelname, Xname, rName=None, pure_python=True, **kwargs): + data = self.data_store.get(Xname) + model = self.model_store.get(modelname) + result = model.predict(reshaped(data), **kwargs) + if pure_python: + result = result.tolist() + if rName: + meta = self.data_store.put(result, rName) + result = meta + return result + + def predict_proba( + self, modelname, Xname, rName=None, pure_python=True, **kwargs): + data = self.data_store.get(Xname) + model = self.model_store.get(modelname) + result = model.predict_proba(reshaped(data), **kwargs) + if pure_python: + result = result.tolist() + if rName: + meta = self.data_store.put(result, rName) + result = meta + return result + + def fit(self, modelname, Xname, Yname=None, pure_python=True, **kwargs): + model = self.model_store.get(modelname) + X, metaX = self.data_store.get(Xname), self.data_store.metadata(Xname) + Y, metaY = None, None + if Yname: + Y, metaY = (self.data_store.get(Yname), + self.data_store.metadata(Yname)) + model.fit(reshaped(X), reshaped(Y), **kwargs) + # store information required for retraining + model_attrs = { + 'metaX': metaX.to_mongo(), + 'metaY': metaY.to_mongo() if metaY is not None else None, + } + try: + import sklearn + model_attrs['scikit-learn'] = sklearn.__version__ + except: + model_attrs['scikit-learn'] = 'unknown' + meta = self.model_store.put(model, modelname, attributes=model_attrs) + return meta + + def partial_fit( + self, modelname, Xname, Yname=None, pure_python=True, **kwargs): + model = self.model_store.get(modelname) + X, metaX = self.data_store.get(Xname), self.data_store.metadata(Xname) + Y, metaY = None, None + if Yname: + Y, metaY = (self.data_store.get(Yname), + self.data_store.metadata(Yname)) + model.partial_fit(reshaped(X), reshaped(Y), **kwargs) + # store information required for retraining + model_attrs = { + 'metaX': metaX.to_mongo(), + 'metaY': metaY.to_mongo() if metaY is not None else None, + } + try: + import sklearn + model_attrs['scikit-learn'] = sklearn.__version__ + except: + model_attrs['scikit-learn'] = 'unknown' + meta = self.model_store.put(model, modelname, attributes=model_attrs) + return meta + + def score( + self, modelname, Xname, Yname, rName=None, pure_python=True, + **kwargs): + model = self.model_store.get(modelname) + X = self.data_store.get(Xname) + Y = self.data_store.get(Yname) + result = model.score(reshaped(X), reshaped(Y), **kwargs) + if rName: + meta = self.model_store.put(result, rName) + result = meta + return result + + def fit_transform( + self, modelname, Xname, Yname=None, rName=None, pure_python=True, + **kwargs): + model = self.model_store.get(modelname) + X, metaX = self.data_store.get(Xname), self.data_store.metadata(Xname) + Y, metaY = None, None + if Yname: + Y, metaY = (self.data_store.get(Yname), + self.data_store.metadata(Yname)) + result = model.fit_transform(reshaped(X), reshaped(Y), **kwargs) + # store information required for retraining + model_attrs = { + 'metaX': metaX.to_mongo(), + 'metaY': metaY.to_mongo() if metaY is not None else None + } + try: + import sklearn + model_attrs['scikit-learn'] = sklearn.__version__ + except: + model_attrs['scikit-learn'] = 'unknown' + meta = self.model_store.put(model, modelname, attributes=model_attrs) + if pure_python: + result = result.tolist() + if rName: + meta = self.data_store.put(result, rName) + result = meta + return result + + def transform(self, modelname, Xname, rName=None, pure_python=True, **kwargs): + model = self.model_store.get(modelname) + X = self.data_store.get(Xname) + result = model.transform(reshaped(X), **kwargs) + if pure_python: + result = result.tolist() + if rName: + meta = self.data_store.put(result, rName) + result = meta + return result + + def decision_function(self, modelname, Xname, rName=None, pure_python=True, **kwargs): + model = self.model_store.get(modelname) + X = self.data_store.get(Xname) + result = model.decision_function(reshaped(X), **kwargs) + if pure_python: + result = result.tolist() + if rName: + meta = self.data_store.put(result, rName) + result = meta + return result + + def gridsearch(self, modelname, Xname, Yname, rName=None, + parameters=None, pure_python=True, **kwargs): + model, meta = self.model_store.get(modelname), self.model_store.metadata(modelname) + X = self.data_store.get(Xname) + if Yname: + y = self.data_store.get(Yname) + else: + y = None + gs_model = GridSearchCV(cv=None, estimator=model, param_grid=parameters, **kwargs) + gs_model.fit(X, gsreshaped(y)) + nowdt = datetime.datetime.now() + if rName: + gs_modelname = rName + else: + gs_modelname = '{}.{}.gs'.format(modelname, nowdt.isoformat()) + gs_meta = self.model_store.put(gs_model, gs_modelname) + attributes = meta.attributes + if not 'gridsearch' in attributes: + attributes['gridsearch'] = [] + attributes['gridsearch'].append({ + 'datetime': nowdt, + 'Xname': Xname, + 'Yname': Yname, + 'gsModel': gs_modelname, + }) + meta.save() + return meta diff --git a/omegaml/celery_util.py b/omegaml/celery_util.py new file mode 100644 index 00000000..7591dbaf --- /dev/null +++ b/omegaml/celery_util.py @@ -0,0 +1,39 @@ +import six +from celery import Task + + +class OmegamlTask(Task): + abstract = True + + def __init__(self, *args, **kwargs): + super(OmegamlTask, self).__init__(*args, **kwargs) + self._om = None + + @property + def om(self): + if self._om is None: + from omegaml import get_omega_for_task + self._om = get_omega_for_task(self) + return self._om + + def get_delegate(self, name, kind='models'): + get_delegate_provider = getattr(self.om, kind) + return get_delegate_provider.get_backend(name, data_store=self.om.datasets) + + @property + def delegate_args(self): + return self.request.args + + @property + def delegate_kwargs(self): + return {k: v for k, v in six.iteritems(self.request.kwargs) if not k.startswith('__')} + + +def get_dataset_representations(items): + """ + returns dict with x and y datasets + """ + results = {} + results['Xname'] = items.get('Xname') + results['Yname'] = items.get('Yname') + return results diff --git a/omegaml/celeryapp.py b/omegaml/celeryapp.py new file mode 100644 index 00000000..7d651122 --- /dev/null +++ b/omegaml/celeryapp.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import +''' +make sure Celery is correctly configured +see http://chriskief.com/2013/11/15/celery-3-1-with-django-django-celery-rabbitmq-and-macports/ +''' + +import os + +from celery import Celery +from omegaml.util import settings + +# get rid of celery's Django compatibility mode +os.environ['DJANGO_SETTINGS_MODULE'] = '' + +defaults = settings() +app = Celery('omegaml') +app.config_from_object(defaults.OMEGA_CELERY_CONFIG) +app.autodiscover_tasks(defaults.OMEGA_CELERY_IMPORTS, related_name='tasks') diff --git a/omegaml/defaults.py b/omegaml/defaults.py new file mode 100644 index 00000000..ef800eec --- /dev/null +++ b/omegaml/defaults.py @@ -0,0 +1,155 @@ +from __future__ import absolute_import +import os +import sys +import six +import yaml + +user_homedir = os.path.expanduser('~') + +#: configuration file, defaults to $HOME/.omegaml/config.yml +OMEGA_CONFIG_FILE = os.path.join(user_homedir, '.omegaml', 'config.yml') +#: the temp directory used by omegaml processes +OMEGA_TMP = '/tmp' +#: the fully qualified mongodb database URL, including the database name +OMEGA_MONGO_URL = (os.environ.get('OMEGA_MONGO_URL') or + os.environ.get('MONGO_URL') or + 'mongodb://admin:foobar@localhost:27019/omega') +#: the collection name in the mongodb used by omegaml storage +OMEGA_MONGO_COLLECTION = 'omegaml' +#: the celery broker name or URL +OMEGA_BROKER = (os.environ.get('OMEGA_BROKER') or + os.environ.get('RABBITMQ_URL') or + 'amqp://guest@127.0.0.1:5672//') +#: (deprecated) the collection used to store ipython notebooks +OMEGA_NOTEBOOK_COLLECTION = 'ipynb' +#: the celery backend name or URL +OMEGA_RESULT_BACKEND = 'amqp' +#: the celery configurations +OMEGA_CELERY_CONFIG = { + 'CELERY_ACCEPT_CONTENT': ['pickle', 'json', 'msgpack', 'yaml'], + 'BROKER_URL': OMEGA_BROKER, + 'CELERY_RESULT_BACKEND': OMEGA_RESULT_BACKEND, + 'CELERYBEAT_SCHEDULE': { + 'execute_scripts': { + 'task': 'omegaml.notebook.tasks.execute_scripts', + 'schedule': 60, + }, + }, +} +#: celery task packages +OMEGA_CELERY_IMPORTS = ['omegaml.tasks', 'omegaml.notebook.tasks'] +#: storage backends +OMEGA_STORE_BACKENDS = { + 'sklearn.joblib': 'omegaml.backends.ScikitLearnBackend', +} +#: storage mixins +OMEGA_STORE_MIXINS = [ + 'omegaml.mixins.store.ProjectedMixin', +] +#: runtimes mixins +OMEGA_RUNTIME_MIXINS = [ + 'omegaml.runtimes.mixins.ModelMixin', + 'omegaml.runtimes.mixins.GridSearchMixin', +] +#: mdataframe mixins +OMEGA_MDF_MIXINS = [ + ('omegaml.mixins.mdf.ApplyMixin', 'MDataFrame,MSeries'), + ('omegaml.mixins.mdf.FilterOpsMixin', 'MDataFrame,MSeries'), + ('omegaml.mixins.mdf.apply.ApplyStatistics', 'MDataFrame,MSeries'), +] +#: mdataframe apply context mixins +OMEGA_MDF_APPLY_MIXINS = [ + ('omegaml.mixins.mdf.ApplyArithmetics', 'MDataFrame,MSeries'), + ('omegaml.mixins.mdf.ApplyDateTime', 'MDataFrame,MSeries'), + ('omegaml.mixins.mdf.ApplyString', 'MDataFrame,MSeries'), + ('omegaml.mixins.mdf.ApplyAccumulators', 'MDataFrame,MSeries'), +] + + + + +# ========================================= +# ----- DO NOT MODIFY BELOW THIS LINE ----- +# ========================================= +def update_from_config(vars=globals(), config_file=OMEGA_CONFIG_FILE): + """ + update omegaml.defaults from configuration file + + :param vars: the variables to update + :param config_file: the path to config.yml or a file object + :return: + """ + # override from configuration file + userconfig = {} + if isinstance(config_file, six.string_types): + if os.path.exists(config_file): + with open(config_file, 'r') as fin: + userconfig = yaml.load(fin) + else: + userconfig = yaml.load(config_file) + if userconfig: + for k in [k for k in vars.keys() if k.startswith('OMEGA')]: + vars[k] = userconfig.get(k, None) or vars[k] + return vars + + +def update_from_env(vars=globals()): + # simple override from env vars + # -- top-level OMEGA_* + for k in [k for k in vars.keys() if k.startswith('OMEGA')]: + vars[k] = os.environ.get(k, None) or vars[k] + # -- OMEGA_CELERY_CONFIG updates + for k in [k for k in os.environ.keys() if k.startswith('OMEGA_CELERY')]: + celery_k = k.replace('OMEGA_', '') + vars['OMEGA_CELERY_CONFIG'][celery_k] = os.environ[k] + # -- debug if required + if '--print-omega-defaults' in sys.argv: + from pprint import pprint + vars = {k: v for k, v in six.iteritems(vars) if k.startswith('OMEGA')} + pprint(vars) + return vars + + +def update_from_obj(obj, vars=globals()): + """ + helper function to update omegaml.defaults from arbitrary module + + :param obj: the source object (must support getattr). Any + variable starting with OMEGA is set in omegaml.defaults + """ + for k in [k for k in dir(obj) if k.startswith('OMEGA')]: + if hasattr(obj, k): + value = getattr(obj, k) + vars[k] = value + + +def update_from_dict(d, vars=globals()): + """ + helper function to update omegaml.defaults from arbitrary dictionary + + :param d: the source dict (must support [] lookup). Any + variable starting with OMEGA is set in omegaml.defaults + """ + for k, v in six.iteritems(d): + if k.startswith('OMEGA'): + vars[k] = v + + +# load Enterprise Edition if available +try: + from omegaee import eedefaults + + update_from_obj(eedefaults) +except Exception as e: + pass + +# -- test +if any(m in ' '.join(sys.argv) for m in ('unittest', 'test', 'nosetest', 'noserunner')): + OMEGA_MONGO_URL = OMEGA_MONGO_URL.replace('/omega', '/testdb') + OMEGA_CELERY_CONFIG['CELERY_ALWAYS_EAGER'] = True + OMEGA_RESTAPI_URL = '' +else: + # overrides in actual operations + # this is to avoid using production settings during test + update_from_config(globals()) + update_from_env(globals()) diff --git a/omegaml/documents.py b/omegaml/documents.py new file mode 100644 index 00000000..7f01d515 --- /dev/null +++ b/omegaml/documents.py @@ -0,0 +1,84 @@ +from __future__ import absolute_import +import datetime + +from mongoengine.base.fields import ObjectIdField +from mongoengine.document import Document +from mongoengine.fields import ( + StringField, FileField, DictField, DateTimeField, URLField +) +from omegaml.util import settings + + +class Metadata(Document): + """ + Metadata stores information about objects in OmegaStore + """ + # default kinds of data + PANDAS_DFROWS = 'pandas.dfrows' # dataframe + PANDAS_SEROWS = 'pandas.serows' # series + PANDAS_HDF = 'pandas.hdf' + PYTHON_DATA = 'python.data' + PANDAS_DFGROUP = 'pandas.dfgroup' + SKLEARN_JOBLIB = 'sklearn.joblib' + OMEGAML_JOBS = 'script.ipynb' + SPARK_MLLIB = 'spark.mllib' + OMEGAML_RUNNING_JOBS = 'job.run' + #: the list of accepted data types. extend using OmegaStore.register_backend + KINDS = [ + PANDAS_DFROWS, PANDAS_SEROWS, PANDAS_HDF, PYTHON_DATA, SKLEARN_JOBLIB, + PANDAS_DFGROUP, OMEGAML_JOBS, OMEGAML_RUNNING_JOBS, SPARK_MLLIB] + # fields + #: this is the name of the data + name = StringField() + #: bucket + bucket = StringField() + #: prefix + prefix = StringField() + #: kind of data + kind = StringField(choices=KINDS) + #: for PANDAS_HDF and SKLEARN_JOBLIB this is the gridfile + gridfile = FileField( + db_alias='omega', + collection_name=settings().OMEGA_MONGO_COLLECTION) + #: for PANDAS_DFROWS this is the collection + collection = StringField() + #: for PYTHON_DATA this is the actual document + objid = ObjectIdField() + #: omegaml technical attributes, e.g. column indicies + kind_meta = DictField() + #: customer-defined other meta attributes + attributes = DictField() + #: s3file attributes + s3file = DictField() + #: location URI + uri = StringField() + #: created datetime + created = DateTimeField(default=datetime.datetime.now) + # the actual db is defined in settings, OMEGA_MONGO_URL + meta = { + 'db_alias': 'omega', + 'indexes': [ + # unique entry + { + 'fields': ['bucket', 'prefix', 'name'], + }, + 'created', # most recent is last, i.e. [-1] + ] + } + + def __unicode__(self): + kwargs = ('%s=%s' % (k, getattr(self, k)) + for k in self._fields.keys() if k in ('bucket', 'prefix', 'created', 'kind')) + return u"Metadata(%s)" % ','.join(kwargs) + + +class QueryCache(Document): + collection = StringField() + key = StringField() + value = DictField() + meta = { + 'db_alias': 'omega', + 'indexes': [ + 'key', + ] + } diff --git a/omegaml/jobs.py b/omegaml/jobs.py new file mode 100644 index 00000000..a2cf0285 --- /dev/null +++ b/omegaml/jobs.py @@ -0,0 +1,266 @@ +from __future__ import absolute_import + +import datetime +import re +from uuid import uuid4 + +import gridfs +import yaml +from croniter import croniter +from mongoengine.fields import GridFSProxy +from nbconvert.preprocessors.execute import ExecutePreprocessor +from nbformat import read as nbread, write as nbwrite, v4 as nbv4 +from six import StringIO, BytesIO + +from omegaml.documents import Metadata +from omegaml.notebook.tasks import run_omegaml_job +from omegaml.store import OmegaStore +from omegaml.util import settings as omega_settings + + +class OmegaJobs(object): + """ + Omega Jobs API + """ + + # TODO this class should be a proper backend class + + def __init__(self, prefix=None, store=None, defaults=None): + self.defaults = defaults or omega_settings() + prefix = prefix or 'jobs' + self.store = store or OmegaStore(prefix=prefix) + self.kind = Metadata.OMEGAML_JOBS + + def __repr__(self): + return 'OmegaJobs(store={})'.format(self.store.__repr__()) + + @property + def _db(self): + return self.store.mongodb + + @property + def _fs(self): + return self.store.fs + + def collection(self, name): + if not name.endswith('.ipynb'): + name += '.ipynb' + return self.store.collection(name) + + def drop(self, name): + if not name.endswith('.ipynb'): + name += '.ipynb' + return self.store.drop(name) + + def metadata(self, name): + if not name.endswith('.ipynb'): + name += '.ipynb' + return self.store.metadata(name) + + def exists(self, name): + if not name.endswith('.ipynb'): + name += '.ipynb' + return len(self.store.list(name)) > 0 + + def put(self, obj, name, attributes=None): + """ + Store a NotebookNode + + :param obj: the NotebookNode to store + :param name: the name of the notebook + """ + if not name.endswith('.ipynb'): + name += '.ipynb' + sbuf = StringIO() + bbuf = BytesIO() + # nbwrite expects string, fs.put expects bytes + nbwrite(obj, sbuf, version=4) + sbuf.seek(0) + bbuf.write(sbuf.getvalue().encode('utf8')) + bbuf.seek(0) + # see if we have a file already, if so replace the gridfile + meta = self.store.metadata(name) + if not meta: + filename = uuid4().hex + fileid = self._fs.put(bbuf, filename=filename) + meta = self.store._make_metadata(name=name, + prefix=self.store.prefix, + bucket=self.store.bucket, + kind=self.kind, + attributes=attributes, + gridfile=GridFSProxy(grid_id=fileid)) + else: + meta.gridfile.replace(bbuf) + return meta.save() + + def get(self, name): + """ + Retrieve a notebook and return a NotebookNode + """ + if not name.endswith('.ipynb'): + name += '.ipynb' + meta = self.store.metadata(name) + if meta: + try: + outf = meta.gridfile + except gridfs.errors.NoFile as e: + raise e + # nbwrite wants a string, outf is bytes + sbuf = StringIO() + data = outf.read() + if data is None: + msg = 'Expected content in {name}, got None'.format(**locals()) + raise ValueError(msg) + sbuf.write(data.decode('utf8')) + sbuf.seek(0) + nb = nbread(sbuf, as_version=4) + return nb + else: + raise gridfs.errors.NoFile( + ">{0}< does not exist in jobs bucket '{1}'".format( + name, self.store.bucket)) + + def create(self, code, name): + """ + create a notebook from code + + :param code: the code as a string + :param name: the name of the job to create + :return: the metadata object created + """ + cells = [] + cells.append(nbv4.new_code_cell(source=code)) + notebook = nbv4.new_notebook(cells=cells) + # put the notebook + meta = self.put(notebook, 'testjob') + return meta + + def get_fs(self, collection=None): + # legacy support + return self._fs + + def get_collection(self, collection): + """ + returns the collection object + """ + # FIXME this should use store.collection + return getattr(self.store.mongodb, collection) + + def list(self, jobfilter='.*', raw=False): + """ + list all jobs matching filter. + filter is a regex on the name of the ipynb entry. + The default is all, i.e. `.*` + """ + job_list = self.store.list(regexp=jobfilter, raw=raw) + return job_list + + def get_notebook_config(self, nb_filename): + """ + returns the omegaml script config on + the notebook's first cell + """ + notebook = self.get(nb_filename) + config_cell = notebook.get('worksheets')[0].get('cells')[0] + yaml_conf = '\n'.join( + [re.sub('#', '', x, 1) for x in str( + config_cell.input).splitlines()]) + try: + yaml_conf = yaml.load(yaml_conf) + # even a comment qualifies as a valid yaml + # so testing to check if the yaml is exactly what we expect + if yaml_conf.get("omegaml.script") is not None: + pass + else: + raise ValueError( + 'Notebook configuration either not present or has errors!') + except Exception: + raise ValueError( + 'Notebook configuration either not present or has errors!') + + return yaml_conf.get("omegaml.script") + + def run(self, name): + """ + Run a job immediately + + The job is run and the results are stored in the given filename + + :param name: the name of the jobfile + :return: the metadata of the job + """ + return self.run_notebook(name) + + def run_notebook(self, name): + """ + run a given notebook immediately. + the job parameter is the name of the job script as in ipynb. + Inserts and returns the Metadata document for the job. + """ + notebook = self.get(name) + meta_job = self.metadata(name) + ts = datetime.datetime.now().strftime('%s') + # execute + try: + ep = ExecutePreprocessor() + ep.preprocess(notebook, {'metadata': {'path': '/'}}) + except Exception as e: + status = str(e) + else: + status = 'OK' + # record results + meta_results = self.put( + notebook, 'results/{name}_{ts}'.format(**locals())) + meta_results.attributes['source_job'] = name + meta_results.save() + job_results = meta_job.attributes.get('job_results', []) + job_results.append(meta_results.name) + meta_job.attributes['job_results'] = job_results + # record final job status + job_runs = meta_job.attributes.get('job_runs', {}) + job_runs[ts] = status + meta_job.attributes['job_runs'] = job_runs + meta_job.save() + return meta_job + + def schedule(self, nb_file): + """ + Schedule a processing of a notebook as per the interval + specified on the job script + """ + # FIXME this looks somewhat unstable. currently we schedule by + # inserting metadata that sets the state of the job to + # RECEIVED. Then the task execute_script which is + # scheduled by celery gets all new jobs not yet in RECEIVED + # state, and schedules for the next iteration. What happens + # if a job was scheduled already how will it get reschduled? + attrs = {} + config = self.get_notebook_config(nb_file) + now = datetime.datetime.now() + interval = config.get('run-at') + iter_next = croniter(interval, now) + run_at = iter_next.get_next(datetime.datetime) + next_run_time = iter_next.get_next(datetime.datetime) + kwargs = dict( + config=config, + run_at=run_at, + next_run_time=next_run_time) + # check if this job was scheduled earlier + try: + metadata = Metadata.objects.get( + name=nb_file, kind=Metadata.OMEGAML_RUNNING_JOBS) + if metadata.attributes.get('state') == "RECEIVED": + # FIXME return only at end of method. + return metadata.attributes.get('task_id') + except Metadata.DoesNotExist: + # set attributes + attrs['config'] = config + attrs['next_run_time'] = run_at + attrs['state'] = 'RECEIVED' + Metadata( + name=nb_file, + kind=Metadata.OMEGAML_RUNNING_JOBS, + attributes=attrs).save() + result = run_omegaml_job.apply_async( + args=[nb_file], eta=run_at, kwargs=kwargs) + return result diff --git a/omegaml/mdataframe.py b/omegaml/mdataframe.py new file mode 100644 index 00000000..a0221f76 --- /dev/null +++ b/omegaml/mdataframe.py @@ -0,0 +1,1007 @@ +from __future__ import absolute_import + +from uuid import uuid4 + +import numpy as np +import pandas as pd +import six +from bson import Code +from numpy import isscalar +from omegaml import defaults +from omegaml.store import qops +from omegaml.store.filtered import FilteredCollection +from omegaml.store.query import Filter, MongoQ +from omegaml.store.queryops import MongoQueryOps +from omegaml.util import make_tuple, make_list, restore_index, \ + cursor_to_dataframe, restore_index_columns_order, PickableCollection, extend_instance +from pymongo.collection import Collection + +INSPECT_CACHE = [] + + +class MGrouper(object): + """ + a Grouper for MDataFrames + """ + STATS_MAP = { + 'std': 'stdDevSamp', + 'mean': 'avg', + } + + def __init__(self, mdataframe, collection, columns, sort=True): + self.mdataframe = mdataframe + self.collection = collection + self.columns = make_tuple(columns) + self.should_sort = sort + + def __getattr__(self, attr): + if attr in self.columns: + return MSeriesGroupby(self, self.collection, attr) + + def statfunc(): + columns = self.columns or self._non_group_columns() + return self.agg({col: attr for col in columns}) + + return statfunc + + def agg(self, specs): + """ + shortcut for .aggregate + """ + return self.aggregate(specs) + + def aggregate(self, specs): + """ + aggregate by given specs + + See the following link for a list of supported operations. + https://docs.mongodb.com/manual/reference/operator/aggregation/group/ + + :param specs: a dictionary of { column : function | list[functions] } + pairs. + """ + + def add_stats(specs, column, stat): + specs['%s_%s' % (column, stat)] = { + '$%s' % MGrouper.STATS_MAP.get(stat, stat): '$%s' % column} + + # generate $group command + _specs = {} + for column, stats in six.iteritems(specs): + stats = make_tuple(stats) + for stat in stats: + add_stats(_specs, column, stat) + groupby = qops.GROUP(columns=self.columns, + **_specs) + # execute and return a dataframe + pipeline = self._amend_pipeline([groupby]) + data = self.collection.aggregate(pipeline, allowDiskUse=True) + + def get_data(): + # we need this to build a pipeline for from_records + # to process, otherwise the cursor will be exhausted already + for group in data: + _id = group.pop('_id') + if isinstance(_id, dict): + group.update(_id) + yield group + + df = pd.DataFrame.from_records(get_data()) + columns = make_list(self.columns) + if columns: + df = df.set_index(columns, drop=True) + return df + + def _amend_pipeline(self, pipeline): + """ amend pipeline with default ops on coll.aggregate() calls """ + if self.should_sort: + sort = qops.SORT(**dict(qops.make_sortkey('_id'))) + pipeline.append(sort) + return pipeline + + def _non_group_columns(self): + """ get all columns in mdataframe that is not in columns """ + return [col for col in self.mdataframe.columns + if col not in self.columns and col != '_id' + and not col.startswith('_idx') + and not col.startswith('_om#')] + + def _count(self): + count_columns = self._non_group_columns() + if len(count_columns) == 0: + count_columns.append('_'.join(self.columns) + '_count') + groupby = { + "$group": { + "_id": {k: "$%s" % k for k in self.columns}, + } + } + for k in count_columns: + groupby['$group']['%s' % k] = {"$sum": 1} + pipeline = self._amend_pipeline([groupby]) + if self.should_sort: + sort = qops.SORT(**dict(qops.make_sortkey('_id'))) + pipeline.append(sort) + return list(self.collection.aggregate(pipeline)) + + def count(self): + """ return counts by group columns """ + counts = self._count() + # remove mongo object _id + for group in counts: + group.update(group.pop('_id')) + # transform results to dataframe, then return as pandas would + resultdf = pd.DataFrame(counts).set_index(make_list(self.columns), + drop=True) + return resultdf + + def __iter__(self): + """ for each group returns the key and a Filter object""" + groups = self._count() + for group in groups: + keys = group.get('_id') + data = Filter(self.collection, **keys) + yield keys, data + + +class MLocIndexer(object): + """ + implements the LocIndexer for MDataFrames + """ + + def __init__(self, mdataframe, positional=False): + self.mdataframe = mdataframe + # if positional, any loc[spec] will be applied on the rowid only + self.positional = positional + # indicator will be set true if loc specs are from a range type (list, tuple, np.ndarray) + self._from_range = False + + def __getitem__(self, specs): + """ + access by index + + use as mdf.loc[specs] where specs is any of + + * a list or tuple of scalar index values, e.g. .loc[(1,2,3)] + * a slice of values e.g. .loc[1:5] + * a list of slices, e.g. .loc[1:5, 2:3] + + :return: the sliced part of the MDataFrame + """ + filterq, projection = self._get_filter(specs) + df = self.mdataframe + if filterq: + df = self.mdataframe.query(filterq) + df.from_loc_indexer = True + df.from_loc_range = self._from_range + if projection: + df = df[projection] + if isinstance(self.mdataframe, MSeries): + df = df._as_mseries(df.columns[0]) + if getattr(df, 'immediate_loc', False): + df = df.value + return df + + def __setitem__(self, specs, value): + raise NotImplemented + + def _get_filter(self, specs): + filterq = [] + projection = [] + if self.positional: + idx_cols = ['_om#rowid'] + else: + idx_cols = self.mdataframe._get_frame_index() + flt_kwargs = {} + enumerable_types = (list, tuple, np.ndarray) + if isinstance(specs, np.ndarray): + specs = specs.tolist() + if (isinstance(specs, enumerable_types) + and isscalar(specs[0]) and len(idx_cols) == 1 + and not any(isinstance(s, slice) for s in specs)): + # single column index with list of scalar values + if (self.positional and isinstance(specs, tuple) and len(specs) == 2 + and all(isscalar(v) for v in specs)): + # iloc[int, int] is a cell access + flt_kwargs[idx_cols[0]] = specs[0] + projection.extend(self._get_projection(specs[1])) + else: + flt_kwargs['{}__in'.format(idx_cols[0])] = specs + self._from_range = True + elif isinstance(specs, (int, str)): + flt_kwargs[idx_cols[0]] = specs + else: + specs = make_tuple(specs) + # list/tuple of slices or scalar values, or MultiIndex + for i, spec in enumerate(specs): + if i < len(idx_cols): + col = idx_cols[i] + if isinstance(spec, slice): + self._from_range = True + start, stop = spec.start, spec.stop + if start is not None: + flt_kwargs['{}__gte'.format(col)] = start + if stop is not None: + if isinstance(stop, int): + stop -= int(self.positional) + flt_kwargs['{}__lte'.format(col)] = stop + elif isinstance(spec, enumerable_types) and isscalar(spec[0]): + self._from_range = True + # single column index with list of scalar values + # -- convert to list for PyMongo serialization + if isinstance(spec, np.ndarray): + spec = spec.tolist() + flt_kwargs['{}__in'.format(col)] = spec + elif isscalar(col): + flt_kwargs[col] = spec + else: + # we're out of index columns, let's look at columns + projection.extend(self._get_projection(spec)) + if flt_kwargs: + filterq.append(MongoQ(**flt_kwargs)) + finalq = None + for q in filterq: + if finalq: + finalq |= q + else: + finalq = q + return finalq, projection + + def _get_projection(self, spec): + columns = self.mdataframe.columns + if np.isscalar(spec): + return [spec] + if isinstance(spec, (tuple, list)): + assert all(columns.index(col) for col in columns) + return spec + if isinstance(spec, slice): + start, stop = spec.start, spec.stop + if all(isinstance(v, int) for v in (start, stop)): + start, stop, step = spec.indices(len(columns)) + else: + start = columns.index(start) if start is not None else 0 + stop = columns.index(stop) + 1 if stop is not None else len(columns) + return columns[slice(start, stop)] + raise IndexError + + +class MPosIndexer(MLocIndexer): + """ + implements the position-based indexer for MDataFrames + """ + + def __init__(self, mdataframe): + super(MPosIndexer, self).__init__(mdataframe, positional=True) + + def _get_projection(self, spec): + columns = self.mdataframe.columns + if np.isscalar(spec): + return columns[spec] + if isinstance(spec, (tuple, list)): + return [col for i, col in enumerate(spec) if i in spec] + if isinstance(spec, slice): + start, stop = slice.start, slice.stop + if start and not isinstance(start, int): + start = 0 + if stop and not isinstance(stop, int): + # sliced ranges are inclusive + stop = len(columns) + return columns[slice(start, stop)] + raise IndexError + + +class MSeriesGroupby(MGrouper): + """ + like a MGrouper but limited to one column + """ + + def count(self): + """ + return series count + + :return: counts by group + """ + # MGrouper will insert a _count column, see _count(). we remove + # that column again and return a series named as the group column + resultdf = super(MSeriesGroupby, self).count() + count_column = [col for col in resultdf.columns + if col.endswith('_count')][0] + new_column = count_column.replace('_count', '') + resultdf = resultdf.rename(columns={count_column: new_column}) + return resultdf[new_column] + + +class MDataFrame(object): + """ + A DataFrame for mongodb + + Performs out-of-core, lazy computation on a mongodb cluster. + Behaves like a pandas DataFrame. Actual results are returned + as pandas DataFrames. + """ + + STATFUNCS = ['mean', 'std', 'min', 'max', 'sum', 'var'] + + def __init__(self, collection, columns=None, query=None, + limit=None, skip=None, sort_order=None, + force_columns=None, immediate_loc=False, auto_inspect=False, + preparefn=None, **kwargs): + self.collection = PickableCollection(collection) + # columns in frame + self.columns = make_tuple(columns) if columns else self._get_fields() + self.columns = [str(col) for col in self.columns] + # columns to sort by, defaults to not sorted + self.sort_order = sort_order + # top n documents to fetch + self.head_limit = limit + # top n documents to skip before returning + self.skip_topn = skip + # filter criteria + self.filter_criteria = query or {} + # force columns -- on output add columns not present + self.force_columns = force_columns or [] + # was this created from the loc indexer? + self.from_loc_indexer = kwargs.get('from_loc_indexer', False) + # was the loc index used a range? Else a single value + self.from_loc_range = None + # setup query for filter criteries, if provided + if self.filter_criteria: + # make sure we have a filtered collection with the criteria given + if isinstance(self.filter_criteria, dict): + self.query_inplace(**self.filter_criteria) + elif isinstance(self.filter_criteria, Filter): + self.query_inplace(self.filter_criteria) + else: + raise ValueError('Invalid query specification of type {}'.format(type(self.filter_criteria))) + # if immediate_loc is True, .loc and .iloc always evaluate + self.immediate_loc = immediate_loc + # __array__ will return this value if it is set, set it otherwise + self._evaluated = None + # set true to automatically capture inspects on .value. retrieve using .inspect(cached=True) + self.auto_inspect = auto_inspect + self._inspect_cache = INSPECT_CACHE + # apply mixins + self._applyto = str(self.__class__) + self._apply_mixins() + # prepare function to be applied just before returning from .value + self._preparefn = preparefn + + def _apply_mixins(self, *args, **kwargs): + """ + apply mixins in defaults.OMEGA_MDF_MIXINS + """ + for mixin, applyto in defaults.OMEGA_MDF_MIXINS: + if any(v in self._applyto for v in applyto.split(',')): + extend_instance(self, mixin, *args, **kwargs) + + def __getstate__(self): + # pickle support. note that the hard work is done in PickableCollection + data = dict(self.__dict__) + data.update(_evaluated=None) + data.update(_inspect_cache=None) + data.update(auto_inspect=None) + data.update(_preparefn=None) + data.update(collection=self.collection) + return data + + def __setstate__(self, state): + # pickle support. note that the hard work is done in PickableCollection + self.__dict__.update(**state) + + def _getcopy_kwargs(self, without=None): + """ return all parameters required on a copy of this MDataFrame """ + kwargs = dict(columns=self.columns, + sort_order=self.sort_order, + limit=self.head_limit, + skip=self.skip_topn, + from_loc_indexer=self.from_loc_indexer, + immediate_loc=self.immediate_loc, + query=self.filter_criteria, + auto_inspect=self.auto_inspect, + preparefn=self._preparefn) + [kwargs.pop(k) for k in make_tuple(without or [])] + return kwargs + + def __array__(self): + # FIXME inefficient. make MDataFrame a drop-in replacement for any numpy ndarray + # this evaluates every single time + if self._evaluated is None: + self._evaluated = array = self.value.as_matrix() + else: + array = self._evaluated + return array + + def __getattr__(self, attr): + if attr in MDataFrame.STATFUNCS: + return self.statfunc(attr) + if attr in self.columns: + kwargs = self._getcopy_kwargs() + kwargs.update(columns=attr) + return MSeries(self.collection, **kwargs) + raise AttributeError(attr) + + def __getitem__(self, cols_or_slice): + """ + select and project by column, columns, slice, masked-style filter + + Masked-style filters work similar to pd.DataFrame/Series masks + but do not actually return masks but an instance of Filter. A + Filter is a delayed evaluation on the data frame. + + # select all rows where any column is == 5 + mdf = MDataFrame(coll) + flt = mdf == 5 + mdf[flt] + => + + :param cols_or_slice: single column (str), multi-columns (list), + slice to select columns or a masked-style + :return: filtered MDataFrame or MSeries + """ + if isinstance(cols_or_slice, six.string_types): + # column name => MSeries + return self._as_mseries(cols_or_slice) + elif isinstance(cols_or_slice, int): + # column number => MSeries + column = self.columns[cols_or_slice] + return self._as_mseries(column) + elif isinstance(cols_or_slice, (tuple, list)): + # list of column names => MDataFrame subset on columns + kwargs = self._getcopy_kwargs() + kwargs.update(columns=cols_or_slice) + return MDataFrame(self.collection, **kwargs) + elif isinstance(cols_or_slice, Filter): + kwargs = self._getcopy_kwargs() + kwargs.update(query=cols_or_slice.query) + return MDataFrame(self.collection, **kwargs) + elif isinstance(cols_or_slice, np.ndarray): + raise NotImplemented + raise ValueError('unknown accessor type %s' % type(cols_or_slice)) + + def __setitem__(self, column, value): + # True for any scalar type, numeric, bool, string + if np.isscalar(value): + result = self.collection.update_many(filter=self.filter_criteria, + update=qops.SET(column, value)) + self.columns.append(column) + return self + + def statfunc(self, stat): + aggr = MGrouper(self, self.collection, [], sort=False) + return getattr(aggr, stat) + + def groupby(self, columns, sort=True): + """ + Group by a given set of columns + + :param columns: the list of columns + :param sort: if True sort by group key + :return: MGrouper + """ + return MGrouper(self, self.collection, columns, sort=sort) + + def _get_fields(self): + doc = self.collection.find_one() + if doc is None: + result = [] + else: + result = [col for col in doc.keys() + if col != '_id' + and not col.startswith('_idx') + and not col.startswith('_om#')] + return result + + def _get_frame_index(self): + """ return the dataframe's index columns """ + doc = self.collection.find_one() + if doc is None: + result = [] + else: + result = restore_index_columns_order(doc.keys()) + return result + + def _get_frame_om_fields(self): + """ return the dataframe's omega special fields columns """ + doc = self.collection.find_one() + if doc is None: + result = [] + else: + result = [k for k in list(doc.keys()) if k.startswith('_om#')] + return result + + def _as_mseries(self, column): + kwargs = self._getcopy_kwargs() + kwargs.update(columns=make_tuple(column)) + return MSeries(self.collection, **kwargs) + + def inspect(self, explain=False, cached=False, cursor=None, raw=False): + """ + inspect this dataframe's actual mongodb query + + :param explain: if True explains access path + """ + if not cached: + if isinstance(self.collection, FilteredCollection): + query = self.collection.query + else: + query = '*', + if explain: + cursor = cursor or self._get_cursor() + explain = cursor.explain() + data = { + 'projection': self.columns, + 'query': query, + 'explain': explain or 'specify explain=True' + } + else: + data = self._inspect_cache + if not (raw or explain): + data = pd.DataFrame(pd.io.json.json_normalize(data)) + return data + + def count(self): + """ + projected number of rows when resolving + """ + counts = pd.Series({ + col: len(self) + for col in self.columns}, index=self.columns) + return counts + + def __len__(self): + """ + the projected number of rows when resolving + """ + return self._get_cursor().count() + + @property + def shape(self): + """ + return shape of dataframe + """ + return len(self), len(self.columns) + + @property + def ndim(self): + return len(self.shape) + + @property + def value(self): + """ + resolve the query and return a Pandas DataFrame + + :return: the result of the query as a pandas DataFrame + """ + cursor = self._get_cursor() + df = self._get_dataframe_from_cursor(cursor) + if self.auto_inspect: + self._inspect_cache.append(self.inspect(explain=True, cursor=cursor, raw=True)) + # this ensures the equiv. of pandas df.loc[n] is a Series + if self.from_loc_indexer: + if len(df) == 1 and not self.from_loc_range: + idx = df.index + df = df.T + df = df[df.columns[0]] + if df.ndim == 1 and len(df) == 1 and not isinstance(idx, pd.MultiIndex): + # single row single dimension, numeric index only + df = df.iloc[0] + elif (df.ndim == 1 or df.shape[1] == 1) and not self.from_loc_range: + df = df[df.columns[0]] + if self._preparefn: + df = self._preparefn(df) + return df + + def _get_dataframe_from_cursor(self, cursor): + """ + from the given cursor return a DataFrame + """ + df = cursor_to_dataframe(cursor) + df = self._restore_dataframe_proper(df) + return df + + def _restore_dataframe_proper(self, df): + df = restore_index(df, dict()) + if '_id' in df.columns: + df.drop('_id', axis=1, inplace=True) + if self.force_columns: + missing = set(self.force_columns) - set(self.columns) + for col in missing: + df[col] = np.NaN + return df + + def _get_cursor(self): + projection = make_tuple(self.columns) + projection += make_tuple(self._get_frame_index()) + if not self.sort_order: + # implicit sort + projection += make_tuple(self._get_frame_om_fields()) + cursor = self.collection.find(projection=projection) + if self.sort_order: + cursor.sort(qops.make_sortkey(make_tuple(self.sort_order))) + if self.head_limit: + cursor.limit(self.head_limit) + if self.skip_topn: + cursor.skip(self.skip_topn) + return cursor + + def sort(self, columns): + """ + sort by specified columns + + :param columns: str of single column or a list of columns. Sort order + is specified as the + (ascending) or - (descending) + prefix to the column name. Default sort order is + ascending. + :return: the MDataFrame + """ + self._evaluated = None + self.sort_order = make_tuple(columns) + return self + + def head(self, limit=10): + """ + return up to limit numbers of rows + + :param limit: the number of rows to return. Defaults to 10 + :return: the MDataFrame + """ + self._evaluated = None + self.head_limit = limit + return self + + def tail(self, limit=10): + """ + return up to limit number of rows from last inserted values + + :param limit: + :return: + """ + self._evaluated = None + self.skip(len(self) - limit) + return self + + def skip(self, topn): + """ + skip the topn number of rows + + :param topn: the number of rows to skip. + :return: the MDataFrame + """ + self._evaluated = None + self.skip_topn = topn + return self + + def merge(self, right, on=None, left_on=None, right_on=None, + how='inner', target=None, suffixes=('_x', '_y'), + sort=False, inspect=False): + """ + merge this dataframe with another dataframe. only left outer joins + are currently supported. the output is saved as a new collection, + target name (defaults to a generated name if not specified). + + :param right: the other MDataFrame + :param on: the list of key columns to merge by + :param left_on: the list of the key columns to merge on this dataframe + :param right_on: the list of the key columns to merge on the other + dataframe + :param how: the method to merge. supported are left, inner, right. + Defaults to inner + :param target: the name of the collection to store the merge results + in. If not provided a temporary name will be created. + :param suffixes: the suffixes to apply to identical left and right + columns + :param sort: if True the merge results will be sorted. If False the + MongoDB natural order is implied. + :returns: the MDataFrame to the target MDataFrame + """ + # validate input + supported_how = ["left", 'inner', 'right'] + assert how in supported_how, "only %s merges are currently supported" % supported_how + for key in [on, left_on, right_on]: + if key: + assert isinstance( + key, six.string_types), "only single column merge keys are supported (%s)" % key + if isinstance(right, Collection): + right = MDataFrame(right) + assert isinstance( + right, MDataFrame), "both must be MDataFrames, got right=%" % type(right) + if how == 'right': + # A right B == B left A + return right.merge(self, on=on, left_on=right_on, right_on=left_on, + how='left', target=target, suffixes=suffixes) + # generate lookup parameters + on = on or '_id' + right_name = self._get_collection_name_of(right, right) + target_name = self._get_collection_name_of( + target, '_temp.merge.%s' % uuid4().hex) + target_field = ( + "%s_%s" % (right_name.replace('.', '_'), right_on or on)) + lookup = qops.LOOKUP(right_name, + key=on, + left_key=left_on, + right_key=right_on, + target=target_field) + # unwind merged documents from arrays to top-level document fields + unwind = qops.UNWIND(target_field, preserve=how != 'inner') + # get all fields from left, right + project = {} + for left_col in self.columns: + source_left_col = left_col + if left_col == '_id': + project[left_col] = 1 + continue + if left_col.startswith('_idx'): + continue + if left_col.startswith('_om#'): + continue + if left_col != (on or left_on) and left_col in right.columns: + left_col = '%s%s' % (left_col, suffixes[0]) + project[left_col] = "$%s" % source_left_col + for right_col in right.columns: + if right_col == '_id': + continue + if right_col.startswith('_idx'): + continue + if right_col.startswith('_om#'): + continue + if right_col == (on or right_on) and right_col == (on or left_on): + # if the merge field is the same in both frames, we already + # have it from left + continue + if right_col in self.columns: + left_col = '%s%s' % (right_col, suffixes[1]) + else: + left_col = '%s' % right_col + project[left_col] = '$%s.%s' % (target_field, right_col) + expected_columns = list(project.keys()) + project = {"$project": project} + # store merged documents and return an MDataFrame to it + out = qops.OUT(target_name) + pipeline = [lookup, unwind, project] + if sort: + sort_cols = make_list(on or [left_on, right_on]) + sort_key = qops.make_sortkey(sort_cols) + sort = qops.SORT(**dict(sort_key)) + pipeline.append(sort) + pipeline.append(out) + if inspect: + result = pipeline + else: + result = self.collection.aggregate(pipeline) + result = MDataFrame(self.collection.database[target_name], + force_columns=expected_columns) + return result + + def append(self, other): + if isinstance(other, Collection): + right = MDataFrame(other) + assert isinstance( + other, MDataFrame), "both must be MDataFrames, got other={}".format(type(other)) + outname = self.collection.name + mrout = { + 'merge': outname, + 'nonAtomic': True, + } + mapfn = Code(""" + function() { + this._id = ObjectId(); + if(this['_om#rowid']) { + this['_om#rowid'] += %s; + } + emit(this._id, this); + } + """ % len(self)) + reducefn = Code(""" + function(key, value) { + return value; + } + """) + finfn = Code(""" + function(key, value) { + return value; + } + """) + other.collection.map_reduce(mapfn, reducefn, mrout, finalize=finfn, jsMode=True) + unwind = { + "$replaceRoot": { + "newRoot": { + "$ifNull": ["$value", "$$CURRENT"], + } + } + } + output = qops.OUT(outname) + pipeline = [unwind, output] + self.collection.aggregate(pipeline) + return self + + def _get_collection_name_of(self, some, default=None): + """ + determine the collection name of the given parameter + + returns the collection name if some is a MDataFrame, a Collection + or a string_type. Otherwise returns default + """ + if isinstance(some, MDataFrame): + name = some.collection.name + elif isinstance(some, Collection): + name = some.name + else: + name = default + return name + + def _get_filter_criteria(self, *args, **kwargs): + """ + return mongo query from filter specs + + this uses a Filter to produce the query from the kwargs. + + :param args: a Q object or logical combination of Q objects + (optional) + :param kwargs: all AND filter criteria + """ + if len(args) > 0: + q = args[0] + if isinstance(q, MongoQ): + filter_criteria = Filter(self.collection, q).query + elif isinstance(q, Filter): + filter_criteria = Filter(self.collection, q.q).query + else: + filter_criteria = Filter(self.collection, **kwargs).query + return filter_criteria + + def query_inplace(self, *args, **kwargs): + """ + filters this MDataFrame and returns it. + + Any subsequent operation on the dataframe will have the filter + applied. To reset the filter call .reset() without arguments. + + :param args: a Q object or logical combination of Q objects + (optional) + :param kwargs: all AND filter criteria + :return: self + """ + self._evaluated = None + self.filter_criteria = self._get_filter_criteria(*args, **kwargs) + self.collection = FilteredCollection( + self.collection, query=self.filter_criteria) + return self + + def query(self, *args, **kwargs): + """ + return a new MDataFrame with a filter criteria + + Any subsequent operation on the new dataframe will have the filter + applied. To reset the filter call .reset() without arguments. + + Note: Unlike pandas DataFrames, a filtered MDataFrame operates + on the same collection as the original DataFrame + + :param args: a Q object or logical combination of Q objects + (optional) + :param kwargs: all AND filter criteria + :return: a new MDataFrame with the filter applied + """ + effective_filter = dict(self.filter_criteria) + filter_criteria = self._get_filter_criteria(*args, **kwargs) + if '$and' in effective_filter: + effective_filter['$and'].extend(filter_criteria.get('$and')) + else: + effective_filter.update(filter_criteria) + coll = FilteredCollection(self.collection, query=effective_filter) + return self.__class__(coll, query=effective_filter, + **self._getcopy_kwargs(without='query')) + + def create_index(self, keys, **kwargs): + """ + create and index the easy way + """ + keys, kwargs = MongoQueryOps().make_index(keys) + result = self.collection.create_index(keys, **kwargs) + return result + + def list_indexes(self): + """ + list all indices in database + """ + return cursor_to_dataframe(self.collection.list_indexes()) + + @property + def loc(self): + """ + Access by index + + Use as mdf.loc[index_value] + + :return: MLocIndexer + """ + self._evaluated = None + indexer = MLocIndexer(self) + return indexer + + @property + def iloc(self): + self._evaluated = None + indexer = MPosIndexer(self) + return indexer + + def __repr__(self): + kwargs = ', '.join('{}={}'.format(k, v) for k, v in six.iteritems(self._getcopy_kwargs())) + return "MDataFrame(collection={collection.name}, {kwargs})".format(collection=self.collection, + kwargs=kwargs) + + +class MSeries(MDataFrame): + """ + Series implementation for MDataFrames + + behaves like a DataFrame but limited to one column. + """ + + def __init__(self, *args, **kwargs): + super(MSeries, self).__init__(*args, **kwargs) + # true if only unique values apply + self.is_unique = False + # apply mixins + self._applyto = str(self.__class__) + self._apply_mixins(*args, **kwargs) + + def __getitem__(self, cols_or_slice): + if isinstance(cols_or_slice, Filter): + return MSeries(self.collection, columns=self.columns, + query=cols_or_slice.query) + return super(MSeries, self).__getitem__(cols_or_slice) + + @property + def name(self): + return self.columns[0] + + def unique(self): + """ + return the unique set of values for the series + + :return: MSeries + """ + self.is_unique = True + return self + + def _get_cursor(self): + if self.is_unique: + # this way indexes get applied + cursor = self.collection.distinct(make_tuple(self.columns)[0]) + else: + cursor = super(MSeries, self)._get_cursor() + return cursor + + @property + def value(self): + """ + return the value of the series + + this is a Series unless unique() was called. If unique() + only distinct values are returned as an array, matching + the behavior of a Series + + :return: pandas.Series + """ + cursor = self._get_cursor() + column = make_tuple(self.columns)[0] + if self.is_unique: + # the .distinct() cursor returns a list of values + # this is to make sure we return the same thing as pandas + val = [v for v in cursor] + else: + val = self._get_dataframe_from_cursor(cursor) + val = val[column] + val.name = self.name + if len(val) == 1 and self.from_loc_indexer: + val = val.iloc[0] + if self.auto_inspect: + self._inspect_cache.append(self.inspect(explain=True, cursor=cursor, raw=True)) + if self._preparefn: + df = self._preparefn(val) + return val + + def __repr__(self): + kwargs = ', '.join('{}={}'.format(k, v) for k, v in six.iteritems(self._getcopy_kwargs())) + return "MSeries(collection={collection.name}, {kwargs})".format(collection=self.collection, + kwargs=kwargs) diff --git a/omegaml/mixins/__init__.py b/omegaml/mixins/__init__.py new file mode 100644 index 00000000..b7e45611 --- /dev/null +++ b/omegaml/mixins/__init__.py @@ -0,0 +1 @@ +from .store.projected import ProjectedMixin \ No newline at end of file diff --git a/omegaml/mixins/mdf/__init__.py b/omegaml/mixins/mdf/__init__.py new file mode 100644 index 00000000..a4d2fb7a --- /dev/null +++ b/omegaml/mixins/mdf/__init__.py @@ -0,0 +1,2 @@ +from .apply import ApplyMixin, ApplyContext, ApplyArithmetics, ApplyDateTime, ApplyString, ApplyAccumulators +from .filterops import FilterOpsMixin \ No newline at end of file diff --git a/omegaml/mixins/mdf/apply.py b/omegaml/mixins/mdf/apply.py new file mode 100644 index 00000000..1ef70add --- /dev/null +++ b/omegaml/mixins/mdf/apply.py @@ -0,0 +1,997 @@ +from itertools import product + +import hashlib +import json +from uuid import uuid4 + +import pandas as pd +import six +from omegaml import defaults +from omegaml.documents import QueryCache +from omegaml.mdataframe import MDataFrame, MSeries +from omegaml.store import qops +from omegaml.util import make_tuple, extend_instance + + +class ApplyMixin(object): + """ + Implements the apply() mixin supporting arbitrary functions to build aggregation pipelines + + Note that .apply() does not execute immediately. Instead it builds an aggregation pipeline + that is executed on MDataFrame.value. Note that .apply() calls cannot be cascaded yet, i.e. + a later .apply() will override a previous.apply(). + + See ApplyContext for usage examples. + """ + + def __init__(self, *args, **kwargs): + super(ApplyMixin, self).__init__(*args, **kwargs) + self._init_mixin(*args, **kwargs) + + def _init_mixin(self, *args, **kwargs): + self.apply_fn = kwargs.get('apply_fn', None) + # set to True if the pipeline is a facet operation + self.is_from_facet = kwargs.get('is_from_facet', False) + # index columns + self.index_columns = kwargs.get('index_columns', []) + # cache used on persist() + self.cache = kwargs.get('cache', ApplyCache()) + + def nocache(self): + self.cache = None + return self + + def reset_cache(self, full=False): + """ + Reset the apply cache + + :param full: if True will reset all caches for the collection, if False will only remove + the cache for the specific .apply operations + :return: + """ + if full: + QueryCache.objects.filter(value__collection=self.collection.name).delete() + else: + pipeline = self._build_pipeline() + key = self._make_cache_key(self.collection, pipeline) + QueryCache.objects.filter(key=key).delete() + return self + + def _make_cache_key(self, collection, pipeline): + # remove random output value + if '$out' in pipeline[-1] and pipeline[-1]['$out'].startswith('cache'): + pipeline = list(pipeline)[:-1] + spipeline = json.dumps(pipeline, sort_keys=True) + data = '{}_{}'.format(collection.name, spipeline).encode('utf-8') + key = hashlib.md5(data).hexdigest() + return key + + def _getcopy_kwargs(self, **kwargs): + kwargs = super(ApplyMixin, self)._getcopy_kwargs(**kwargs) + kwargs.update(is_from_facet=self.is_from_facet, + index_columns=self.index_columns, + cache=self.cache, + apply_fn=self.apply_fn) + return kwargs + + def noapply(self): + self.apply_fn = None + return self + + def apply(self, fn, inplace=False, preparefn=None): + if inplace: + obj = self + else: + kwargs = self._getcopy_kwargs() + kwargs.update(preparefn=preparefn) + if isinstance(self, MSeries): + obj = MSeries(self.collection, **kwargs) + else: + obj = MDataFrame(self.collection, **kwargs) + obj.apply_fn = fn + return obj + + def persist(self): + """ + Execute and store results in cache + + Any pipeline of the same operations, in the same order, on + the same collection will return the same result. + """ + # generate a cache key + pipeline = self._build_pipeline() + key = self._make_cache_key(self.collection, pipeline) + outname = 'cache_{}'.format(uuid4().hex) + value = { + 'collection': self.collection.name, + 'result': outname, + } + # do usual processing, store result + # -- note we pass pipeline to avoid processing iterators twice + pipeline.append({ + '$out': outname, + }) + cursor = self._get_cursor(pipeline=pipeline, use_cache=False) + # consume cursor to store output (via $out) + for v in cursor: + pass + # set cache + self.cache.set(key, value) + return key + + def set_index(self, columns): + self.index_columns = make_tuple(columns) + return self + + def inspect(self, explain=False, *args, **kwargs): + if self.apply_fn: + details = { + 'pipeline': self._build_pipeline() + } + if explain: + details.update(self.__dict__) + return details + return super(ApplyMixin, self).inspect(*args, explain=explain, **kwargs) + + def _execute(self): + ctx = ApplyContext(self, columns=self.columns) + try: + result = self.apply_fn(ctx) + except Exception as e: + msg = [repr(stage) for stage in ctx.stages] + [repr(e)] + raise RuntimeError(msg) + if result is None or isinstance(result, ApplyContext): + result = result or ctx + self.index_columns = self.index_columns or result.index_columns + return result + elif isinstance(result, list): + return result + elif isinstance(result, dict): + # expect a mapping of col=ApplyContext each with its own list of stages + # -- build a combined context by adding each expression + # this ensures any multi-stage projections are carried forward + facets = {} + for col, expr in six.iteritems(result): + if isinstance(expr, ApplyContext): + facets[col] = list(expr) + project = { + '$project': { + col: '$' + expr.columns[0] + }, + } + facets[col].append(project) + else: + facets[col] = expr + facet = { + '$facet': facets + } + self.is_from_facet = True + return [facet] + raise ValueError('Cannot build pipeline from apply result of type {}'.format(type(result))) + + def _build_pipeline(self): + pipeline = [] + stages = self._execute() + pipeline.extend(stages) + self._amend_pipeline(pipeline) + return pipeline + + def _amend_pipeline(self, pipeline): + """ amend pipeline with default ops on coll.aggregate() calls """ + if self.sort_order: + sort = qops.SORT(**dict(qops.make_sortkey(self.sort_order))) + pipeline.append(sort) + return pipeline + + def _get_cached_cursor(self, pipeline=None, use_cache=True): + pipeline = pipeline or self._build_pipeline() + if use_cache and self.cache: + key = self._make_cache_key(self.collection, pipeline) + entry = self.cache.get(key) + if entry is not None: + # read result + outname = entry.value['result'] + return self.collection.database[outname].find() + + def _get_cursor(self, pipeline=None, use_cache=True): + # for apply functions, call the apply function, expecting a pipeline in return + if self.apply_fn: + pipeline = pipeline or self._build_pipeline() + cursor = self._get_cached_cursor(pipeline=pipeline, use_cache=use_cache) + if cursor is None: + filter_criteria = self._get_filter_criteria() + cursor = self.collection.aggregate(pipeline, filter=filter_criteria) + else: + cursor = super(ApplyMixin, self)._get_cursor() + return cursor + + def _get_dataframe_from_cursor(self, cursor): + df = super(ApplyMixin, self)._get_dataframe_from_cursor(cursor) + if self.is_from_facet: + # if this was from a facet pipeline (i.e. multi-column mapping), combine + # $facet returns one document for each stage. + frames = [] + for col in df.columns: + coldf = pd.DataFrame(df[col].iloc[0]).set_index('_id') + frames.append(coldf) + df = pd.concat(frames, axis=1).reset_index() + df = self._restore_dataframe_proper(df) + # TODO write a unit test for this condition + if self.index_columns and all(col in df.columns for col in self.index_columns): + df.set_index(self.index_columns, inplace=True) + return df + + +class ApplyContext(object): + """ + Enable apply functions + + .apply(fn) will call fn(ctx) where ctx is an ApplyContext. The context supports + methods to apply functions in a Pandas-style apply manner. ApplyContext is extensible + by adding an extension class to defaults.OMEGA_MDF_APPLY_MIXINS. + + Note that unlike a Pandas DataFrame, ApplyContext does not itself contain any data. + Rather it is part of an expression tree, i.e. the aggregation pipeline. Thus any + expressions applied are translated into operations on the expression tree. The expression + tree is evaluated on MDataFrame.value, at which point the ApplyContext nor the function + that created it are active. + + Examples: + + mdf.apply(lambda v: v * 5 ) => multiply every column in dataframe + mdf.apply(lambda v: v['foo'].dt.week) => get week of date for column foo + mdf.apply(dict(a=v['foo'].dt.week, + b=v['bar'] * 5) => run multiple pipelines and get results + + The callable passed to apply can be any function. It can either return None, + the context passed in or a list of pipeline stages. + + # apply any of the below functions + mdf.apply(customfn) + + # same as lambda v: v.dt.week + def customfn(ctx): + return ctx.dt.week + + # simple pipeline + def customfn(ctx): + ctx.project(x={'$multiply: ['$x', 5]}) + ctx.project(y={'$divide: ['$x', 2]}) + + # complex pipeline + def customfn(ctx): + return [ + { '$match': ... }, + { '$project': ... }, + ] + """ + + def __init__(self, caller, columns=None, index=None): + self.caller = caller + self.columns = columns + self.index_columns = index or [] + self.computed = [] + self.stages = [] + self.expressions = [] + self._apply_mixins() + + def _apply_mixins(self): + """ + apply mixins in defaults.OMEGA_MDF_APPLY_MIXINS + """ + for mixin, applyto in defaults.OMEGA_MDF_APPLY_MIXINS: + if any(v in self.caller._applyto for v in applyto.split(',')): + extend_instance(self, mixin) + + def __iter__(self): + # return pipeline stages + for stage in self.stages: + if isinstance(stage, ApplyContext): + for sub_stage in stage: + yield sub_stage + else: + yield stage + + def __getitem__(self, sel): + """ + return a stage subset on a column + """ + subctx = ApplyContext(self.caller, columns=make_tuple(sel), index=self.index_columns) + self.add(subctx) + return subctx + + def __setitem__(self, sel, val): + """ + add a projection to a sub context + + ctx['col'] = value-expression + """ + mapping = { + col: v + for (col, v) in zip(make_tuple(sel), make_tuple(val))} + self.project(mapping) + + def __repr__(self): + return 'ApplyContext(stages={}, expressions={})'.format(self.stages, self.expressions) + + def add(self, stage): + """ + Add a processing stage to the pipeline + + see https://docs.mongodb.com/manual/meta/aggregation-quick-reference/ + """ + self.stages.append(stage) + return self + + def project_keeper_columns(self): + # keep index, computed + index = { + col: '$' + col + for col in self.index_columns} + computed = { + col: '$' + col + for col in self.computed} + keep = {} + keep.update(index) + keep.update(computed) + project = self.project(keep, keep=True) + return project + + def _getLastStageKind(self, kind): + # see if there is already an open projection stage + for stage in self.stages[::-1]: + if kind in stage: + return stage + + def _getProjection(self, append=False): + stage = self._getLastStageKind('$project') + if stage is None or append: + stage = { + '$project': { + '_id': 1, + } + } + self.stages.append(stage) + return stage + + def _getGroupBy(self, by=None, append=False): + stage = self._getLastStageKind('$group') + if stage and stage['$group']['_id'] != by and by != '$$last': + # if a different groupby criteria, add a new one + stage = None + if stage is None and by == '$$last': + by = None + if stage is None or append: + stage = { + '$group': { + '_id': by, + } + } + self.stages.append(stage) + return stage + + def groupby(self, by, expr=None, append=None, **kwargs): + """ + add a groupby accumulation using $group + + :param by: the groupby columns, if provided as a list will be transformed + :param expr: + :param append: + :param kwargs: + :return: + """ + by = make_tuple(by) + self.index_columns = self.index_columns + list(by) + # define groupby + by = {col: '$' + col for col in by} + stage = self._getGroupBy(by) + groupby = stage['$group'] + # add acccumulators + expr = expr or { + col: colExpr + for col, colExpr in six.iteritems(kwargs)} + groupby.update(expr) + # add a projection to extract groupby values + extractId = { + col: '$_id.' + col + for col in by} + # add a projection to keep accumulator columns + keepCols = { + col: 1 + for col in expr} + keepCols.update(extractId) + self.project(keepCols, append=True) + # sort by groupby keys + self.add({ + '$sort': { + col: 1 + for col in by} + }) + return self + + def project(self, expr=None, append=False, keep=False, **kwargs): + """ + add a projection using $project + + :param expr: the column-operator mapping + :param append: if True add a $project stage, otherwise add to existing + :param kwargs: if expr is None, the column-operator mapping as kwargs + :return: ApplyContext + """ + # get last $project stage in pipeline + stage = self._getProjection(append=append) + expr = expr or kwargs + self.expressions.append(expr) + for k, v in six.iteritems(expr): + # only append to stage if no other column projection was there + project = stage.get('$project') + if k not in project: + project.update({ + k: v + }) + elif not keep: + # if a column is already projected, add a new projection stage + stage = self._getProjection(append=True) + project = stage.get('$project') + project.update({ + k: v + }) + return self + + +class ApplyArithmetics(object): + """ + Math operators for ApplyContext + + * :code:`__mul__` (*) + * :code:`__add__` (+) + * :code:`__sub__` (-) + * :code:`__div__` (/) + * :code:`__floordiv__` (//) + * :code:`__mod__` (%) + * :code:`__pow__` (pow) + * :code:`__ceil__` (ceil) + * :code:`__floor__` (floor) + * :code:`__trunc__` (trunc) + * :code:`__abs__` (abs) + * :code:`sqrt` (math.sqrt) + """ + + def __arithmop__(op, wrap_op=None): + """ + return a pipeline $project stage math operator as + { col: + { '$operator': [ values, ...] } + ... + } + + If wrap_op is specified, will wrap the $operator clause as + { col: + { '$wrap_op': { '$operator': [ values, ...] } }0 + ... + } + """ + + def inner(self, other): + terms = [] + for term in make_tuple(other): + if isinstance(term, six.string_types): + term = '$' + term + terms.append(term) + def wrap(expr): + if wrap_op is not None: + expr = { + wrap_op: expr + } + return expr + mapping = { + col: wrap({ + op: ['$' + col] + terms, + }) for col in self.columns} + keepCols = { + col: '$' + col + for col in self.index_columns} + mapping.update(keepCols) + self.project(mapping) + return self + + return inner + + #: multiply + __mul__ = __arithmop__('$multiply') + #: add + __add__ = __arithmop__('$add') + #: subtract + __sub__ = __arithmop__('$subtract') + #: divide + __div__ = __arithmop__('$divide') + __truediv__ = __arithmop__('$divide') + #: divide integer + __floordiv__ = __arithmop__('$divide', wrap_op='$floor') + #: modulo (%) + __mod__ = __arithmop__('$mod') + #: pow + __pow_ = __arithmop__('$pow') + #: ceil + __ceil__ = __arithmop__('$ceil') + #: floor + __floor__ = __arithmop__('$floor') + #: truncate + __trunc__ = __arithmop__('$trunc') + #: absolute + __abs__ = __arithmop__('$abs') + #: square root + sqrt = __arithmop__('sqrt') + + +class ApplyDateTime(object): + """ + Datetime operators for ApplyContext + """ + + @property + def dt(self): + return self + + def __dtop__(op): + """ + return a datetime $project operator as + { col: + { '$operator': '$col} } + ... + } + """ + + def inner(self, columns=None): + columns = make_tuple(columns or self.columns) + mapping = { + col: { + op: '$' + col, + } + for col in columns} + self.project(mapping) + return self + + inner.__doc__ = op.replace('$', '') + return inner + + # mongodb mappings + _year = __dtop__('$year') + _month = __dtop__('$month') + _week = __dtop__('$week') + _dayOfWeek = __dtop__('$dayOfWeek') + _dayOfMonth = __dtop__('$dayOfMonth') + _dayOfYear = __dtop__('$dayOfYear') + _hour = __dtop__('$hour') + _minute = __dtop__('$minute') + _second = __dtop__('$second') + _millisecond = __dtop__('$millisecond') + _isoDayOfWeek = __dtop__('$isoDayOfWeek') + _isoWeek = __dtop__('$isoWeek') + _isoWeekYear = __dtop__('$isoWeekYear') + + # .dt accessor convenience similar to pandas.dt + # see https://pandas.pydata.org/pandas-docs/stable/api.html#datetimelike-properties + year = property(_year) + month = property(_month) + day = property(_dayOfMonth) + hour = property(_hour) + minute = property(_minute) + second = property(_second) + millisecond = property(_millisecond) + week = property(_isoWeek) + dayofyear = property(_dayOfYear) + dayofweek = property(_dayOfWeek) + + +class ApplyString(object): + """ + String operators + """ + + @property + def str(self): + return self + + def __strexpr__(op, unwind=False, base=None, max_terms=None): + """ + return a pipeline $project string operator as + { col: + { '$operator': [ values, ...] } + ... + } + """ + + def inner(self, other, *args): + # get all values passed and build terms from them + values = list(make_tuple(other) + args) + terms = [] + for term in values: + if isinstance(term, six.string_types): + # if the term is a column name, add as a column name + if term in self.columns: + term = '$' + term + # allow to specify values explicitely by $$ => + term = term.replace('$$', '') + terms.append(term) + # limit number of terms if requested + if max_terms: + terms = terms[:max_terms] + # add projection of output columns to operator + mapping = { + col: { + op: terms if base is None else ['$' + col] + terms, + } for col in self.columns} + self.project(mapping) + # unwind all columns if requested + if unwind: + exprs = [{'$unwind': { + 'path': '$' + col + }} for col in self.columns] + self.stages.extend(exprs) + return self + + inner.__doc__ = op.replace('$', '') + return inner + + def __strunary__(op, unwind=False): + """ + return a datetime $project operator as + { col: + { '$operator': '$col} } + ... + } + """ + + def inner(self, columns=None): + columns = make_tuple(columns or self.columns) + mapping = { + col: { + op: '$' + col, + } + for col in columns} + self.project(mapping) + if unwind: + self.stages.append({ + '$unwind': { + '' + } + }) + return self + + inner.__doc__ = op.replace('$', '') + + return inner + + def isequal(self, other): + self.strcasecmp(other) + # strcasecmp returns 0 for equality, 1 and -1 for greater/less than + # https://docs.mongodb.com/manual/reference/operator/aggregation/strcasecmp/ + mapping = { + col: { + '$cond': { + 'if': {'$eq': ['$' + col, 0]}, + 'then': True, + 'else': False, + } + } + for col in self.columns} + self.project(mapping) + + concat = __strexpr__('$concat', base=True) + split = __strexpr__('$split', unwind=True, base=True, max_terms=2) + upper = __strunary__('$toUpper') + lower = __strunary__('$toLower') + substr = __strexpr__('$substr', base=True) + strcasecmp = __strexpr__('$strcasecmp', base=True) + len = __strunary__('$strLenBytes') + index = __strexpr__('$indexOfBytes', base=True) + + +class ApplyAccumulators(object): + def agg(self, map=None, **kwargs): + stage = self._getGroupBy(by='$$last') + specs = map or kwargs + for col, colExpr in six.iteritems(specs): + if isinstance(colExpr, dict): + # specify an arbitrary expression + groupby = stage['$group'] + groupby[col] = colExpr + elif isinstance(colExpr, six.string_types): + # specify some known operator + if hasattr(self, colExpr): + method = getattr(self, colExpr) + method(col) + else: + raise SyntaxError('{} is not known'.format(colExpr)) + elif isinstance(colExpr, (tuple, list)): + # specify a list of some known operators + for statExpr in colExpr: + if hasattr(self, statExpr): + method = getattr(self, statExpr) + method(col) + else: + raise SyntaxError('{} is not known'.format(statExpr)) + elif callable(colExpr): + # specify a callable that returns an expression + groupby = stage['$group'] + groupby[col] = colExpr(col) + else: + SyntaxError('{} on column {} is unknown or invalid'.format(colExpr, col)) + return self + + def __statop__(op, opname=None): + opname = opname or op.replace('$', '') + + def inner(self, columns=None): + columns = make_tuple(columns or self.columns) + stage = self._getGroupBy(by='$$last') + groupby = stage['$group'] + groupby.update({ + '{}_{}'.format(col, opname): { + op: '$' + col + } for col in columns + }) + self.computed.extend(groupby.keys()) + self.project_keeper_columns() + return self + + return inner + + sum = __statop__('$sum') + avg = __statop__('$avg') + mean = __statop__('$avg') + min = __statop__('$min') + max = __statop__('$max') + std = __statop__('$stdDevSamp', 'std') + + +class ApplyCache(object): + """ + A Cache that works on collections and pipelines + """ + + def set(self, key, value): + # https://stackoverflow.com/a/22003440/890242 + QueryCache.objects(key=key).update_one(set__key="{}".format(key), + set__value=value, upsert=True) + + def get(self, key): + try: + result = QueryCache.objects.get(key=key) + except: + result = None + return result + + +class ApplyStatistics(object): + def quantile(self, q=.5): + def preparefn(val): + return val.pivot('percentile', 'var', 'value') + return self.apply(self._percentile(q), preparefn=preparefn) + + def cov(self): + def preparefn(val): + val = val.pivot('x', 'y', 'cov') + val.index.name = None + val.columns.name = None + return val + return self.apply(self._covariance, preparefn=preparefn) + + def corr(self): + def preparefn(val): + val = val.pivot('x', 'y', 'rho') + val.index.name = None + val.columns.name = None + return val + return self.apply(self._pearson, preparefn=preparefn) + + def _covariance(self, ctx): + # this works + # source http://ci.columbia.edu/ci/premba_test/c0331/s7/s7_5.html + facets = {} + means = {} + unwinds = [] + count = len(ctx.caller.noapply()) - 1 + for x, y in product(ctx.columns, ctx.columns): + xcol = '$' + x + ycol = '$' + y + # only calculate the same column's mean once + if xcol not in means: + means[xcol] = ctx.caller[x].noapply().mean().values[0, 0] + if ycol not in means: + means[ycol] = ctx.caller[y].noapply().mean().values[0, 0] + sumands = { + xcol: { + '$subtract': [xcol, means[xcol]] + }, + ycol: { + '$subtract': [ycol, means[ycol]] + } + } + multiply = { + '$multiply': [sumands[xcol], sumands[ycol]] + } + agg = { + '$group': { + '_id': None, + 'value': { + '$sum': multiply + } + } + } + project = { + '$project': { + 'cov': { + '$divide': ['$value', count], + }, + 'x': x, + 'y': y, + } + } + pipeline = [agg, project] + outcol = '{}_{}'.format(x, y) + facets[outcol] = pipeline + unwinds.append({'$unwind': '$' + outcol}) + facet = { + '$facet': facets, + } + expand = [{ + '$project': { + 'value': { + '$objectToArray': '$$CURRENT', + } + } + }, { + '$unwind': '$value' + }, { + '$replaceRoot': { + 'newRoot': '$value.v' + } + }] + return [facet, *unwinds, *expand] + + def _pearson(self, ctx): + # this works + # source http://ilearnasigoalong.blogspot.ch/2017/10/calculating-correlation-inside-mongodb.html + facets = {} + unwinds = [] + for x, y in product(ctx.columns, ctx.columns): + xcol = '$' + x + ycol = '$' + y + sumcolumns = {'$group': {'_id': None, + 'count': {'$sum': 1}, + 'sumx': {'$sum': xcol}, + 'sumy': {'$sum': ycol}, + 'sumxsquared': {'$sum': {'$multiply': [xcol, xcol]}}, + 'sumysquared': {'$sum': {'$multiply': [ycol, ycol]}}, + 'sumxy': {'$sum': {'$multiply': [xcol, ycol]}} + }} + + multiply_sumx_sumy = {'$multiply': ["$sumx", "$sumy"]} + multiply_sumxy_count = {'$multiply': ["$sumxy", "$count"]} + partone = {'$subtract': [multiply_sumxy_count, multiply_sumx_sumy]} + + multiply_sumxsquared_count = {'$multiply': ["$sumxsquared", "$count"]} + sumx_squared = {'$multiply': ["$sumx", "$sumx"]} + subparttwo = {'$subtract': [multiply_sumxsquared_count, sumx_squared]} + + multiply_sumysquared_count = {'$multiply': ["$sumysquared", "$count"]} + sumy_squared = {'$multiply': ["$sumy", "$sumy"]} + subpartthree = {'$subtract': [multiply_sumysquared_count, sumy_squared]} + + parttwo = {'$sqrt': {'$multiply': [subparttwo, subpartthree]}} + + rho = {'$project': { + 'rho': { + '$divide': [partone, parttwo] + }, + 'x': x, + 'y': y + }} + pipeline = [sumcolumns, rho] + outcol = '{}_{}'.format(x, y) + facets[outcol] = pipeline + unwinds.append({'$unwind': '$' + outcol}) + facet = { + '$facet': facets, + } + expand = [{ + '$project': { + 'value': { + '$objectToArray': '$$CURRENT', + } + } + }, { + '$unwind': '$value' + }, { + '$replaceRoot': { + 'newRoot': '$value.v' + } + }] + return [facet, *unwinds, *expand] + + def _percentile(self, pctls=None): + """ + calculate percentiles for all columns + """ + pctls = pctls or [.25, .5, .75] + if not isinstance(pctls, (list, tuple)): + pctls = [pctls] + + def calc(col, p, outcol): + # sort values + sort = { + '$sort': { + col: 1, + } + } + # group/push to get an array of all values + group = { + '$group': { + '_id': col, + 'values': { + '$push': "$" + col + }, + } + } + # find value at requested percentile + perc = { + '$arrayElemAt': [ + '$values', { + '$floor': { + '$multiply': [{ + '$size': '$values' + }, p] + }} + ] + } + # map percentile value to output column + project = { + '$project': { + 'var': col, + 'percentile': 'p{}'.format(p), + 'value': perc, + } + } + return [sort, group, project] + + def inner(ctx): + # for each column and requested percentile, build a pipeline + # all pipelines will be combined into a $facet stage to + # calculate every column/percentile tuple in parallel + facets = {} + unwind = [] + # for each column build a pipeline to calculate the percentiles + for col in ctx.columns: + for p in pctls: + # e.g. outcol for perc .25 of column abc => abcp25 + outcol = '{}_p{}'.format(col, p).replace('0.', '') + facets[outcol] = calc(col, p, outcol) + unwind.append({'$unwind': '$'+ outcol}) + # process per-column pipelines in parallel, resulting in one + # document for each variable + percentile combination + facet = { + '$facet': facets + } + # expand single document into one document per variable + percentile combo + # the resulting set of documents contains var/percentile/value + expand = [{ + '$project': { + 'value': { + '$objectToArray': '$$CURRENT', + } + } + }, { + '$unwind': '$value' + }, { + '$replaceRoot': { + 'newRoot': '$value.v' + } + }] + pipeline = [facet, *unwind, *expand] + return pipeline + + return inner + + diff --git a/omegaml/mixins/mdf/filterops.py b/omegaml/mixins/mdf/filterops.py new file mode 100644 index 00000000..de6bd42b --- /dev/null +++ b/omegaml/mixins/mdf/filterops.py @@ -0,0 +1,36 @@ +from functools import partial + +from omegaml.store import Filter +from omegaml.store import MongoQ +from omegaml.store.filtered import FilteredCollection + + +class FilterOpsMixin(object): + """ + filter operators on MSeries + """ + + def __getfltop(op): + def inner(self, other): + return self.__fltop__(op, other) + + return inner + + def __fltop__(self, op, other): + # the actual filter operator + q = None + for col in self.columns: + queryop = '{col}__{op}'.format(col=col, op=op) + qq = MongoQ(**{queryop: other}) + if q is None: + q = qq + else: + q = q | qq + return Filter(self.collection, q) + + __eq__ = __getfltop('eq') + __ne__ = __getfltop('ne') + __lt__ = __getfltop('lt') + __le__ = __getfltop('lte') + __gt__ = __getfltop('gt') + __ge__ = __getfltop('gte') diff --git a/omegaml/mixins/store/__init__.py b/omegaml/mixins/store/__init__.py new file mode 100644 index 00000000..8b2ef513 --- /dev/null +++ b/omegaml/mixins/store/__init__.py @@ -0,0 +1 @@ +from .projected import ProjectedMixin \ No newline at end of file diff --git a/omegaml/mixins/store/projected.py b/omegaml/mixins/store/projected.py new file mode 100644 index 00000000..e1ee956b --- /dev/null +++ b/omegaml/mixins/store/projected.py @@ -0,0 +1,101 @@ +import re +import pandas as pd +import six + + +class ProjectedMixin(object): + + """ + A OmegaStore mixin to process column specifications in dataset name + """ + colspec_pattern = re.compile(r"(?P.*)\[(?P.*)\]$") + + def metadata(self, name, *args, **kwargs): + if isinstance(name, six.string_types): + name = name.split('[')[0] + return super(ProjectedMixin, self).metadata(name, *args, **kwargs) + + def get(self, name, *args, **kwargs): + """ + Return a projected dataset given a name of form name[colspec] + + colspec can be any of + + * a comma separated list of columns, e.g. foo[a,b] + * an open-ended slice, e.g. foo[a:] => all columns following a, inclusive + * an closed slice, e.g. foo[a:b] => all columns between a,b, inclusive + * a close-ended slice, e.g. foo[:b] => all columns up to b, inclusive + * an empty slice, e.g. foo[:] => all columns + * a list of columns to exclude, e.g. foo[^b] => all columns except b + + :param name: (str) the name of the dataset, optionally including a + column specification + :return: the dataset with projected columns + """ + # split base name from specs, get metadata + if '[' not in name: + # no column spec in name, avoid projection + data = super(ProjectedMixin, self).get(name, *args, **kwargs) + else: + # column specs in name, get projected data + data = self._get_data_from_projection(name, *args, **kwargs) + return data + + def _get_data_from_projection(self, name, *args, **kwargs): + # see if we can get columns from metadata + # if so we can specify the columns before getting the data + specs, name = name, name.split('[', 1)[0] + meta = self.metadata(name) + if 'columns' in meta.kind_meta: + colmap = meta.kind_meta['columns'] + if isinstance(colmap, dict): + all_columns = list(colmap.keys())[1] + else: + # colmap is list of tuples (colname, storedname) + all_columns = list(zip(*colmap))[1] + name, columns = columnset(specs, all_columns) + kwargs['columns'] = columns + data = super(ProjectedMixin, self).get(name, *args, **kwargs) + else: + # we don't have columns in metadata, get the data first + # only subset on dataframes + data = super(ProjectedMixin, self).get(name, *args, **kwargs) + if isinstance(data, pd.DataFrame): + all_columns = data.columns + name, columns = columnset(specs, all_columns) + data = data[columns] + return data + + +def columnset(specs, all_columns): + """ + find the specified columns in data[colspec] in list of all columns + + colspec can be any of + + * a comma separated list of columns, e.g. foo[a,b] + * an open-ended slice, e.g. foo[a:] => all columns following a, inclusive + * an closed slice, e.g. foo[a:b] => all columns between a,b, inclusive + * a close-ended slice, e.g. foo[:b] => all columns up to b, inclusive + * an empty slice, e.g. foo[:] => all columns + * a list of columns to exclude, e.g. foo[^b] => all columns except b + """ + colspec_pattern = re.compile(r"(?P.*)\[(?P.*)\]$") + match = colspec_pattern.match(specs) + if match is not None: + specs, colspec = match.groups() + if ':' in colspec: + from_col, to_col = colspec.split(':') + from_i = (all_columns.index(from_col) + if from_col else 0) + to_i = (all_columns.index(to_col) + if to_col else len(all_columns)) + 1 + columns = all_columns[from_i:to_i] + elif colspec.startswith('^'): + columns = [col for col in all_columns + if col not in colspec[1:].split(',')] + else: + columns = colspec.split(',') + else: + columns = all_columns + return specs, columns diff --git a/omegaml/notebook/__init__.py b/omegaml/notebook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/omegaml/notebook/checkpoints.py b/omegaml/notebook/checkpoints.py new file mode 100644 index 00000000..07bbacf1 --- /dev/null +++ b/omegaml/notebook/checkpoints.py @@ -0,0 +1,30 @@ +from notebook.services.contents.checkpoints import GenericCheckpointsMixin, Checkpoints + + +class NoOpCheckpoints(GenericCheckpointsMixin, Checkpoints): + # source https://jupyter-notebook.readthedocs.io/en/stable/extending/contents.html#customizing-checkpoints + # © Copyright 2015, Jupyter Team, https://jupyter.org. Revision 775cb20d. + + def create_file_checkpoint(self, content, format, path): + """ -> checkpoint model""" + + def create_notebook_checkpoint(self, nb, path): + """ -> checkpoint model""" + + def get_file_checkpoint(self, checkpoint_id, path): + """ -> {'type': 'file', 'content': , 'format': {'text', 'base64'}}""" + + def get_notebook_checkpoint(self, checkpoint_id, path): + """ -> {'type': 'notebook', 'content': }""" + + def delete_checkpoint(self, checkpoint_id, path): + """deletes a checkpoint for a file""" + + def list_checkpoints(self, path): + """returns a list of checkpoint models for a given file, + default just does one per file + """ + return [] + + def rename_checkpoint(self, checkpoint_id, old_path, new_path): + """renames checkpoint from old path to new path""" diff --git a/omegaml/notebook/jupyter/__init__.py b/omegaml/notebook/jupyter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/omegaml/notebook/jupyter/ipystart.py b/omegaml/notebook/jupyter/ipystart.py new file mode 100644 index 00000000..7e1d5793 --- /dev/null +++ b/omegaml/notebook/jupyter/ipystart.py @@ -0,0 +1,10 @@ +# this will be executed on jupyter notebook / ipython startup +import sys +import os +base_dir = os.environ.get('OMEGA_ROOT') +if not base_dir: + base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') +sys.path.insert(0, base_dir) +import omegaml as om +print("omegaml initializing from {}".format(__file__)) +print(om.defaults.OMEGA_CONFIG_FILE) diff --git a/omegaml/notebook/jupyter/ipython_config.py b/omegaml/notebook/jupyter/ipython_config.py new file mode 100644 index 00000000..741c9d4a --- /dev/null +++ b/omegaml/notebook/jupyter/ipython_config.py @@ -0,0 +1,10 @@ +# http://ipython.readthedocs.io/en/5.x/config/intro.html#profiles +# http://ipython.readthedocs.io/en/5.x/config/intro.html#example-config-file +# note in the link above it says "you can also keep a profile in the +# current working directory". so put this file in the cwd and it will actually +# execute on kernel starts +c = get_config() +c.InteractiveShellApp.exec_files = [ + 'ipystart.py' +] +print("omegaml: ipython initialized from {}".format(__file__)) diff --git a/omegaml/notebook/jupyter/jupyter_notebook_config.py b/omegaml/notebook/jupyter/jupyter_notebook_config.py new file mode 100644 index 00000000..02788fc6 --- /dev/null +++ b/omegaml/notebook/jupyter/jupyter_notebook_config.py @@ -0,0 +1,633 @@ +# Configuration file for jupyter-notebook. +import os +import sys + + +def setup(): + # avoid IDE error + global c + return c + + +c = setup() + +# ------------------------------------------------------------------------------ +# Application(SingletonConfigurable) configuration +# ------------------------------------------------------------------------------ + +# This is an application. + +# The date format used by logging formatters for %(asctime)s +# c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' + +# The Logging format template +# c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' + +# Set the log level by value or name. +# c.Application.log_level = 30 + +# ------------------------------------------------------------------------------ +# JupyterApp(Application) configuration +# ------------------------------------------------------------------------------ + +# Base class for Jupyter applications + +# Answer yes to any prompts. +# c.JupyterApp.answer_yes = False + +# Full path of a config file. +# c.JupyterApp.config_file = '' + +# Specify a config file to load. +# c.JupyterApp.config_file_name = '' + +# Generate default config file. +# c.JupyterApp.generate_config = False + +# ------------------------------------------------------------------------------ +# NotebookApp(JupyterApp) configuration +# ------------------------------------------------------------------------------ + +# Set the Access-Control-Allow-Credentials: true header +# c.NotebookApp.allow_credentials = False + +# Set the Access-Control-Allow-Origin header +# +# Use '*' to allow any origin to access your server. +# +# Takes precedence over allow_origin_pat. +# c.NotebookApp.allow_origin = '' + +# Use a regular expression for the Access-Control-Allow-Origin header +# +# Requests from an origin matching the expression will get replies with: +# +# Access-Control-Allow-Origin: origin +# +# where `origin` is the origin of the request. +# +# Ignored if allow_origin is set. +# c.NotebookApp.allow_origin_pat = '' + +# Whether to allow the user to run the notebook as root. +c.NotebookApp.allow_root = True if os.environ.get('JY_ALLOW_ROOT') else False + +# DEPRECATED use base_url +# c.NotebookApp.base_project_url = '/' + +# The base URL for the notebook server. +# +# Leading and trailing slashes can be omitted, and will automatically be added. +# c.NotebookApp.base_url = '/' + +# Specify what command to use to invoke a web browser when opening the notebook. +# If not specified, the default browser will be determined by the `webbrowser` +# standard library module, which allows setting of the BROWSER environment +# variable to override it. +# c.NotebookApp.browser = '' + +# The full path to an SSL/TLS certificate file. +# c.NotebookApp.certfile = '' + +# The full path to a certificate authority certificate for SSL/TLS client +# authentication. +# c.NotebookApp.client_ca = '' + +# The config manager class to use +# c.NotebookApp.config_manager_class = 'notebook.services.config.manager.ConfigManager' + +# The notebook manager class to use. +# c.NotebookApp.contents_manager_class = 'notebook.services.contents.largefilemanager.LargeFileManager' + +# Extra keyword arguments to pass to `set_secure_cookie`. See tornado's +# set_secure_cookie docs for details. +# c.NotebookApp.cookie_options = {} + +# The random bytes used to secure cookies. By default this is a new random +# number every time you start the Notebook. Set it to a value in a config file +# to enable logins to persist across server sessions. +# +# Note: Cookie secrets should be kept private, do not share config files with +# cookie_secret stored in plaintext (you can read the value from a file). +# c.NotebookApp.cookie_secret = b'' + +# The file where the cookie secret is stored. +# c.NotebookApp.cookie_secret_file = '' + +# The default URL to redirect to from `/` +# c.NotebookApp.default_url = '/tree' + +# Disable cross-site-request-forgery protection +# +# Jupyter notebook 4.3.1 introduces protection from cross-site request +# forgeries, requiring API requests to either: +# +# - originate from pages served by this server (validated with XSRF cookie and +# token), or - authenticate with a token +# +# Some anonymous compute resources still desire the ability to run code, +# completely without authentication. These services can disable all +# authentication and security checks, with the full knowledge of what that +# implies. +# c.NotebookApp.disable_check_xsrf = False + +# Whether to enable MathJax for typesetting math/TeX +# +# MathJax is the javascript library Jupyter uses to render math/LaTeX. It is +# very large, so you may want to disable it if you have a slow internet +# connection, or for offline use of the notebook. +# +# When disabled, equations etc. will appear as their untransformed TeX source. +# c.NotebookApp.enable_mathjax = True + +# extra paths to look for Javascript notebook extensions +# c.NotebookApp.extra_nbextensions_path = [] + +# Extra paths to search for serving static files. +# +# This allows adding javascript/css to be available from the notebook server +# machine, or overriding individual files in the IPython +# c.NotebookApp.extra_static_paths = [] + +# Extra paths to search for serving jinja templates. +# +# Can be used to override templates from notebook.templates. +# c.NotebookApp.extra_template_paths = [] + +## +# c.NotebookApp.file_to_run = '' + +# Deprecated: Use minified JS file or not, mainly use during dev to avoid JS +# recompilation +# c.NotebookApp.ignore_minified_js = False + +# (bytes/sec) Maximum rate at which messages can be sent on iopub before they +# are limited. +# c.NotebookApp.iopub_data_rate_limit = 1000000 + +# (msgs/sec) Maximum rate at which messages can be sent on iopub before they are +# limited. +# c.NotebookApp.iopub_msg_rate_limit = 1000 + +# The IP address the notebook server will listen on. +# c.NotebookApp.ip = 'localhost' + +# Supply extra arguments that will be passed to Jinja environment. +# c.NotebookApp.jinja_environment_options = {} + +# Extra variables to supply to jinja templates when rendering. +# c.NotebookApp.jinja_template_vars = {} + +# The kernel manager class to use. +# c.NotebookApp.kernel_manager_class = 'notebook.services.kernels.kernelmanager.MappingKernelManager' + +# The kernel spec manager class to use. Should be a subclass of +# `jupyter_client.kernelspec.KernelSpecManager`. +# +# The Api of KernelSpecManager is provisional and might change without warning +# between this version of Jupyter and the next stable one. +# c.NotebookApp.kernel_spec_manager_class = 'jupyter_client.kernelspec.KernelSpecManager' + +# The full path to a private key file for usage with SSL/TLS. +# c.NotebookApp.keyfile = '' + +# The login handler class to use. +# c.NotebookApp.login_handler_class = 'notebook.auth.login.LoginHandler' + +# The logout handler class to use. +# c.NotebookApp.logout_handler_class = 'notebook.auth.logout.LogoutHandler' + +# The MathJax.js configuration file that is to be used. +# c.NotebookApp.mathjax_config = 'TeX-AMS-MML_HTMLorMML-full,Safe' + +# A custom url for MathJax.js. Should be in the form of a case-sensitive url to +# MathJax, for example: /static/components/MathJax/MathJax.js +# c.NotebookApp.mathjax_url = '' + +# Dict of Python modules to load as notebook server extensions.Entry values can +# be used to enable and disable the loading ofthe extensions. The extensions +# will be loaded in alphabetical order. +# c.NotebookApp.nbserver_extensions = {} + +# The directory to use for notebooks and kernels. +c.NotebookApp.notebook_dir = os.getcwd() + +# Whether to open in a browser after starting. The specific browser used is +# platform dependent and determined by the python standard library `webbrowser` +# module, unless it is overridden using the --browser (NotebookApp.browser) +# configuration option. +# c.NotebookApp.open_browser = True + +# Hashed password to use for web authentication. +# +# To generate, type in a python/IPython shell: +# +# from notebook.auth import passwd; passwd() +# +# The string should be of the form type:salt:hashed-password. +if 'JUPYTER_PASSWORD' in os.environ: + # unless it is actually a value in environ we must not set the attribute, as None is not a valid value + # if the value is not set a token is generated which is what we want in this case + c.NotebookApp.password = os.environ.get('JUPYTER_PASSWORD') + +# Forces users to use a password for the Notebook server. This is useful in a +# multi user environment, for instance when everybody in the LAN can access each +# other's machine though ssh. +# +# In such a case, server the notebook server on localhost is not secure since +# any user can connect to the notebook server via ssh. +c.NotebookApp.password_required = 'JUPYTER_PASSWORD' in os.environ + +# The port the notebook server will listen on. +# c.NotebookApp.port = 8888 + +# The number of additional ports to try if the specified port is not available. +# c.NotebookApp.port_retries = 50 + +# DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib. +# c.NotebookApp.pylab = 'disabled' + +# (sec) Time window used to check the message and data rate limits. +# c.NotebookApp.rate_limit_window = 3 + +# Reraise exceptions encountered loading server extensions? +# c.NotebookApp.reraise_server_extension_failures = False + +# DEPRECATED use the nbserver_extensions dict instead +# c.NotebookApp.server_extensions = [] + +# The session manager class to use. +# c.NotebookApp.session_manager_class = 'notebook.services.sessions.sessionmanager.SessionManager' + +# Supply SSL options for the tornado HTTPServer. See the tornado docs for +# details. +# c.NotebookApp.ssl_options = {} + +# Supply overrides for terminado. Currently only supports "shell_command". +# c.NotebookApp.terminado_settings = {} + +# Token used for authenticating first-time connections to the server. +# +# When no password is enabled, the default is to generate a new, random token. +# +# Setting to an empty string disables authentication altogether, which is NOT +# RECOMMENDED. +# c.NotebookApp.token = '' + +# Supply overrides for the tornado.web.Application that the Jupyter notebook +# uses. +# c.NotebookApp.tornado_settings = {} + +# Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded- +# For headerssent by the upstream reverse proxy. Necessary if the proxy handles +# SSL +# c.NotebookApp.trust_xheaders = False + +# DEPRECATED, use tornado_settings +# c.NotebookApp.webapp_settings = {} + +# The base URL for websockets, if it differs from the HTTP server (hint: it +# almost certainly doesn't). +# +# Should be in the form of an HTTP origin: ws[s]://hostname[:port] +# c.NotebookApp.websocket_url = '' + +# ------------------------------------------------------------------------------ +# ConnectionFileMixin(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ + +# Mixin for configurable classes that work with connection files + +# JSON file in which to store connection info [default: kernel-.json] +# +# This file will contain the IP, ports, and authentication key needed to connect +# clients to this kernel. By default, this file will be created in the security +# dir of the current profile, but can be specified by absolute path. +# c.ConnectionFileMixin.connection_file = '' + +# set the control (ROUTER) port [default: random] +# c.ConnectionFileMixin.control_port = 0 + +# set the heartbeat port [default: random] +# c.ConnectionFileMixin.hb_port = 0 + +# set the iopub (PUB) port [default: random] +# c.ConnectionFileMixin.iopub_port = 0 + +# Set the kernel's IP address [default localhost]. If the IP address is +# something other than localhost, then Consoles on other machines will be able +# to connect to the Kernel, so be careful! +# c.ConnectionFileMixin.ip = '' + +# set the shell (ROUTER) port [default: random] +# c.ConnectionFileMixin.shell_port = 0 + +# set the stdin (ROUTER) port [default: random] +# c.ConnectionFileMixin.stdin_port = 0 + +## +# c.ConnectionFileMixin.transport = 'tcp' + +# ------------------------------------------------------------------------------ +# KernelManager(ConnectionFileMixin) configuration +# ------------------------------------------------------------------------------ + +# Manages a single kernel in a subprocess on this host. +# +# This version starts kernels with Popen. + +# Should we autorestart the kernel if it dies. +# c.KernelManager.autorestart = True + +# DEPRECATED: Use kernel_name instead. +# +# The Popen Command to launch the kernel. Override this if you have a custom +# kernel. If kernel_cmd is specified in a configuration file, Jupyter does not +# pass any arguments to the kernel, because it cannot make any assumptions about +# the arguments that the kernel understands. In particular, this means that the +# kernel does not receive the option --debug if it given on the Jupyter command +# line. +# c.KernelManager.kernel_cmd = [] + +# Time to wait for a kernel to terminate before killing it, in seconds. +# c.KernelManager.shutdown_wait_time = 5.0 + +# ------------------------------------------------------------------------------ +# Session(Configurable) configuration +# ------------------------------------------------------------------------------ + +# Object for handling serialization and sending of messages. +# +# The Session object handles building messages and sending them with ZMQ sockets +# or ZMQStream objects. Objects can communicate with each other over the +# network via Session objects, and only need to work with the dict-based IPython +# message spec. The Session will handle serialization/deserialization, security, +# and metadata. +# +# Sessions support configurable serialization via packer/unpacker traits, and +# signing with HMAC digests via the key/keyfile traits. +# +# Parameters ---------- +# +# debug : bool +# whether to trigger extra debugging statements +# packer/unpacker : str : 'json', 'pickle' or import_string +# importstrings for methods to serialize message parts. If just +# 'json' or 'pickle', predefined JSON and pickle packers will be used. +# Otherwise, the entire importstring must be used. +# +# The functions must accept at least valid JSON input, and output *bytes*. +# +# For example, to use msgpack: +# packer = 'msgpack.packb', unpacker='msgpack.unpackb' +# pack/unpack : callables +# You can also set the pack/unpack callables for serialization directly. +# session : bytes +# the ID of this Session object. The default is to generate a new UUID. +# username : unicode +# username added to message headers. The default is to ask the OS. +# key : bytes +# The key used to initialize an HMAC signature. If unset, messages +# will not be signed or checked. +# keyfile : filepath +# The file containing a key. If this is set, `key` will be initialized +# to the contents of the file. + +# Threshold (in bytes) beyond which an object's buffer should be extracted to +# avoid pickling. +# c.Session.buffer_threshold = 1024 + +# Whether to check PID to protect against calls after fork. +# +# This check can be disabled if fork-safety is handled elsewhere. +# c.Session.check_pid = True + +# Threshold (in bytes) beyond which a buffer should be sent without copying. +# c.Session.copy_threshold = 65536 + +# Debug output in the Session +c.Session.debug = True + +# The maximum number of digests to remember. +# +# The digest history will be culled when it exceeds this value. +# c.Session.digest_history_size = 65536 + +# The maximum number of items for a container to be introspected for custom +# serialization. Containers larger than this are pickled outright. +# c.Session.item_threshold = 64 + +# execution key, for signing messages. +# c.Session.key = b'' + +# path to file containing execution key. +# c.Session.keyfile = '' + +# Metadata dictionary, which serves as the default top-level metadata dict for +# each message. +# c.Session.metadata = {} + +# The name of the packer for serializing messages. Should be one of 'json', +# 'pickle', or an import name for a custom callable serializer. +# c.Session.packer = 'json' + +# The UUID identifying this session. +# c.Session.session = '' + +# The digest scheme used to construct the message signatures. Must have the form +# 'hmac-HASH'. +# c.Session.signature_scheme = 'hmac-sha256' + +# The name of the unpacker for unserializing messages. Only used with custom +# functions for `packer`. +# c.Session.unpacker = 'json' + +# Username for the Session. Default is your system username. +# c.Session.username = 'patrick' + +# ------------------------------------------------------------------------------ +# MultiKernelManager(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ + +# A class for managing multiple kernels. + +# The name of the default kernel to start +# c.MultiKernelManager.default_kernel_name = 'python3' + +# The kernel manager class. This is configurable to allow subclassing of the +# KernelManager for customized behavior. +# c.MultiKernelManager.kernel_manager_class = 'jupyter_client.ioloop.IOLoopKernelManager' + +# ------------------------------------------------------------------------------ +# MappingKernelManager(MultiKernelManager) configuration +# ------------------------------------------------------------------------------ + +# A KernelManager that handles notebook mapping and HTTP error handling + +## +# c.MappingKernelManager.root_dir = '' + +# ------------------------------------------------------------------------------ +# ContentsManager(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ + +# Base class for serving files and directories. +# +# This serves any text or binary file, as well as directories, with special +# handling for JSON notebook documents. +# +# Most APIs take a path argument, which is always an API-style unicode path, and +# always refers to a directory. +# +# - unicode, not url-escaped +# - '/'-separated +# - leading and trailing '/' will be stripped +# - if unspecified, path defaults to '', +# indicating the root path. + +## +# c.ContentsManager.checkpoints = None + +## +# c.ContentsManager.checkpoints_class = 'notebook.services.contents.checkpoints.Checkpoints' + +## +# c.ContentsManager.checkpoints_kwargs = {} + +# Glob patterns to hide in file and directory listings. +# c.ContentsManager.hide_globs = ['__pycache__', '*.pyc', '*.pyo', '.DS_Store', '*.so', '*.dylib', '*~'] + +# Python callable or importstring thereof +# +# To be called on a contents model prior to save. +# +# This can be used to process the structure, such as removing notebook outputs +# or other side effects that should not be saved. +# +# It will be called as (all arguments passed by keyword):: +# +# hook(path=path, model=model, contents_manager=self) +# +# - model: the model to be saved. Includes file contents. +# Modifying this dict will affect the file that is stored. +# - path: the API path of the save destination +# - contents_manager: this ContentsManager instance +# c.ContentsManager.pre_save_hook = None + +## +# c.ContentsManager.root_dir = '/' + +# The base name used when creating untitled directories. +# c.ContentsManager.untitled_directory = 'Untitled Folder' + +# The base name used when creating untitled files. +# c.ContentsManager.untitled_file = 'untitled' + +# The base name used when creating untitled notebooks. +# c.ContentsManager.untitled_notebook = 'Untitled' + +# ------------------------------------------------------------------------------ +# FileManagerMixin(Configurable) configuration +# ------------------------------------------------------------------------------ + +# Mixin for ContentsAPI classes that interact with the filesystem. +# +# Provides facilities for reading, writing, and copying both notebooks and +# generic files. +# +# Shared by FileContentsManager and FileCheckpoints. +# +# Note ---- Classes using this mixin must provide the following attributes: +# +# root_dir : unicode +# A directory against against which API-style paths are to be resolved. +# +# log : logging.Logger + +# By default notebooks are saved on disk on a temporary file and then if +# succefully written, it replaces the old ones. This procedure, namely +# 'atomic_writing', causes some bugs on file system whitout operation order +# enforcement (like some networked fs). If set to False, the new notebook is +# written directly on the old one which could fail (eg: full filesystem or quota +# ) +# c.FileManagerMixin.use_atomic_writing = True + +# ------------------------------------------------------------------------------ +# FileContentsManager(FileManagerMixin,ContentsManager) configuration +# ------------------------------------------------------------------------------ + +# Python callable or importstring thereof +# +# to be called on the path of a file just saved. +# +# This can be used to process the file on disk, such as converting the notebook +# to a script or HTML via nbconvert. +# +# It will be called as (all arguments passed by keyword):: +# +# hook(os_path=os_path, model=model, contents_manager=instance) +# +# - path: the filesystem path to the file just written - model: the model +# representing the file - contents_manager: this ContentsManager instance +# c.FileContentsManager.post_save_hook = None + +## +# c.FileContentsManager.root_dir = '' + +# DEPRECATED, use post_save_hook. Will be removed in Notebook 5.0 +# c.FileContentsManager.save_script = False + +# ------------------------------------------------------------------------------ +# NotebookNotary(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ + +# A class for computing and verifying notebook signatures. + +# The hashing algorithm used to sign notebooks. +# c.NotebookNotary.algorithm = 'sha256' + +# The sqlite file in which to store notebook signatures. By default, this will +# be in your Jupyter data directory. You can set it to ':memory:' to disable +# sqlite writing to the filesystem. +# c.NotebookNotary.db_file = '' + +# The secret key with which notebooks are signed. +# c.NotebookNotary.secret = b'' + +# The file where the secret key is stored. +# c.NotebookNotary.secret_file = '' + +# A callable returning the storage backend for notebook signatures. The default +# uses an SQLite database. +# c.NotebookNotary.store_factory = traitlets.Undefined + +# ------------------------------------------------------------------------------ +# KernelSpecManager(LoggingConfigurable) configuration +# ------------------------------------------------------------------------------ + +# If there is no Python kernelspec registered and the IPython kernel is +# available, ensure it is added to the spec list. +# c.KernelSpecManager.ensure_native_kernel = True + +# The kernel spec class. This is configurable to allow subclassing of the +# KernelSpecManager for customized behavior. +# c.KernelSpecManager.kernel_spec_class = 'jupyter_client.kernelspec.KernelSpec' + +# Whitelist of allowed kernel names. +# +# By default, all installed kernels are allowed. +# c.KernelSpecManager.whitelist = set() + + +# omegaml setup +default_contents_manager = 'omegaml.notebook.omegacontentsmgr.OmegaStoreContentsManager' +contents_manager = os.environ.get('JY_CONTENTS_MANAGER', default_contents_manager) +c.NotebookApp.contents_manager_class = contents_manager + +import logging + +logger = logging.getLogger(c.NotebookApp.__class__.__name__) +handler = logging.FileHandler('/tmp/jupyter.log') +logger.addHandler(handler) + +sys.path.insert(0, '/app') diff --git a/omegaml/notebook/omegacontentsmgr.py b/omegaml/notebook/omegacontentsmgr.py new file mode 100644 index 00000000..bd51fa05 --- /dev/null +++ b/omegaml/notebook/omegacontentsmgr.py @@ -0,0 +1,223 @@ +import os +from logging import warning + +from IPython.utils import tz +import nbformat +from notebook.services.contents.manager import ContentsManager +from tornado import web + +from omegaml.notebook.checkpoints import NoOpCheckpoints + + +class OmegaStoreContentsManager(ContentsManager): + """ + Jupyter notebook storage manager for omegaml + + This requires a properly configured omegaml instance. + + see http://jupyter-notebook.readthedocs.io/en/stable/extending/contents.html + """ + + def __init__(self, **kwargs): + super(OmegaStoreContentsManager, self).__init__(**kwargs) + self._omega = None + + def _checkpoints_class_default(self): + return NoOpCheckpoints + + @property + def omega(self): + """ + return the omega instance used by the contents manager + """ + if self._omega is None: + import omegaml as om + self._omega = om + return self._omega + + @property + def store(self): + """ + return the OmageStore for jobs (notebooks) + """ + return self.omega.jobs.store + + def get(self, path, content=True, type=None, format=None): + """ + get an entry in the store + + this is called by the contents engine to get the contents of the jobs + store. + """ + path = path.strip('/') + if not self.exists(path): + raise web.HTTPError(404, u'No such file or directorys: %s' % path) + + if path == '': + if type not in (None, 'directory'): + raise web.HTTPError(400, u'%s is a directory, not a %s' % ( + path, type), reason='bad type') + model = self._dir_model(path, content=content) + elif type == 'notebook' or (type is None and path.endswith('.ipynb')): + model = self._notebook_model(path, content=content) + else: + raise web.HTTPError(400, u'%s is not a directory' % path, + reason='bad type') + return model + + def save(self, model, path): + """ + save an entry in the store + + this is called by the contents engine to store a notebook + """ + path = path.strip('/') + if 'type' not in model: + raise web.HTTPError(400, u'No file type provided') + if 'content' not in model and model['type'] != 'directory': + raise web.HTTPError(400, u'No file content provided') + + self.run_pre_save_hook(model=model, path=path) + om = self.omega + + try: + if model['type'] == 'notebook': + nb = nbformat.from_dict(model['content']) + self.check_and_sign(nb, path) + self.omega.jobs.put(nb, path) + else: + raise web.HTTPError( + 400, "Unhandled contents type: %s" % model['type']) + except web.HTTPError: + raise + except Exception as e: + self.log.error( + u'Error while saving file: %s %s', path, e, exc_info=True) + raise web.HTTPError( + 500, u'Unexpected error while saving file: %s %s' % (path, e)) + + validation_message = None + if model['type'] == 'notebook': + self.validate_notebook_model(model) + validation_message = model.get('message', None) + + model = self.get(path, content=False) + if validation_message: + model['message'] = validation_message + + return model + + def delete_file(self, path): + """ + delete an entry + + this is called by the contents engine to delete an entry + """ + path = path.strip('/') + self.omega.jobs.drop(path) + + def rename_file(self, old_path, new_path): + """ + rename a file + + this is called by the contents engine to rename an entry + """ + old_path = old_path.strip('/') + new_path = new_path.strip('/') + if self.file_exists(new_path): + raise web.HTTPError(409, u'Notebook already exists: %s' % new_path) + # rename on metadata. Note the gridfile instance stays the same + meta = self.omega.jobs.metadata(old_path) + meta.name = new_path + meta.save() + + def exists(self, path): + """ + Does a file or dir exist at the given collection in gridFS? + We do not have dir so dir_exists returns true. + + :param path: (str) The relative path to the file's directory + (with '/' as separator) + :returns exists: (boo) The relative path to the file's directory (with '/' as separator) + """ + path = path.strip('/') + return self.file_exists(path) or self.dir_exists(path) + + def dir_exists(self, path=''): + path = path.strip('/') + if path == '': + return True + return len(self.omega.jobs.list('{path}.*'.format(path=path))) > 0 + + def file_exists(self, path): + path = path.strip('/') + return path in self.omega.jobs.list(path) + + def is_hidden(self, path): + return False + + def _read_notebook(self, path, as_version=None): + path = path.strip('/') + return self.omega.jobs.get(path) + + def _notebook_model(self, path, content=True): + """ + Build a notebook model + if content is requested, the notebook content will be populated + as a JSON structure (not double-serialized) + """ + path = path.strip('/') + model = self._base_model(path) + model['type'] = 'notebook' + if content: + nb = self._read_notebook(path, as_version=4) + self.mark_trusted_cells(nb, path) + model['content'] = nb + model['format'] = 'json' + self.validate_notebook_model(model) + # if exists already fake last modified and created timestamps + # otherwise jupyter notebook will claim a newer version "on disk" + if self.exists(path): + model['last_modified'] = tz.datetime(1970, 1, 1) + model['created'] = tz.datetime(1970, 1, 1) + return model + + def _base_model(self, path): + """Build the common base of a contents model""" + # http://jupyter-notebook.readthedocs.io/en/stable/extending/contents.html + path = path.strip('/') + last_modified = tz.utcnow() + created = tz.utcnow() + # Create the base model. + model = {} + model['name'] = path.rsplit('/', 1)[-1] + model['path'] = path + model['last_modified'] = last_modified + model['created'] = created + model['content'] = None + model['format'] = None + model['mimetype'] = None + model['writable'] = True + return model + + def _dir_model(self, path, content=True): + """ + Build a model to return all of the files in gridfs + if content is requested, will include a listing of the directory + """ + path = path.strip('/') + model = self._base_model(path) + model['type'] = 'directory' + model['content'] = contents = [] + entries = self.omega.jobs.list('{path}.*'.format(path=path), raw=True) + for meta in entries: + try: + entry = self.get(meta.name, content=content) + except: + msg = ('_dir_model error, cannot get {}, ' + 'removing from list'.format(meta.name)) + self.log.warning(msg) + else: + contents.append(entry) + model['format'] = 'json' + return model diff --git a/omegaml/notebook/tasks.py b/omegaml/notebook/tasks.py new file mode 100644 index 00000000..f2b3bb6c --- /dev/null +++ b/omegaml/notebook/tasks.py @@ -0,0 +1,94 @@ +""" +omega runtime job tasks +""" +from __future__ import absolute_import + +from celery import shared_task +from mongoengine.errors import DoesNotExist + +from omegaml.documents import Metadata +from omegaml.celery_util import OmegamlTask + + +class NotebookTask(OmegamlTask): + abstract = True + + def on_success(self, retval, task_id, *args, **kwargs): + om = self.om + args, kwargs = args[0:2] + nbfile = args[0] + meta = om.jobs.metadata(nbfile) + attrs = meta.attributes + attrs['state'] = 'SUCCESS' + attrs['task_id'] = task_id + meta.kind = Metadata.OMEGAML_JOBS + + if not kwargs: + pass + else: + attrs['last_run_time'] = kwargs.get('run_at') + attrs['next_run_time'] = kwargs.get('next_run_time') + + meta.attributes = attrs + meta.save() + + def on_failure(self, retval, task_id, *args, **kwargs): + om = self.om + args, kwargs = args[0:2] + nbfile = args[0] + meta = om.jobs.metadata(nbfile) + attrs = meta.attributes + attrs['state'] = 'FAILURE' + attrs['task_id'] = task_id + meta.kind = Metadata.OMEGAML_JOBS + + if not kwargs: + pass + else: + attrs['last_run_time'] = kwargs.get('run_at') + attrs['next_run_time'] = kwargs.get('next_run_time') + + meta.attributes = attrs + meta.save() + + +@shared_task(bind=True, base=NotebookTask) +def run_omegaml_job(self, nb_file, **kwargs): + """ + runs omegaml job + """ + result = self.om.jobs.run_notebook(nb_file) + return result.to_json() + + +@shared_task(base=NotebookTask, bind=True) +def schedule_omegaml_job(self, nb_file, **kwargs): + """ + schedules the running of omegaml job + """ + result = self.om.jobs.schedule(nb_file) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def execute_scripts(self, **kwargs): + """ + + will retrieve all scripts from the mongodb + (as per a respective OMEGAML_SCRIPTS_GRIDFS setting), + provided they are marked for execution at the time of execution + """ + om = self.om + # Search tasks from mongo + job_list = om.jobs.list() + for nb_file in job_list: + try: + metadata = Metadata.objects.get( + name=nb_file, kind=Metadata.OMEGAML_RUNNING_JOBS) + task_state = metadata.attributes.get('state') + if task_state == "RECEIVED": + pass + else: + om.jobs.schedule(nb_file) + except DoesNotExist: + om.jobs.schedule(nb_file) diff --git a/omegaml/omega.py b/omegaml/omega.py new file mode 100644 index 00000000..35f43f85 --- /dev/null +++ b/omegaml/omega.py @@ -0,0 +1,107 @@ +from ._version import version + + +class Omega(object): + """ + Client API to omegaml + + Provides the following APIs: + + * :code:`datasets` - access to datasets stored in the cluster + * :code:`models` - access to models stored in the cluster + * :code:`runtimes` - access to the cluster compute resources + * :code:`jobs` - access to jobs stored and executed in the cluster + * :code:`scripts` - access to lambda modules stored and executed in the cluster + + """ + + def __init__(self, mongo_url=None, backend=None, broker=None, + celeryconf=None, defaults=None): + """ + Initialize the client API + + Without arguments create the client API according to the user's + configuration in :code:`~/omegaml/config.yml`. + + Arguments override the user's configuration. + + :param mongo_url: the fully qualified URI to the mongo database, + of format :code:`mongodb://user:password@host:port/database` + :param broker: the celery broker URI + :param backend: the celery result backend URI + :param celeryconf: the celery configuration dictionary + :param celerykwargs: kwargs to create the Celery instance + """ + from omegaml.util import settings + # avoid circular imports + from omegaml.jobs import OmegaJobs + from omegaml.runtimes import OmegaRuntime + from omegaml.store import OmegaStore + # celery and mongo configuration + self.defaults = defaults or settings() + self.mongo_url = mongo_url or self.defaults.OMEGA_MONGO_URL + self.broker = broker or self.defaults.OMEGA_BROKER + self.backend = backend or self.defaults.OMEGA_RESULT_BACKEND + self.celeryconf = celeryconf + # setup storage locations + self.models = OmegaStore(mongo_url=mongo_url, prefix='models/', defaults=self.defaults) + self.datasets = OmegaStore(mongo_url=mongo_url, prefix='data/', defaults=self.defaults) + self._jobdata = OmegaStore(mongo_url=mongo_url, prefix='jobs/', defaults=self.defaults) + # runtimes environments + self.runtime = OmegaRuntime(self, backend=backend, + broker=broker, celeryconf=celeryconf, + defaults=self.defaults) + self.jobs = OmegaJobs(store=self._jobdata) + + def __repr__(self): + return 'Omega(mongo_url={})'.format(self.mongo_url) + + +class OmegaDeferredInstance(object): + """ + A deferred instance of Omega() that is only instantiated on access + + This is to ensure that module-level imports don't trigger instantiation + of Omega. + """ + + def __init__(self, base=None, attribute=None): + self.omega = 'not initialized -- call .setup() or access an attribute' + self.initialized = False + self.base = base + self.attribute = attribute + + def setup(self): + self.omega = Omega() + self.initialized = True + return self + + def __getattr__(self, name): + if self.base: + base = getattr(self.base, self.attribute) + return getattr(base, name) + if not self.initialized: + self.setup() + return getattr(self.omega, name) + + def __repr__(self): + if self.base: + return repr(getattr(self.base, self.attribute)) + self.setup() + return repr(self.omega) + + +def setup(): + """ + configure and return the omega client instance + """ + return _om.setup().omega + +# dynamic lookup of Omega instance in a task context +get_omega_for_task = lambda args: _om + +# default instance +# -- these are deferred instanced that is the actual Omega instance +# is only created on actual attribute access +_om = OmegaDeferredInstance() +version = version # ensure we keep imports diff --git a/omegaml/restapi/__init__.py b/omegaml/restapi/__init__.py new file mode 100644 index 00000000..9492f669 --- /dev/null +++ b/omegaml/restapi/__init__.py @@ -0,0 +1,146 @@ +import datetime + +import pandas as pd +import six +from flask import request +from flask_restplus import Resource, Model, fields +from mongoengine import DoesNotExist + +import omegaml as om +from omegaml.restapi.util import strict +from .app import api + +import numpy as np + +isTrue = lambda v: v if isinstance(v, bool) else ( + v.lower() in ['yes', 'y', 't', 'true', '1']) + + +PredictInput = strict(api).model('ModelInputSchema', { + 'columns': fields.List(fields.String), + 'data': fields.List(fields.Raw), +}) + +PredictOutput = api.model('PredictOutput', { + 'model': fields.String, + 'result': fields.List(fields.Raw), +}) + +DatasetInput = api.model('DatasetInput', { + 'data': fields.Raw, + 'dtypes': fields.Raw, + 'append': fields.Boolean, +}) + +DatasetIndex = api.model('DatasetIndex', { + 'values': fields.List(fields.Raw), + 'type': fields.String, +}) + +DatasetQueryOutput = api.model('DatasetQueryOutput', { + 'data': fields.Raw, + 'index': fields.Nested(DatasetIndex) +}) + +@api.route('/v1/ping') +class PingResource(Resource): + def get(self): + dt = datetime.datetime.now() + return {'ping': dt.isoformat()} + + +@api.route('/v1/model//predict') +class ModelResource(Resource): + @api.expect(PredictInput, validate=True) + @api.marshal_with(PredictOutput) + def put(self, model_id): + data = api.payload.get('data') + columns = api.payload.get('columns') + df = pd.DataFrame(data)[columns] + promise = om.runtime.model(model_id).predict(df) + result = promise.get() + return {'model': model_id, 'result': result.tolist()} + + +@api.route('/v1/dataset/') +class DatasetResource(Resource): + def _restore_filter(self, om, fltparams, name): + """ + restore filter kwargs for query in om.datasets.get + """ + # -- get filters as specified on request query args + fltkwargs = {k: v for k, v in six.iteritems(fltparams) + if k not in ['orient', 'limit', 'skip', 'page']} + # -- get dtypes of dataframe and convert filter values + metadata = om.datasets.metadata(name) + kind_meta = metadata.kind_meta or {} + dtypes = kind_meta.get('dtypes') + # get numpy/python typemap. this is required for Py3 support + # adopted from https://stackoverflow.com/a/34919415 + np_typemap = {v: getattr(six.moves.builtins, k) + for k, v in np.typeDict.items() + if k in vars(six.moves.builtins)} + for k, v in six.iteritems(fltkwargs): + # -- get column name without operator (e.g. x__gt => x) + col = k.split('__')[0] + value = six.moves.urllib.parse.unquote(v) + dtype = dtypes.get(str(col)) + if dtype: + # -- get dtyped value and convert to python type + value = np_typemap.get(getattr(np, dtype, str), str)(value) + fltkwargs[k] = value + return fltkwargs + + @api.marshal_with(DatasetQueryOutput) + def get(self, dataset_id): + orient = request.args.get('orient', 'dict') + fltkwargs = self._restore_filter(om, request.args, dataset_id) + df = om.datasets.getl(dataset_id, filter=fltkwargs).value + # get index values as python types to support Py3 + index_values = list(df.index.astype('O').values) + index_type = type(df.index).__name__ + # get data set values as python types to support Py3 + df = df.reset_index(drop=True) + df.index = df.index.astype('O') + # convert nan to None + # https://stackoverflow.com/a/34467382 + data = df.where(pd.notnull(df), None).astype('O').to_dict(orient) + return { + 'data': data, + 'index': { + 'values': index_values, + 'type': index_type, + } + } + + @api.expect(DatasetInput, validate=True) + @api.response(200, 'updated') + def put(self, dataset_id): + orient = request.args.get('orient', 'dict') + if 'append' in request.args: + append = isTrue(request.args.get('append', 'true')) + else: + append = isTrue(api.payload.get('append', 'true')) + dtypes = api.payload.get('dtypes') + if orient == 'dict': + orient = 'columns' + if dtypes: + # due to https://github.com/pandas-dev/pandas/issues/14655#issuecomment-260736368 + dtypes = {k: np.dtype(v) for k, v in six.iteritems(dtypes)} + df = pd.DataFrame.from_dict(api.payload.get('data'), + orient=orient).astype(dtypes) + om.datasets.put(df, dataset_id, append=append) + return '', 200 + + @api.response(200, 'updated') + @api.response(404, 'does not exist') + def delete(self, dataset_id): + try: + om.datasets.drop(dataset_id) + except DoesNotExist: + status = 404 + else: + status = 200 + return '', status + + diff --git a/omegaml/restapi/__main__.py b/omegaml/restapi/__main__.py new file mode 100644 index 00000000..4d4f7bbd --- /dev/null +++ b/omegaml/restapi/__main__.py @@ -0,0 +1,5 @@ +from omegaml.restapi import PingResource, ModelResource +from omegaml.restapi.app import app, api + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/omegaml/restapi/app.py b/omegaml/restapi/app.py new file mode 100644 index 00000000..9c463e50 --- /dev/null +++ b/omegaml/restapi/app.py @@ -0,0 +1,11 @@ +from flask import Flask +from flask_restplus import Api +from werkzeug.utils import redirect + +app = Flask(__name__) +api = Api(app) + + +@app.route('/docs') +def docs(): + return redirect("https://omegaml.github.io/omegaml/", code=302) diff --git a/omegaml/restapi/tests/__init__.py b/omegaml/restapi/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/omegaml/restapi/tests/test_api.py b/omegaml/restapi/tests/test_api.py new file mode 100644 index 00000000..f727d1e9 --- /dev/null +++ b/omegaml/restapi/tests/test_api.py @@ -0,0 +1,120 @@ +from unittest import TestCase + +import numpy as np +import pandas as pd +from sklearn.linear_model import LinearRegression + +from omegaml import Omega +from omegaml.restapi.app import app + + +class OmegaRestApiTests(TestCase): + def setUp(self): + self.client = app.test_client() + self.om = Omega() + + def test_predict(self): + X = np.arange(10).reshape(-1, 1) + y = X * 2 + # train model locally + clf = LinearRegression() + clf.fit(X, y) + result = clf.predict(X) + # store model in om + self.om.models.put(clf, 'regression') + # check we can use it to predict + resp = self.client.put('/v1/model/regression/predict', json={ + 'columns': ['v'], + 'data': [dict(v=5)] + }) + self.assertEqual(resp.status_code, 200) + data = resp.get_json() + self.assertEqual(data.get('model'), 'regression') + self.assertEqual(data.get('result'), [[10.]]) + + def test_dataset_query(self): + om = self.om + df = pd.DataFrame({ + 'x': np.arange(100), + 'y': np.arange(100), + }) + om.datasets.put(df, 'test', append=False) + resp = self.client.get('/v1/dataset/test') + self.assertEqual(resp.status_code, 200) + data = resp.get_json() + self.assertIn('data', data) + self.assertIn('index', data) + self.assertIn('values', data['index']) + self.assertEqual(len(data['index']['values']), 100) + self.assertEqual(len(data['data']['x']), 100) + self.assertEqual(len(data['data']['y']), 100) + + def test_dataset_query_filter(self): + om = self.om + df = pd.DataFrame({ + 'x': np.arange(100), + 'y': np.arange(100), + }) + om.datasets.put(df, 'test', append=False) + query = { + 'x__gte': 90, + } + resp = self.client.get('/v1/dataset/test', query_string=query) + self.assertEqual(resp.status_code, 200) + data = resp.get_json() + self.assertIn('data', data) + self.assertIn('index', data) + self.assertIn('values', data['index']) + self.assertEqual(len(data['index']['values']), 10) + self.assertEqual(len(data['data']['x']), 10) + self.assertEqual(len(data['data']['y']), 10) + + def test_dataset_put(self): + om = self.om + om.datasets.drop('foo', force=True) + # create a new dataset + data = { + 'data': { + 'x': list(range(10)), + 'y': list(range(10)), + 's': [str(v) for v in range(10)], + }, + 'dtypes': { + 's': 'str', + }, + 'append': False, + } + resp = self.client.put('/v1/dataset/foo', json=data) + self.assertEqual(resp.status_code, 200) + # -- see if we can query + df = om.datasets.get('foo') + self.assertEqual(10, len(df)) + self.assertEqual(list(range(10)), list(df['x'])) + self.assertEqual(list(range(10)), list(df['y'])) + self.assertEqual([str(v) for v in range(10)], list(df['s'])) + # append more records + data['append'] = True + resp = self.client.put('/v1/dataset/foo', json=data) + self.assertEqual(resp.status_code, 200) + df = om.datasets.get('foo') + self.assertEqual(20, len(df)) + self.assertEqual(list(range(10)) * 2, list(df['x'])) + self.assertEqual(list(range(10)) * 2, list(df['y'])) + self.assertEqual([str(v) for v in list(range(10)) * 2], list(df['s'])) + + + def test_dataset_delete(self): + om = self.om + # test non-existent dataset + om.datasets.drop('foo', force=True) + resp = self.client.delete('/v1/dataset/foo') + self.assertEqual(404, resp.status_code) + df = pd.DataFrame({ + 'x': np.arange(100), + 'y': np.arange(100), + }) + # test existing dataset + om.datasets.put(df, 'foo', append=False) + resp = self.client.delete('/v1/dataset/foo') + self.assertEqual(200, resp.status_code) + self.assertEqual(None, om.datasets.get('foo')) diff --git a/omegaml/restapi/util.py b/omegaml/restapi/util.py new file mode 100644 index 00000000..a871296f --- /dev/null +++ b/omegaml/restapi/util.py @@ -0,0 +1,26 @@ +from flask_restplus import Model + + +class StrictModel(Model): + # To implement a model that supports strict validation + # on fields, we need to explicitly add into the schema + # 'additionalProperties: False' + # See: https://github.com/noirbizarre/flask-restplus/issues/241 + @property + def _schema(self): + old = super(StrictModel, self)._schema + old['additionalProperties'] = False + return old + + +class strict(object): + # a poor man's stand-in for api.model + def __init__(self, api): + self.api = api + + def model(self, name=None, model=None, mask=None, **kwargs): + # create a strict model and attach to api as in flask-restplus issue #241 + smodel = StrictModel(name, model, mask=mask) + smodel.__apidoc__.update(kwargs) + self.api.models[smodel.name] = smodel + return smodel \ No newline at end of file diff --git a/omegaml/runtimes/__init__.py b/omegaml/runtimes/__init__.py new file mode 100644 index 00000000..a495811e --- /dev/null +++ b/omegaml/runtimes/__init__.py @@ -0,0 +1,4 @@ +from .runtime import OmegaRuntime +from .modelproxy import OmegaModelProxy +from .daskruntime import OmegaRuntimeDask +from .jobproxy import OmegaJobProxy diff --git a/omegaml/runtimes/daskruntime.py b/omegaml/runtimes/daskruntime.py new file mode 100644 index 00000000..d8959321 --- /dev/null +++ b/omegaml/runtimes/daskruntime.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import + +import datetime +from importlib import import_module +import os + +from omegaml.runtimes.jobproxy import OmegaJobProxy + + +class DaskTask(object): + """ + A dask remote function wrapper mimicking a Celery task + """ + + def __init__(self, fn, client, pure=True): + """ + :param fn: (function) the function to be called + :param client: (dask client) the dask client to use + :param pure: (bool) whether this is a dask pure function (will + be cached or not). Defaults to True. + """ + self.client = client + self.fn = fn + self.pure = pure + + def delay(self, *args, **kwargs): + """ + submit the function and execute on cluster. + """ + kwargs['pure'] = kwargs.get('pure', self.pure) + return DaskAsyncResult(self.client.submit(self.fn, *args, **kwargs)) + + +class DaskAsyncResult(object): + """ + A dask Future wrapper mimicking a Celery AsyncResult + """ + + def __init__(self, future): + self.future = future + + def get(self): + import dask + + if os.environ.get('DASK_DEBUG'): + with dask.set_options(get=dask.threaded.get): + return self.future.result() + return self.future.result() + + +def daskhello(*args, **kwargs): + # test function for dask distributed + return "hello from {} at {}".format(os.getpid(), datetime.datetime.now()) + + +class OmegaRuntimeDask(object): + """ + omegaml compute cluster gateway to a dask distributed cluster + + set environ DASK_DEBUG=1 to run dask tasks locally + """ + + def __init__(self, omega, dask_url=None): + self.dask_url = dask_url + self.omega = omega + self._client = None + + @property + def client(self): + from distributed import Client, LocalCluster + if self._client is None: + if os.environ.get('DASK_DEBUG'): + # http://dask.pydata.org/en/latest/setup/single-distributed.html?highlight=single-threaded#localcluster + single_threaded = LocalCluster(processes=False) + self._client = Client(single_threaded) + else: + self._client = Client(self.dask_url) + return self._client + + def model(self, modelname): + """ + return a model for remote execution + """ + from omegaml.runtimes.modelproxy import OmegaModelProxy + return OmegaModelProxy(modelname, runtime=self) + + def job(self, jobname): + """ + return a job for remote exeuction + """ + return OmegaJobProxy(jobname, runtime=self) + + def task(self, name): + """ + retrieve the task function from the task module + + This retrieves the task function and wraps it into a + DaskTask. DaskTask mimicks a celery task and is + called on the cluster using .delay(), the same way we + call a celery task. .delay() will return a DaskAsyncResult, + supporting the celery .get() semantics. This way we can use + the same proxy objects, as all they do is call .delay() and + return an AsyncResult. + """ + modname, funcname = name.rsplit('.', 1) + mod = import_module(modname) + func = getattr(mod, funcname) + # we pass pure=False to force dask to reevaluate the task + # http://distributed.readthedocs.io/en/latest/client.html?highlight=pure#pure-functions-by-default + return DaskTask(func, self.client, pure=False) + + def settings(self): + """ + return the runtimes's cluster settings + """ + return self.task('omegaml.tasks.omega_settings').delay().get() + + def ping(self): + return DaskTask(daskhello, self.client, pure=False) diff --git a/omegaml/runtimes/jobproxy.py b/omegaml/runtimes/jobproxy.py new file mode 100644 index 00000000..db0f0564 --- /dev/null +++ b/omegaml/runtimes/jobproxy.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import + +import logging +from uuid import uuid4 + +import six + +from omegaml.util import is_dataframe, settings, is_ndarray +logger = logging.getLogger(__file__) + + +class OmegaJobProxy(object): + + """ + proxy to a remote job in a celery worker + + Usage: + + .. code:: + + om = Omega() + # result is AsyncResult, use .get() to return it's result + result = om.runtime.job('foojob').run() + result.get() + + # result is AsyncResult, use .get() to return it's result + result = om.runtime.job('foojob').schedule() + result.get() + """ + def __init__(self, jobname, runtime=None): + self.jobname = jobname + self.runtime = runtime + + @property + def _common_kwargs(self): + return dict() + + def run(self, **kwargs): + """ + run the job + + :return: the result + """ + job_run = self.runtime.task('omegaml.notebook.tasks.run_omegaml_job') + return job_run.delay(self.jobname, **self._common_kwargs, **kwargs) + + def schedule(self, **kwargs): + """ + schedule the job + """ + job_run = self.runtime.task('omegaml.notebook.tasks.schedule_omegaml_job') + return job_run.delay(self.jobname, **self._common_kwargs, **kwargs) diff --git a/omegaml/runtimes/mixins/__init__.py b/omegaml/runtimes/mixins/__init__.py new file mode 100644 index 00000000..be14a1c7 --- /dev/null +++ b/omegaml/runtimes/mixins/__init__.py @@ -0,0 +1,2 @@ +from .modelmixin import ModelMixin +from .gridsearch import GridSearchMixin \ No newline at end of file diff --git a/omegaml/runtimes/mixins/gridsearch.py b/omegaml/runtimes/mixins/gridsearch.py new file mode 100644 index 00000000..5c7f3492 --- /dev/null +++ b/omegaml/runtimes/mixins/gridsearch.py @@ -0,0 +1,12 @@ +class GridSearchMixin(object): + @property + def _common_kwargs(self): + return dict(pure_python=self.pure_python) + + def gridsearch(self, Xname, Yname, parameters=None, pure_python=False, **kwargs): + gs_task = self.runtime.task('omegaml.tasks.omega_gridsearch') + Xname = self._ensure_data_is_stored(Xname, prefix='_fitX') + if Yname is not None: + Yname = self._ensure_data_is_stored(Yname, prefix='_fitY') + return gs_task.delay(self.modelname, Xname, Yname, parameters=parameters, + **self._common_kwargs, **kwargs) diff --git a/omegaml/runtimes/mixins/modelmixin.py b/omegaml/runtimes/mixins/modelmixin.py new file mode 100644 index 00000000..dec969c7 --- /dev/null +++ b/omegaml/runtimes/mixins/modelmixin.py @@ -0,0 +1,191 @@ +from __future__ import absolute_import + +import logging +from uuid import uuid4 + +import six + +from omegaml.util import is_dataframe, settings, is_ndarray, extend_instance + +logger = logging.getLogger(__file__) + + +class ModelMixin(object): + @property + def _common_kwargs(self): + return dict(pure_python=self.pure_python) + + def fit(self, Xname, Yname=None, **kwargs): + """ + fit the model + + Calls :code:`.fit(X, Y, **kwargs)`. If instead of dataset names actual data + is given, the data is stored using _fitX/fitY prefixes and a unique + name. + + After fitting, a new model version is stored with its attributes + fitX and fitY pointing to the datasets, as well as the sklearn + version used. + + :param Xname: name of X dataset or data + :param Yname: name of Y dataset or data + :return: the model (self) or the string representation (python clients) + """ + omega_fit = self.runtime.task('omegaml.tasks.omega_fit') + Xname = self._ensure_data_is_stored(Xname, prefix='_fitX') + if Yname is not None: + Yname = self._ensure_data_is_stored(Yname, prefix='_fitY') + return omega_fit.delay(self.modelname, Xname, Yname, + **self._common_kwargs, **kwargs) + + def partial_fit(self, Xname, Yname=None, **kwargs): + """ + update the model + + Calls :code:`.partial_fit(X, Y, **kwargs)`. If instead of dataset names actual + data is given, the data is stored using _fitX/fitY prefixes and + a unique name. + + After fitting, a new model version is stored with its attributes + fitX and fitY pointing to the datasets, as well as the sklearn + version used. + + :param Xname: name of X dataset or data + :param Yname: name of Y dataset or data + :return: the model (self) or the string representation (python clients) + """ + omega_fit = self.runtime.task('omegaml.tasks.omega_partial_fit') + Xname = self._ensure_data_is_stored(Xname, prefix='_fitX') + if Yname is not None: + Yname = self._ensure_data_is_stored(Yname, prefix='_fitY') + return omega_fit.delay(self.modelname, Xname, Yname, + **self._common_kwargs, **kwargs) + + def transform(self, Xname, rName=None, **kwargs): + """ + transform X + + Calls :code:`.transform(X, **kwargs)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .transform, or the metadata of the rName + dataset if rName was given + """ + omega_transform = self.runtime.task('omegaml.tasks.omega_transform') + Xname = self._ensure_data_is_stored(Xname) + return omega_transform.delay(self.modelname, Xname, + rName=rName, + **self._common_kwargs, **kwargs) + + def fit_transform(self, Xname, Yname=None, rName=None, **kwargs): + """ + fit & transform X + + Calls :code:`.fit_transform(X, Y, **kwargs)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param Yname: name of the Y dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .fit_transform, or the metadata of the rName + dataset if rName was given + """ + + omega_fit_transform = self.runtime.task( + 'omegaml.tasks.omega_fit_transform') + Xname = self._ensure_data_is_stored(Xname) + if Yname is not None: + Yname = self._ensure_data_is_stored(Yname) + return omega_fit_transform.delay(self.modelname, Xname, Yname, + rName=rName, transform=True, + **self._common_kwargs, **kwargs) + + def predict(self, Xpath_or_data, rName=None, **kwargs): + """ + predict + + Calls :code:`.predict(X)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .predict, or the metadata of the rName + dataset if rName was given + """ + omega_predict = self.runtime.task('omegaml.tasks.omega_predict') + Xname = self._ensure_data_is_stored(Xpath_or_data) + return omega_predict.delay(self.modelname, Xname, rName=rName, + **self._common_kwargs, **kwargs) + + def predict_proba(self, Xpath_or_data, rName=None, **kwargs): + """ + predict probabilities + + Calls :code:`.predict_proba(X)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .predict_proba, or the metadata of the rName + dataset if rName was given + """ + omega_predict_proba = self.runtime.task( + 'omegaml.tasks.omega_predict_proba') + Xname = self._ensure_data_is_stored(Xpath_or_data) + return omega_predict_proba.delay(self.modelname, Xname, rName=rName, + **self._common_kwargs, **kwargs) + + def score(self, Xname, yName, rName=None, **kwargs): + """ + calculate score + + Calls :code:`.score(X, y, **kwargs)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param yName: name of the y dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .score, or the metadata of the rName + dataset if rName was given + """ + omega_score = self.runtime.task('omegaml.tasks.omega_score') + Xname = self._ensure_data_is_stored(Xname) + yName = self._ensure_data_is_stored(yName) + return omega_score.delay(self.modelname, Xname, yName, rName=rName, + **self._common_kwargs, **kwargs) + + def decision_function(self, Xname, rName=None, **kwargs): + """ + calculate score + + Calls :code:`.decision_function(X, y, **kwargs)`. If rName is given the result is + stored as object rName + + :param Xname: name of the X dataset + :param rName: name of the resulting dataset (optional) + :return: the data returned by .score, or the metadata of the rName + dataset if rName was given + """ + omega_decision_function = self.runtime.task('omegaml.tasks.omega_decision_function') + Xname = self._ensure_data_is_stored(Xname) + return omega_decision_function.delay(self.modelname, Xname, rName=rName, + **self._common_kwargs, **kwargs) + + def _ensure_data_is_stored(self, name_or_data, prefix='_temp'): + if is_dataframe(name_or_data): + name = '%s_%s' % (prefix, uuid4().hex) + self.runtime.omega.datasets.put(name_or_data, name) + elif is_ndarray(name_or_data): + name = '%s_%s' % (prefix, uuid4().hex) + self.runtime.omega.datasets.put(name_or_data, name) + elif isinstance(name_or_data, (list, tuple, dict)): + name = '%s_%s' % (prefix, uuid4().hex) + self.runtime.omega.datasets.put(name_or_data, name) + elif isinstance(name_or_data, six.string_types): + name = name_or_data + else: + raise TypeError( + 'invalid type for Xpath_or_data', type(name_or_data)) + return name diff --git a/omegaml/runtimes/modelproxy.py b/omegaml/runtimes/modelproxy.py new file mode 100644 index 00000000..ef71590d --- /dev/null +++ b/omegaml/runtimes/modelproxy.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +import logging +from uuid import uuid4 + +import six + +from omegaml.util import is_dataframe, settings, is_ndarray, extend_instance +logger = logging.getLogger(__file__) + + +class OmegaModelProxy(object): + + """ + proxy to a remote model in a celery worker + + The proxy provides the same methods as the model but will + execute the methods using celery tasks and return celery + AsyncResult objects + + Usage: + + .. code:: + + om = Omega() + # train a model + # result is AsyncResult, use .get() to return it's result + result = om.runtime.model('foo').fit('datax', 'datay') + result.get() + + # predict + result = om.runtime.model('foo').predict('datax') + # result is AsyncResult, use .get() to return it's result + print result.get() + """ + +# Implementation note: +# +# We decided to implement each method call explicitely in both +# this class (mixins) and the celery tasks. While it would be possible to +# implement a generic method and task that passes the method and +# arguments to be called, maintainability would suffer and the +# documentation would become very unspecific. We think it is much +# cleaner to have an explicit interface at the chance of missing +# features. If need should arise we can still implement a generic +# method call. + + def __init__(self, modelname, runtime=None): + self.modelname = modelname + self.runtime = runtime + self.pure_python = getattr(settings(), 'OMEGA_FORCE_PYTHON_CLIENT', + False) + self.pure_python = self.pure_python or self._client_is_pure_python() + self.apply_mixins() + + def apply_mixins(self): + """ + apply mixins in defaults.OMEGA_RUNTIME_MIXINS + """ + from omegaml import defaults + for mixin in defaults.OMEGA_RUNTIME_MIXINS: + extend_instance(self, mixin) + + def _client_is_pure_python(self): + try: + import pandas as pd + import numpy as np + import sklearn + except Exception as e: + logging.getLogger().info(e) + return True + else: + return False diff --git a/omegaml/runtimes/runtime.py b/omegaml/runtimes/runtime.py new file mode 100644 index 00000000..63fd286e --- /dev/null +++ b/omegaml/runtimes/runtime.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import + +from celery import Celery + +from omegaml.runtimes.jobproxy import OmegaJobProxy +from omegaml.util import settings + + +class OmegaRuntime(object): + """ + omegaml compute cluster gateway + """ + + def __init__(self, omega, backend=None, + broker=None, celerykwargs=None, celeryconf=None, defaults=None): + self.backend = backend or 'amqp' + self.broker = broker or 'amqp://guest@localhost//' + self.omega = omega + defaults = defaults or settings() + # initialize celery as a runtimes + taskpkgs = defaults.OMEGA_CELERY_IMPORTS + celerykwargs = celerykwargs or {} + celerykwargs.update({'backend': self.backend, + 'broker': self.broker, + 'include': taskpkgs, + }) + celeryconf = celeryconf or defaults.OMEGA_CELERY_CONFIG + self.celeryapp = Celery('omegaml', **celerykwargs) + self.celeryapp.conf.update(celeryconf) + # needed to get it to actually load the tasks (???) + # https://stackoverflow.com/a/35735471 + self.celeryapp.autodiscover_tasks(taskpkgs, force=True) + self.celeryapp.finalize() + + def __repr__(self): + return 'OmegaRuntime({})'.format(self.omega.__repr__()) + + def model(self, modelname): + """ + return a model for remote execution + """ + from omegaml.runtimes.modelproxy import OmegaModelProxy + return OmegaModelProxy(modelname, runtime=self) + + def job(self, jobname): + """ + return a job for remote exeuction + """ + return OmegaJobProxy(jobname, runtime=self) + + def task(self, name): + """ + retrieve the task function from the celery instance + + we do it like this so we can per-OmegaRuntime instance + celery configurations (as opposed to using the default app's + import, which seems to confuse celery) + """ + # import omegapkg.tasks + return self.celeryapp.tasks.get(name) + + def settings(self): + """ + return the runtimes's cluster settings + """ + return self.task('omegaml.tasks.omega_settings').delay().get() + + def ping(self, *args, **kwargs): + """ + ping the runtimes + """ + return self.task('omegaml.tasks.omega_ping').delay(*args, **kwargs).get() diff --git a/omegaml/store/__init__.py b/omegaml/store/__init__.py new file mode 100644 index 00000000..c8d3cd94 --- /dev/null +++ b/omegaml/store/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import +from .base import OmegaStore +from .query import MongoQ, Filter +from .queryops import MongoQueryOps, GeoJSON + +qops = MongoQueryOps() \ No newline at end of file diff --git a/omegaml/store/base.py b/omegaml/store/base.py new file mode 100644 index 00000000..ca97449f --- /dev/null +++ b/omegaml/store/base.py @@ -0,0 +1,976 @@ +""" +Native storage for OmegaML using mongodb as the storage layer + +An OmegaStore instance is a MongoDB database. It has at least the +metadata collection which lists all objects stored in it. A metadata +document refers to the following types of objects (metadata.kind): + +* pandas.dfrows - a Pandas DataFrame stored as a collection of rows +* sklearn.joblib - a scikit learn estimator/pipline dumped using joblib.dump() +* python.data - an arbitrary python dict, tuple, list stored as a document + +Note that storing Pandas and scikit learn objects requires the availability +of the respective packages. If either can not be imported, the OmegaStore +degrades to a python.data store only. It will still .list() and get() any +object, however reverts to pure python objects. In this case it is up +to the client to convert the data into an appropriate format for processing. + +Pandas and scikit-learn objects can only be stored if these packages are +availables. put() raises a TypeError if you pass such objects and these +modules cannot be loaded. + +All data are stored within the same mongodb, in per-object collections +as follows: + + * .metadata + all metadata. each object is one document, + See **omegaml.documents.Metadata** for details + * ..files + this is the GridFS instance used to store + blobs (models, numpy, hdf). The actual file name + will be /., where ext is + optionally generated by put() / get(). + * ....data + every other dataset is stored in a separate + collection (dataframes, dicts, lists, tuples). + Any forward slash in prefix is ignored (e.g. 'data/' + becomes 'data') + + DataFrames by default are stored in their own collection, every + row becomes a document. To store dataframes as a binary file, + use `put(...., as_hdf=True).` `.get()` will always return a dataframe. + + Python dicts, lists, tuples are stored as a single document with + a `.data` attribute holding the JSON-converted representation. `.get()` + will always return the corresponding python object of .data. + + Models are joblib.dump()'ed and ziped prior to transferring into + GridFs. .get() will always unzip and joblib.load() before returning + the model. Note this requires that the process using .get() supports + joblib as well as all python classes referred to. If joblib is not + supported, .get() returns a file-like object. + + The .metadata entry specifies the format used to store each + object as well as it's location: + + * metadata.kind + the type of object + * metadata.name + the name of the object, as given on put() + * metadata.gridfile + the gridfs object (if any, null otherwise) + * metadata.collection + the name of the collection + * metadata.attributes + arbitrary custom attributes set in + put(attributes=obj). This is used e.g. by + OmegaRuntime's fit() method to record the + data used in the model's training. + + **.put()** and **.get()** use helper methods specific to the type in + object's type and metadata.kind, respectively. In the future + a plugin system will enable extension to other types. +""" +from __future__ import absolute_import + +import os +import re +import tempfile +from fnmatch import fnmatch +from uuid import uuid4 + +import gridfs +import six +from datetime import datetime +from mongoengine.connection import disconnect, \ + connect +from mongoengine.errors import DoesNotExist +from mongoengine.fields import GridFSProxy +from six import iteritems + +from omegaml.store.fastinsert import fast_insert +from omegaml.util import unravel_index, restore_index, make_tuple, jsonescape, \ + cursor_to_dataframe, convert_dtypes, load_class, extend_instance +from ..documents import Metadata +from ..util import (is_estimator, is_dataframe, is_ndarray, is_spark_mllib, + settings as omega_settings, urlparse, is_series) + + +class OmegaStore(object): + """ + The storage backend for models and data + """ + + def __init__(self, mongo_url=None, bucket=None, prefix=None, kind=None, defaults=None): + """ + :param mongo_url: the mongourl to use for the gridfs + :param bucket: the mongo collection to use for gridfs + :param prefix: the path prefix for files. defaults to blank + :param kind: the kind or list of kinds to limit this store to + """ + self.defaults = defaults or omega_settings() + self.mongo_url = mongo_url or self.defaults.OMEGA_MONGO_URL + self.bucket = bucket or self.defaults.OMEGA_MONGO_COLLECTION + self._fs = None + self._tmppath = None + self.prefix = prefix or '' + self.force_kind = kind + # don't initialize db here to avoid using the default settings + # otherwise Metadata will already have a connection and not use + # the one provided in override_settings + self._db = None + # add backends and mixins + self._apply_mixins() + # register backends + self.register_backends() + + def __repr__(self): + return 'OmegaStore(mongo_url={}, bucket={}, prefix={})'.format(self.mongo_url, self.bucket, self.prefix) + + @property + def tmppath(self): + """ + return an instance-specific temporary path + """ + if self._tmppath is not None: + return self._tmppath + self._tmppath = os.path.join(self.defaults.OMEGA_TMP, uuid4().hex) + os.makedirs(self._tmppath) + return self._tmppath + + @property + def mongodb(self): + """ + Returns a mongo database object + """ + if self._db is not None: + return self._db + # parse salient parts of mongourl, e.g. + # mongodb://user:password@host/dbname + self.parsed_url = urlparse.urlparse(self.mongo_url) + self.database_name = self.parsed_url.path[1:] + host = self.parsed_url.netloc + username, password = None, None + if '@' in host: + creds, host = host.split('@', 1) + if ':' in creds: + username, password = creds.split(':') + # connect via mongoengine + # note this uses a MongoClient in the background, with pooled + # connections. there are multiprocessing issues with pymongo: + # http://api.mongodb.org/python/3.2/faq.html#using-pymongo-with-multiprocessing + # connect=False is due to https://jira.mongodb.org/browse/PYTHON-961 + # this defers connecting until the first access + # serverSelectionTimeoutMS=2500 is to fail fast, the default is 30000 + # FIXME use an instance specific alias. requires that every access + # to Metadata is configured correctly. this to avoid sharing + # inadevertedly between threads and processes. + # alias = 'omega-{}'.format(uuid4().hex) + alias = 'omega' + # always disconnect before registering a new connection because + # connect forgets all connection settings upon disconnect WTF?! + disconnect(alias) + connection = connect(alias=alias, db=self.database_name, + host=host, + username=username, + password=password, + connect=False, + authentication_source='admin', + serverSelectionTimeoutMS=2500) + self._db = getattr(connection, self.database_name) + # mongoengine 0.15.0 connection setup is seriously broken -- it does + # not remember username/password on authenticated connections + # so we reauthenticate here + if username and password: + self._db.logout() + self._db.authenticate(username, password, source='admin') + return self._db + + @property + def fs(self): + """ + Retrieve a gridfs instance using url and collection provided + + :return: a gridfs instance + """ + if self._fs is not None: + return self._fs + try: + self._fs = gridfs.GridFS(self.mongodb, collection=self.bucket) + except Exception as e: + raise e + return self._fs + + def metadata(self, name=None, bucket=None, prefix=None, version=-1): + """ + Returns a metadata document for the given entry name + + FIXME: version attribute does not do anything + FIXME: metadata should be stored in a bucket-specific collection + to enable access control, see https://docs.mongodb.com/manual/reference/method/db.createRole/#db.createRole + """ + db = self.mongodb + fs = self.fs + prefix = prefix or self.prefix + bucket = bucket or self.bucket + # Meta is to silence lint on import error + Meta = Metadata + return Meta.objects(name=name, prefix=prefix, bucket=bucket).first() + + def make_metadata(self, name, kind, bucket=None, prefix=None, **kwargs): + """ + create or update a metadata object + + this retrieves a Metadata object if it exists given the kwargs. Only + the name, prefix and bucket arguments are considered + + for existing Metadata objects, the attributes kw is treated as follows: + + * attributes=None, the existing attributes are left as is + * attributes={}, the attributes value on an existing metadata object + is reset to the empty dict + * attributes={ some : value }, the existing attributes are updated + + For new metadata objects, attributes defaults to {} if not specified, + else is set as provided. + + :param name: the object name + :param bucket: the bucket, optional, defaults to self.bucket + :param prefix: the prefix, optional, defaults to self.prefix + + """ + # TODO kept _make_metadata for backwards compatibility. + return self._make_metadata(name, bucket=bucket, prefix=prefix, + kind=kind, **kwargs) + + def _make_metadata(self, name=None, bucket=None, prefix=None, **kwargs): + """ + create or update a metadata object + + this retrieves a Metadata object if it exists given the kwargs. Only + the name, prefix and bucket arguments are considered + + for existing Metadata objects, the attributes kw is treated as follows: + + * attributes=None, the existing attributes are left as is + * attributes={}, the attributes value on an existing metadata object + is reset to the empty dict + * attributes={ some : value }, the existing attributes are updated + + For new metadata objects, attributes defaults to {} if not specified, + else is set as provided. + + :param name: the object name + :param bucket: the bucket, optional, defaults to self.bucket + :param prefix: the prefix, optional, defaults to self.prefix + """ + bucket = bucket or self.bucket + prefix = prefix or self.prefix + meta = self.metadata(name=name, + prefix=prefix, + bucket=bucket) + if meta: + for k, v in six.iteritems(kwargs): + if k == 'attributes' and v is not None and len(v) > 0: + previous = getattr(meta, k, {}) + previous.update(v) + setattr(meta, k, previous) + elif k == 'attributes' and v is not None and len(v) == 0: + setattr(meta, k, {}) + elif k == 'attributes' and v is None: + # ignore non specified attributes + continue + else: + # by default set whatever attribute is provided + setattr(meta, k, v) + else: + meta = Metadata(name=name, bucket=bucket, prefix=prefix, + **kwargs) + return meta + + def _drop_metadata(self, name=None, **kwargs): + # internal method to delete meta data of an object + meta = self.metadata(name, **kwargs) + if meta is not None: + meta.delete() + + def collection(self, name=None): + """ + Returns a mongo db collection as a datastore + + :param name: the collection to use. if none defaults to the + collection name given on instantiation. the actual collection name + used is always prefix + name + '.data' + """ + collection = self._get_obj_store_key(name, '.datastore') + collection = collection.replace('..', '.') + try: + datastore = getattr(self.mongodb, collection) + except Exception as e: + raise e + return datastore + + def _apply_mixins(self): + """ + apply mixins in defaults.OMEGA_STORE_MIXINS + """ + for mixin in self.defaults.OMEGA_STORE_MIXINS: + extend_instance(self, mixin) + + def register_backends(self): + """ + register backends in defaults.OMEGA_STORE_BACKENDS + """ + for kind, backend in six.iteritems(self.defaults.OMEGA_STORE_BACKENDS): + self.register_backend(kind, backend) + + def register_backend(self, kind, backend): + """ + register a backend class + + :param kind: (str) the backend kind + :param backend: (class) the backend class + """ + self.defaults.OMEGA_STORE_BACKENDS[kind] = load_class(backend) + if kind not in Metadata.KINDS: + Metadata.KINDS.append(kind) + return self + + def register_mixin(self, mixincls): + """ + register a mixin class + + :param mixincls: (class) the mixin class + """ + self.defaults.OMEGA_STORE_MIXINS.append(mixincls) + extend_instance(self, mixincls) + return self + + def put(self, obj, name, attributes=None, **kwargs): + """ + Stores an objecs, store estimators, pipelines, numpy arrays or + pandas dataframes + """ + for kind, backend_cls in six.iteritems(self.defaults.OMEGA_STORE_BACKENDS): + backend_cls = load_class(backend_cls) + if backend_cls.supports(obj, name, attributes=attributes, **kwargs): + backend = self.get_backend_bykind(kind) + return backend.put(obj, name, attributes=attributes, **kwargs) + if is_estimator(obj): + backend = self.get_backend_bykind(Metadata.SKLEARN_JOBLIB) + return backend.put_model(obj, name, attributes) + elif is_spark_mllib(obj): + backend = self.get_backend_bykind(Metadata.SKLEARN_JOBLIB) + return backend.put_model(obj, name, attributes, **kwargs) + elif is_dataframe(obj) or is_series(obj): + groupby = kwargs.get('groupby') + if obj.empty: + from warnings import warn + warn( + 'Provided dataframe is empty, ignoring it, doing nothing here!') + return None + if kwargs.pop('as_hdf', False): + return self.put_dataframe_as_hdf( + obj, name, attributes, **kwargs) + elif groupby: + return self.put_dataframe_as_dfgroup( + obj, name, groupby, attributes) + append = kwargs.get('append', None) + timestamp = kwargs.get('timestamp', None) + index = kwargs.get('index', None) + return self.put_dataframe_as_documents( + obj, name, append, attributes, index, timestamp) + elif is_ndarray(obj): + return self.put_ndarray_as_hdf(obj, name, + attributes=attributes, + **kwargs) + elif isinstance(obj, (dict, list, tuple)): + if kwargs.pop('as_hdf', False): + self.put_pyobj_as_hdf(obj, name, + attributes=attributes, **kwargs) + return self.put_pyobj_as_document(obj, name, + attributes=attributes, + **kwargs) + else: + raise TypeError('type %s not supported' % type(obj)) + + def put_dataframe_as_documents(self, obj, name, append=None, + attributes=None, index=None, + timestamp=None): + """ + store a dataframe as a row-wise collection of documents + + :param obj: the dataframe to store + :param name: the name of the item in the store + :param append: if False collection will be dropped before inserting, + if True existing documents will persist. Defaults to True. If not + specified and rows have been previously inserted, will issue a + warning. + :param index: list of columns, using +, -, @ as a column prefix to + specify ASCENDING, DESCENDING, GEOSPHERE respectively. For @ the + column has to represent a valid GeoJSON object. + :param timestamp: if True or a field name adds a timestamp. If the + value is a boolean or datetime, uses _created as the field name. + The timestamp is always datetime.datetime.utcnow(). May be overriden + by specifying the tuple (col, datetime). + :return: the Metadata object created + """ + from .queryops import MongoQueryOps + collection = self.collection(name) + if is_series(obj): + import pandas as pd + obj = pd.DataFrame(obj, index=obj.index, columns=[str(obj.name)]) + store_series = True + else: + store_series = False + if append is False: + self.drop(name, force=True) + elif append is None and collection.count(limit=1): + from warnings import warn + warn('%s already exists, will append rows' % name) + if index: + # get index keys + if isinstance(index, dict): + idx_kwargs = index + index = index.pop('columns') + else: + idx_kwargs = {} + # create index with appropriate options + keys, idx_kwargs = MongoQueryOps().make_index(index, **idx_kwargs) + collection.create_index(keys, **idx_kwargs) + if timestamp: + dt = datetime.utcnow() + if isinstance(timestamp, bool): + col = '_created' + elif isinstance(timestamp, six.string_types): + col = timestamp + elif isinstance(timestamp, datetime): + col, dt = '_created', timestamp + elif isinstance(timestamp, tuple): + col, dt = timestamp + obj[col] = dt + # store dataframe indicies + # FIXME this may be a performance issue, use size stored on stats or metadata + row_count = self.collection(name).count() + obj, idx_meta = unravel_index(obj, row_count=row_count) + stored_columns = [jsonescape(col) for col in obj.columns] + column_map = list(zip(obj.columns, stored_columns)) + dtypes = { + dict(column_map).get(k): v.name + for k, v in iteritems(obj.dtypes) + } + kind_meta = { + 'columns': column_map, + 'dtypes': dtypes, + 'idx_meta': idx_meta + } + # ensure column names to be strings + obj.columns = stored_columns + # create mongon indicies for data frame index columns + df_idxcols = [col for col in obj.columns if col.startswith('_idx')] + if df_idxcols: + keys, idx_kwargs = MongoQueryOps().make_index(df_idxcols) + collection.create_index(keys, **idx_kwargs) + # create index on row id + keys, idx_kwargs = MongoQueryOps().make_index(['_om#rowid']) + collection.create_index(keys, **idx_kwargs) + # bulk insert + # -- get native objects + # -- seems to be required since pymongo 3.3.x. if not converted + # pymongo raises Cannot Encode object for int64 types + obj = obj.astype('O') + fast_insert(obj, self, name) + kind = (Metadata.PANDAS_SEROWS + if store_series + else Metadata.PANDAS_DFROWS) + meta = self._make_metadata(name=name, + prefix=self.prefix, + bucket=self.bucket, + kind=kind, + kind_meta=kind_meta, + attributes=attributes, + collection=collection.name) + return meta.save() + + def put_dataframe_as_dfgroup(self, obj, name, groupby, attributes=None): + """ + store a dataframe grouped by columns in a mongo document + + :Example: + + > # each group + > { + > #group keys + > key: val, + > _data: [ + > # only data keys + > { key: val, ... } + > ]} + + """ + + def row_to_doc(obj): + for gval, gdf in obj.groupby(groupby): + gval = make_tuple(gval.astype('O')) + doc = dict(zip(groupby, gval)) + datacols = list(set(gdf.columns) - set(groupby)) + doc['_data'] = gdf[datacols].astype('O').to_dict('records') + yield doc + + datastore = self.collection(name) + datastore.drop() + datastore.insert_many(row_to_doc(obj)) + return self._make_metadata(name=name, + prefix=self.prefix, + bucket=self.bucket, + kind=Metadata.PANDAS_DFGROUP, + attributes=attributes, + collection=datastore.name).save() + + def put_dataframe_as_hdf(self, obj, name, attributes=None): + filename = self._get_obj_store_key(name, '.hdf') + hdffname = self._package_dataframe2hdf(obj, filename) + with open(hdffname, 'rb') as fhdf: + fileid = self.fs.put(fhdf, filename=filename) + return self._make_metadata(name=name, + prefix=self.prefix, + bucket=self.bucket, + kind=Metadata.PANDAS_HDF, + attributes=attributes, + gridfile=GridFSProxy(grid_id=fileid)).save() + + def put_ndarray_as_hdf(self, obj, name, attributes=None): + """ store numpy array as hdf + + this is hack, converting the array to a dataframe then storing + it + """ + import pandas as pd + df = pd.DataFrame(obj) + return self.put_dataframe_as_hdf(df, name, attributes=attributes) + + def put_pyobj_as_hdf(self, obj, name, attributes=None): + """ + store list, tuple, dict as hdf + + this requires the list, tuple or dict to be convertible into + a dataframe + """ + import pandas as pd + df = pd.DataFrame(obj) + return self.put_dataframe_as_hdf(df, name, attributes=attributes) + + def put_pyobj_as_document(self, obj, name, attributes=None, append=True): + """ + store a dict as a document + + similar to put_dataframe_as_documents no data will be replaced by + default. that is, obj is appended as new documents into the objects' + mongo collection. to replace the data, specify append=False. + """ + collection = self.collection(name) + if append is False: + collection.drop() + elif append is None and collection.count(limit=1): + from warnings import warn + warn('%s already exists, will append rows' % name) + objid = collection.insert({'data': obj}) + return self._make_metadata(name=name, + prefix=self.prefix, + bucket=self.bucket, + kind=Metadata.PYTHON_DATA, + collection=collection.name, + attributes=attributes, + objid=objid).save() + + def drop(self, name, force=False, version=-1): + """ + Drop the object + + :param name: The name of the object + :param force: If True ignores DoesNotExist exception, defaults to False + meaning this raises a DoesNotExist exception of the name does not + exist + :return: True if object was deleted, False if not. + If force is True and + the object does not exist it will still return True + """ + meta = self.metadata(name, version=version) + if meta is None and not force: + raise DoesNotExist() + collection = self.collection(name) + if collection: + self.mongodb.drop_collection(collection.name) + if meta: + if meta.collection: + self.mongodb.drop_collection(meta.collection) + self._drop_metadata(name) + return True + if meta and meta.gridfile is not None: + meta.gridfile.delete() + self._drop_metadata(name) + return True + return False + + def get_backend_bykind(self, kind, model_store=None, data_store=None, + **kwargs): + """ + return the backend by a given object kind + + :param kind: The object kind + :param model_store: the OmegaStore instance used to store models + :param data_store: the OmegaStore instance used to store data + :param kwargs: the kwargs passed to the backend initialization + :return: the backend + """ + backend_cls = load_class(self.defaults.OMEGA_STORE_BACKENDS[kind]) + model_store = model_store or self + data_store = data_store or self + backend = backend_cls(model_store=model_store, + data_store=data_store, **kwargs) + return backend + + def get_backend(self, name, model_store=None, data_store=None, **kwargs): + """ + return the backend by a given object name + + :param kind: The object kind + :param model_store: the OmegaStore instance used to store models + :param data_store: the OmegaStore instance used to store data + :param kwargs: the kwargs passed to the backend initialization + :return: the backend + """ + meta = self.metadata(name) + if meta is not None: + backend_cls = load_class(self.defaults.OMEGA_STORE_BACKENDS.get(meta.kind)) + if backend_cls: + model_store = model_store or self + data_store = data_store or self + backend = backend_cls(model_store=model_store, + data_store=data_store, **kwargs) + return backend + return None + + def getl(self, *args, **kwargs): + """ return a lazy MDataFrame for a given object + + Same as .get, but returns a MDataFrame + + """ + return self.get(*args, lazy=True, **kwargs) + + def get(self, name, version=-1, force_python=False, + **kwargs): + """ + Retrieve an object + + :param name: The name of the object + :param version: Version of the stored object (not supported) + :param force_python: Return as a python object + :param kwargs: kwargs depending on object kind + :return: an object, estimator, pipelines, data array or pandas dataframe + previously stored with put() + """ + meta = self.metadata(name, version=version) + if meta is None: + return None + if not force_python: + backend = self.get_backend(name) + if backend is not None: + return backend.get(name, **kwargs) + if meta.kind == Metadata.SKLEARN_JOBLIB: + backend = self.get_backend(name) + return backend.get_model(name) + elif meta.kind == Metadata.SPARK_MLLIB: + backend = self.get_backend(name) + return backend.get_model(name, version) + elif meta.kind == Metadata.PANDAS_DFROWS: + return self.get_dataframe_documents(name, version=version, + **kwargs) + elif meta.kind == Metadata.PANDAS_SEROWS: + return self.get_dataframe_documents(name, version=version, + is_series=True, + **kwargs) + elif meta.kind == Metadata.PANDAS_DFGROUP: + return self.get_dataframe_dfgroup( + name, version=version, **kwargs) + elif meta.kind == Metadata.PYTHON_DATA: + return self.get_python_data(name, version=version) + elif meta.kind == Metadata.PANDAS_HDF: + return self.get_dataframe_hdf(name, version=version) + return self.get_object_as_python(meta, version=version) + + def get_dataframe_documents(self, name, columns=None, lazy=False, + filter=None, version=-1, is_series=False, + **kwargs): + """ + Internal method to return DataFrame from documents + + :param name: the name of the object (str) + :param columns: the column projection as a list of column names + :param lazy: if True returns a lazy representation as an MDataFrame. + If False retrieves all data and returns a DataFrame (default) + :param filter: the filter to be applied as a column__op=value dict + :param version: the version to retrieve (not supported) + :param is_series: if True retruns a Series instead of a DataFrame + :param kwargs: remaining kwargs are used a filter. The filter kwarg + overrides other kwargs. + :return: the retrieved object (DataFrame, Series or MDataFrame) + + """ + collection = self.collection(name) + if lazy: + from ..mdataframe import MDataFrame + filter = filter or kwargs + df = MDataFrame(collection, columns=columns).query(**filter) + if is_series: + df = df[0] + else: + # TODO ensure the same processing is applied in MDataFrame + # TODO this method should always use a MDataFrame disregarding lazy + filter = filter or kwargs + if filter: + from .query import Filter + query = Filter(collection, **filter).query + cursor = collection.find(filter=query, projection=columns) + else: + cursor = collection.find(projection=columns) + # restore dataframe + df = cursor_to_dataframe(cursor) + if '_id' in df.columns: + del df['_id'] + meta = self.metadata(name) + if hasattr(meta, 'kind_meta'): + df = convert_dtypes(df, meta.kind_meta.get('dtypes', {})) + # -- restore columns + meta_columns = dict(meta.kind_meta.get('columns')) + if meta_columns: + # apply projection, if any + if columns: + # get only projected columns + # meta_columns is {origin_column: stored_column} + orig_columns = dict({k: v for k, v in iteritems(meta_columns) + if k in columns or v in columns}) + else: + # restore columns to original name + orig_columns = meta_columns + df.rename(columns=orig_columns, inplace=True) + # -- restore indexes + idx_meta = meta.kind_meta.get('idx_meta') + if idx_meta: + df = restore_index(df, idx_meta) + # -- restore row order + if is_series: + index = df.index + name = df.columns[0] + df = df[name] + df.index = index + df.name = None if name == 'None' else name + return df + + def rebuild_params(self, kwargs, collection): + """ + Returns a modified set of parameters for querying mongodb + based on how the mongo document is structured and the + fields the document is grouped by. + + **Note: Explicitly to be used with get_grouped_data only** + + :param kwargs: Mongo filter arguments + :param collection: The name of mongodb collection + :return: Returns a set of parameters as dictionary. + """ + modified_params = {} + db_structure = collection.find_one({}, {'_id': False}) + groupby_columns = list(set(db_structure.keys()) - set(['_data'])) + if kwargs is not None: + for item in kwargs: + if item not in groupby_columns: + modified_query_param = '_data.' + item + modified_params[modified_query_param] = kwargs.get(item) + else: + modified_params[item] = kwargs.get(item) + return modified_params + + def get_dataframe_dfgroup(self, name, version=-1, kwargs=None): + """ + Return a grouped dataframe + + :param name: the name of the object + :param version: not supported + :param kwargs: mongo db query arguments to be passed to + collection.find() as a filter. + + """ + import pandas as pd + def convert_doc_to_row(cursor): + for doc in cursor: + data = doc.pop('_data', []) + for row in data: + doc.update(row) + yield doc + + datastore = self.collection(name) + kwargs = kwargs if kwargs else {} + params = self.rebuild_params(kwargs, datastore) + cursor = datastore.find(params, {'_id': False}) + df = pd.DataFrame(convert_doc_to_row(cursor)) + return df + + def get_dataframe_hdf(self, name, version=-1): + """ + Retrieve dataframe from hdf + + :param name: The name of object + :param version: The version of object (not supported) + :return: Returns a python pandas dataframe + :raises: gridfs.errors.NoFile + """ + df = None + filename = self._get_obj_store_key(name, '.hdf') + if filename.endswith('.hdf') and self.fs.exists(filename=filename): + df = self._extract_dataframe_hdf(filename, version=version) + return df + else: + raise gridfs.errors.NoFile( + "{0} does not exist in mongo collection '{1}'".format( + name, self.bucket)) + + def get_python_data(self, name, version=-1): + """ + Retrieve objects as python data + + :param name: The name of object + :param version: The version of object + + :return: Returns the object as python list object + """ + datastore = self.collection(name) + cursor = datastore.find() + data = (d.get('data') for d in cursor) + return list(data) + + def get_object_as_python(self, meta, version=-1): + """ + Retrieve object as python object + + :param meta: The metadata object + :param version: The version of the object + + :return: Returns data as python object + """ + if meta.kind == Metadata.SKLEARN_JOBLIB: + return meta.gridfile + if meta.kind == Metadata.PANDAS_HDF: + return meta.gridfile + if meta.kind == Metadata.PANDAS_DFROWS: + return list(getattr(self.mongodb, meta.collection).find()) + if meta.kind == Metadata.PYTHON_DATA: + col = getattr(self.mongodb, meta.collection) + return col.find_one(dict(_id=meta.objid)).get('data') + raise TypeError('cannot return kind %s as a python object' % meta.kind) + + def list(self, pattern=None, regexp=None, kind=None, raw=False, + include_temp=False, bucket=None, prefix=None): + """ + List all files in store + + specify pattern as a unix pattern (e.g. :code:`models/*`, + or specify regexp) + + :param pattern: the unix file pattern or None for all + :param regexp: the regexp. takes precedence over pattern + :param raw: if True return the meta data objects + :return: List of files in store + + """ + db = self.mongodb + searchkeys = dict(bucket=bucket or self.bucket, + prefix=prefix or self.prefix) + if kind or self.force_kind: + kind = kind or self.force_kind + if isinstance(kind, (tuple, list)): + searchkeys.update(kind__in=kind) + else: + searchkeys.update(kind=kind) + meta = Metadata.objects(**searchkeys) + if raw: + if regexp: + files = [f for f in meta if re.match(regexp, f.name)] + elif pattern: + files = [f for f in meta if fnmatch(f.name, pattern)] + else: + files = [f for f in meta] + else: + files = [d.name for d in meta] + if regexp: + files = [f for f in files if re.match(regexp, f)] + elif pattern: + files = [f for f in files if fnmatch(f, pattern)] + files = [f.replace('.omm', '') for f in files] + if not include_temp: + files = [f for f in files if not f.startswith('_temp')] + return files + + def object_store_key(self, name, ext): + """ + Returns the store key + + :param name: The name of object + :param ext: The extension of the filename + + :return: A filename with relative bucket, prefix and name + """ + return self._get_obj_store_key(name, ext) + + def _get_obj_store_key(self, name, ext): + # backwards compatilibity implementation of object_store_key() + name = '%s.%s' % (name, ext) if not name.endswith(ext) else name + filename = '{bucket}.{prefix}.{name}'.format( + bucket=self.bucket, + prefix=self.prefix, + name=name, + ext=ext).replace('/', '_').replace('..', '.') + return filename + + def _package_dataframe2hdf(self, df, filename, key=None): + """ + Package a dataframe as a hdf file + + :param df: The dataframe + :param filename: Name of file + + :return: Filename of hdf file + """ + lpath = tempfile.mkdtemp() + fname = os.path.basename(filename) + hdffname = os.path.join(self.tmppath, fname + '.hdf') + key = key or 'data' + df.to_hdf(hdffname, key) + return hdffname + + def _extract_dataframe_hdf(self, filename, version=-1): + """ + Extracts a dataframe from a stored hdf file + + :param filename: The name of file + :param version: The version of file + + :return: Pandas dataframe + """ + import pandas as pd + hdffname = os.path.join(self.tmppath, filename) + dirname = os.path.dirname(hdffname) + if not os.path.exists(dirname): + os.makedirs(dirname) + try: + outf = self.fs.get_version(filename, version=version) + except gridfs.errors.NoFile as e: + raise e + with open(hdffname, 'wb') as hdff: + hdff.write(outf.read()) + hdf = pd.HDFStore(hdffname) + key = list(hdf.keys())[0] + df = hdf[key] + hdf.close() + return df \ No newline at end of file diff --git a/omegaml/store/fastinsert.py b/omegaml/store/fastinsert.py new file mode 100644 index 00000000..379d0053 --- /dev/null +++ b/omegaml/store/fastinsert.py @@ -0,0 +1,57 @@ +from multiprocessing import Pool +from itertools import repeat +from pymongo import MongoClient + +pool = None + +def dfchunker(df, size=10000): + """ chunk a dataframe as in iterator """ + return (df.iloc[pos:pos + size].copy() for pos in range(0, len(df), size)) + + +def insert_chunk(job): + """ + insert one chunk of data + + :param job: the (dataframe, mongo_url, collection_name) tuple. mongo_url + should include the database name, as the collection is taken + from the default database of the connection. + """ + sdf, mongo_url, collection_name = job + client = MongoClient(mongo_url, authSource='admin' ) + db = client.get_database() + collection = db[collection_name] + result = collection.insert_many(sdf.to_dict(orient='records')) + client.close() + return mongo_url, db.name, collection_name, len(result.inserted_ids) + + +def fast_insert(df, omstore, name, chunk_size=int(1e4)): + """ + fast insert of dataframe to mongodb + + Depending on size use single-process or multiprocessing. Typically + multiprocessing is faster on datasets with > 10'000 data elements + (rows x columns). Note this may max out your CPU and may use + processor count * chunksize of additional memory. The chunksize is + set to 10'000. The processor count is the default used by multiprocessing, + typically the number of CPUs reported by the operating system. + + :param df: dataframe + :param omstore: the OmegaStore to use. will be used to get the mongo_url + :param name: the dataset name in OmegaStore to use. will be used to get the + collection name from the omstore + """ + global pool + if len(df) * len(df.columns) > chunk_size: + mongo_url = omstore.mongo_url + collection_name = omstore.collection(name).name + # we crossed upper limits of single threaded processing, use a Pool + # use the cached pool + pool = pool or Pool() + jobs = zip(dfchunker(df, size=chunk_size), + repeat(mongo_url), repeat(collection_name)) + pool.map(insert_chunk, (job for job in jobs)) + else: + # still within bounds for single threaded inserts + omstore.collection(name).insert_many(df.to_dict(orient='records')) diff --git a/omegaml/store/filtered.py b/omegaml/store/filtered.py new file mode 100644 index 00000000..e4994cc0 --- /dev/null +++ b/omegaml/store/filtered.py @@ -0,0 +1,169 @@ + +from __future__ import absolute_import + +from omegaml.util import PickableCollection +from pymongo.collection import Collection + +from omegaml.store import qops +from omegaml.store.query import Filter + + +class FilteredCollection(Collection): + + """ + A permanently filtered collection + + Supports all methods as a Collection does, however any filter or query + argument is permanently set at instantiation + + fcoll = FilteredCollection(collection, query={ expression }) + + Any subsequent operation will automatically apply the query expression. + + Note that v.v. a Collection and all methods that accept a filter as their first + argument have a changed signature - the filter argument is optional + with all FilteredCollection methods, as the filter is set at instantiation. + + Example: + + # in pymongo + + filter = {expression} + coll.find_one_and_replace(filter, replace) + + # FilteredCollection + + coll = FilteredCollection(query={expression}) + coll.find_one_and_replace(replace, filter=None) + + This is so that calls to a FilteredCollection feel more natural, as opposed + to specifying an empty filter argument on every call. Still, an additional + filter can be specified on every method that accepts the filter= optional + argument: + + # temporarily add another filter + + coll.find_one_and_replace(replace, filter={expression}) + + Here expression will only apply to this particular method call. The + global filter set by query= is unchanged. + + If no expression is given, the empty expression {} is assumed. To change + the expression for the set fcoll.query = { expression } + """ + + def __init__(self, collection, query=None, projection=None, **kwargs): + if isinstance(collection, (Collection, PickableCollection)): + database = collection.database + name = collection.name + else: + raise ValueError('collection should be a pymongo.Collection') + query = query or {} + super(FilteredCollection, self).__init__( + database, name, create=False, **kwargs) + self.query = Filter(self, **query).query + self.projection = projection + def replace_one(self, replacement, filter=None, upsert=False, + bypass_document_validation=False): + query = dict(self.query) + query.update(filter or {}) + kwargs = dict(query, replacement, upsert=upsert, + bypass_document_validation=bypass_document_validation) + return super(FilteredCollection, self).replace_one(**kwargs) + def update_one(self, update, filter=None, upsert=False, + bypass_document_validation=False): + query = dict(self.query) + query.update(filter or {}) + kwargs = dict(query, update, upsert=upsert, + bypass_document_validation=bypass_document_validation) + return super(FilteredCollection, self).update_one(**kwargs) + def update_many(self, update, filter=None, upsert=False, + bypass_document_validation=False): + query = dict(self.query) + query.update(filter or {}) + kwargs = dict(query, update, upsert=upsert, + bypass_document_validation=bypass_document_validation) + return super(FilteredCollection, self).update_many(**kwargs) + def delete_one(self, filter=None): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).delete_one(query) + def delete_many(self, filter=None): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).delete_many(query) + def aggregate(self, pipeline, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + pipeline.insert(0, qops.MATCH(query)) + return super(FilteredCollection, self).aggregate(pipeline, **kwargs) + def find(self, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).find(filter=query, **kwargs) + def find_one(self, filter=None, *args, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).find_one(query, *args, **kwargs) + def find_one_and_delete(self, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).find_one_and_delete(query, + **kwargs) + def find_one_and_replace(self, replacement, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).find_one_and_replace(query, + replacement, + **kwargs) + def find_one_and_update(self, update, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).find_one_and_update(query, + update, + **kwargs) + def count(self, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).count(filter=query, **kwargs) + def distinct(self, key, filter=None, **kwargs): + query = dict(self.query) + query.update(filter or {}) + return super(FilteredCollection, self).distinct(key, filter=query, + **kwargs) + def group(self, key, initial, reduce, condition=None, **kwargs): + condition = dict(self.query) + condition.update(condition or {}) + return super(FilteredCollection, self).group(key, condition, initial, + reduce, + **kwargs) + def map_reduce(self, m, r, out, full_response=False, query=None, **kwargs): + _query = dict(self.query) + _query.update(query or {}) + return super(FilteredCollection, self).map_reduce(m, r, out, + full_response=False, + query=_query, **kwargs) + def inline_map_reduce(self, m, r, full_response=False, + query=None, **kwargs): + _query = dict(self.query) + _query.update(query or {}) + return super(FilteredCollection, self).inline_map_reduce(m, r, + full_response=False, query=_query, **kwargs) + def insert(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") + def update(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") + def remove(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") + def find_and_modify(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") + def ensure_index(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") + def save(self, *args, **kwargs): + raise NotImplementedError( + "deprecated in Collection and not implemented in FilteredCollection") diff --git a/omegaml/store/query.py b/omegaml/store/query.py new file mode 100644 index 00000000..e96ca718 --- /dev/null +++ b/omegaml/store/query.py @@ -0,0 +1,402 @@ +import copy + +import six + +from omegaml.store.queryops import MongoQueryOps, flatten_keys +from omegaml.util import restore_index + + +class MongoQ(object): + + """ + Query object to filter mongodb collections + + A Q object holds the conditions. You can combine Q objects + using & and | operators, e.g. + + q1 = Q(year=2015) + q2 = Q(year=2014) + q_all = q1 | q2 + + Query objects are passed into a Filter, and evaluated when + the filter's .value property is read. + + Conditions specified in one Q object are AND together. Various + operators can be specified using __ syntax, e.g. year__eq=2015. + + eq == (default) + lt < + lte <= + gt > + gte >= + ne <> + not <> + in isin(v) + between + + startswith + endwith + contains + match + + Evaluation works as follows: + + 1. for each Q object, apply the filters as collection.find(query) + 2. return the dataframe + + """ + def __init__(self, **kwargs): + self.conditions = kwargs + self.qlist = [('', self)] + # should we return ~(conditions) + self._inv = False + # is sorting implied by some operator + self.sorted = False + + def __repr__(self): + r = [] + for op, q in self.qlist: + r.append('%s %s' % (op, q.conditions)) + return 'Q %s' % ('\n'.join(r)) + + def value(self, collection): + """ + resolve the query by actually applying the filter on given collection + + :param collection: the collection objec + :return: the result of the apply_filter() call + """ + return self.apply_filter(collection) + + def apply_filter(self, collection): + """ + apply the filter to the given collection + + This builds the actual mongo query using build_filters and + calls apply_conditions to execute the query + + :param collection: the collection object + :return: the result of the apply_conditions() call + """ + query = self.build_filters() + return self.apply_conditions(collection, query) + + def apply_conditions(self, collection, query): + """ + apply the mongo query on a given collection + + :param collection: the collection + :param query: the query dictionary applicable to collection.find() + :return: the result of collection.find() + """ + operators = flatten_keys(query) + if '$near' in operators: + self.sorted = True + return collection.find(query) + + def build_filters(self): + """ + build the complex mongodb filter from query definitions + + This takes all query definitions in qlist, builds queries + using .build_conditions() and concatenates the queries by + and/or as defined the in qlist's op value. + + :return: the query suitable for the collection.find() method + """ + query = {} + for i, (op, q) in enumerate(self.qlist): + if i == 0: + query = q.build_conditions() + elif op == '&': + query.setdefault("$and", []) + fn = q.build_conditions if q == self else q.build_filters + query["$and"].append(fn()) + elif op == '|': + fn = q.build_conditions if q == self else q.build_filters + if i == 1: + query = {"$or": [dict(query)]} + else: + query.setdefault("$or", []) + query["$or"].append(fn()) + if self._inv: + query = {"$nor": [query]} + return query + + def build_conditions(self): + """ + For a given query definition return the collection.find() simple query + + Using all conditions, build the query as a dictionary suitable for + collection.find(). This uses MongoQueryOps to transform query + definitions into mongo db syntax. + + :return: the query in mongo db syntax + """ + query = {} + qops = MongoQueryOps() + def addq(k, v): + if k not in query: + query[k] = v + else: + subq = [] + query.setdefault("$and", subq) + for vv in [query.pop(k), v]: + if isinstance(vv, (list, tuple)): + subq.extend(vv) + else: + subq.append({k: vv}) + for k, v in six.iteritems(self.conditions): + # transform query operators as '__', + # however preserve dunder '__' names ss columns + if '__' in k and not k.startswith('__'): + parts = k.split('__') + k = '.'.join(parts[0:-1]) + op = parts[-1] + else: + op = 'eq' + # standard logical operators + if op == 'eq': + addq(k, v) + elif op.upper() in qops.UNARY: + addq(k, getattr(qops, op)(v)) + # type queries + elif op == 'between': + addq("$and", [{k: qops.GTE(v[0])}, + {k: qops.LTE(v[1])}]) + elif op == 'isstring': + addq(k, qops.EQ(qops.TYPE('string'))) + elif op == 'isarray': + addq(k, qops.EQ(qops.TYPE('array'))) + elif op == 'isdouble': + addq(k, qops.TYPE('double')) + elif op == 'isobject': + addq(k, qops.TYPE('object')) + elif op == 'isobject': + addq(k, qops.TYPE('object')) + elif op == 'isdate': + addq(k, qops.TYPE('date')) + elif op == 'isbool': + addq(k, qops.TYPE('bool')) + elif op == 'isnull': + # http://stackoverflow.com/a/944733 + nan = float('nan') + addq(k, qops.EQ(nan) if v else qops.NE(nan)) + elif op in ['islong', 'isint']: + addq(k, qops.TYPE('long')) + elif op == 'contains': + addq(k, qops.REGEX('^%s.*' % v)) + elif op == 'startswith': + addq(k, qops.REGEX('^%s.*' % v)) + elif op == 'endswith': + addq(k, qops.REGEX('.*%s$' % v)) + elif op == 'near': + addq(k, qops.NEAR(v)) + else: + # op from parts[-1] was not an opperator, so assume it is + # an attribute name and apply the eq operator + # e.g. Q(key__subkey=value) + addq('%s.%s' % (k, op), v) + return query + + def negate(self): + """ + negate the query + """ + self._inv = True + return self + + def __and__(self, other): + """ + combine with another MongoQ object using AND + """ + q = copy.deepcopy(self) + q.qlist.append(('&', other)) + return q + + def __or__(self, other): + """ + combine with another MongoQ object using OR + """ + q = copy.deepcopy(self) + q.qlist.append(('|', other)) + return q + + def __invert__(self): + """ + return an inverted version of this object + """ + notq = copy.deepcopy(self).negate() + return notq + + +class Filter(object): + + """ + Filter for OmegaStore objects + + Allow keyword style filtering of dataframes: + + direct filtering + filter = Filter(df, year=2015) + filter.value + + filtering with Q objects + q = Q(year=2015) + filter = Filter(df, q) + + filtering with multiple Q objects + q1 = Q(year=2015) + q2 = Q(year=2014) + filter = Filter(df, q1 | q2) + + filtering with filter() and exclude() + filter = Filter(df, year=2015) + filter.filter(month=1) + filter.exclude(day=15) + """ + _debug = False + + def __init__(self, coll, __query=None, **kwargs): + """ + Filter objects use MongoQ query expression to build + complex filters. + + Internally, Filter keeps the .q MongoQ object to represent + the filter. Filter thus is a thin wrapper around MongoQ to + represent the human-readable high-level API to MongoQ: + + .filter -- add a new condition with AND + .exclude -- exclude values given a condition + .count -- count result lengths. note this triggers .value + .value -- evaluate the filter + + In addition Filter supports tracing errors by using the .trace + flag (set to True, defaults to False). + + :param coll: the collection to apply this filter to + :param __query: a MongoQ object (used internally to combine + filters) + :param kwargs: the filter as a dict of column__op=value pairs + """ + self.coll = coll + self.trace = False + self.exc = None + self.q = self.build_query(__query, **kwargs) + + def build_query(self, __query=None, **kwargs): + """ + build the MongoQ object from given kwargs + + Specify either the __query or the kwargs. If __query is + specified it is returned unchanged. If kwargs is passed these + are used to build a new MongoQ object. The rationale for this + is so that build_query can be used without an if statement in + the calling code (i.e. this methods wraps the if statement on + whether to build a new MongoQ object or to use the existing, + making for more readable code). + + :param __query: the MongoQ object to use + :param kwargs: the filter as a dict of column__op=value pairs + :return: the MongoQ object + """ + if __query: + q = __query + else: + q = MongoQ(**kwargs) + return q + + @property + def query(self): + """ + Convenience method to return the filter's query in MongoDB syntax + """ + return self.q.build_filters() + + def count(self): + """ + Resolve the query and count number of rows + """ + return len(self.value.index) + + @property + def value(self): + """ + Resolve the query and return its results + + Uses self.evaluate() in a safe manner. + """ + try: + value = self.evaluate() + except KeyError as e: + self.exc = e + if self.trace: + raise + raise SyntaxError( + 'Error in Q object: column %s is unknown (KeyError on dataframe)' % e) + return value + + def filter(self, query=None, **kwargs): + """ + Add a new query expression using AND + + :param query: an existing MongoQ object (optional) + :param kwargs: the kwargs as column__op=value pairs + """ + self.q &= self.build_query(query, **kwargs) + return self + + def exclude(self, query=None, **kwargs): + """ + Add a new negated query expression + + This is the equivalent of .filter(~query) + + :param query: an existing MongoQ object (optional) + :param kwargs: the kwargs as column__op=value pairs + """ + self.q &= ~self.build_query(query, **kwargs) + return self + + def __invert__(self): + return Filter(self.coll, ~self.q) + + def __and__(self, other): + return Filter(self.coll, self.q & other.q) + + def __or__(self, other): + return Filter(self.coll, self.q | other.q) + + def evaluate(self): + """ + evaluate the query + + :return: the pandas DataFrame + """ + result = self.q.apply_filter(self.coll) + try: + import pandas as pd + result = pd.DataFrame.from_records(result) + if '_id' in result.columns: + del result['_id'] + result = restore_index(result, dict(), + rowid_sort=not self.q.sorted) + except ImportError: + result = list(result) + return result + + def as_mask(self): + """ + return the True mask of this filter + + note this does not actually return a mask but applies the filter + to the real data. makes more sense even though the results + are somewhat different this way, but match better semantically + """ + return self.value == self.value + + def _repr_html_(self): + return self.value._repr_html_() + + def __repr__(self): + return self.value.__repr__() diff --git a/omegaml/store/queryops.py b/omegaml/store/queryops.py new file mode 100644 index 00000000..5d373329 --- /dev/null +++ b/omegaml/store/queryops.py @@ -0,0 +1,353 @@ +from __future__ import absolute_import +import json +import sys + +import pymongo + +from omegaml.util import make_tuple +import six + + +class GeoJSON(dict): + + """ + simple GeoJSON object + + input: + GeoJSON(lon, lat) + GeoJSON('lon,lat') + GeoJSON(geojson-object) + GeoJSON({ geojson dict with 'coordinates': [lon, lat] }) + GeoJSON(coordinates=[lon, lat]) + + output: + GeoJSON.to_dict() + GeOJSON.to_json() + """ + def __init__(self, lon=None, lat=None, coordinates=None): + if isinstance(lon, GeoJSON): + coordinates = [lon.lon, lon.lat] + elif isinstance(lon, (float, int)) and isinstance(lat, (float, int)): + coordinates = [float(lon), float(lat)] + elif isinstance(lon, dict): + coordinates = self.get_coordinates_from_geojson(lon) + elif isinstance(lon, (list, tuple)): + coordinates = lon + elif isinstance(lon, six.string_types): + coordinates = [float(c) for c in lon.split(',')] + elif isinstance(coordinates, GeoJSON): + coordinates = [coordinates.lon, coordinates.lat] + elif isinstance(coordinates, (list, tuple)): + coordinates = coordinates + elif isinstance(coordinates, dict): + coordinates = self.get_coordinates_from_geojson(lon) + elif isinstance(coordinates, six.string_types): + coordinates = [float(c) for c in lon.split(',')] + else: + coordinates = [] + self.update(self.to_dict(coordinates)) + assert coordinates, "%s is not a valid coordinate" % coordinates + def get_coordinates_from_geojson(self, d): + if 'coordinates' in d: + coordinates = d.get('coordinates') + elif 'geometry' in d \ + and d.get('geometry').get('type') == 'Point': + coordinates = d.get('geometry').get('coordinates') + else: + raise ValueError( + 'expected a valid GeoJSON dict, got %s' % coordinates) + return coordinates + @property + def lat(self): + return self.get('coordinates')[1] + @property + def lon(self): + return self.get('coordinates')[0] + def to_dict(self, coordinates=None): + return { + 'type': 'Point', + 'coordinates': coordinates or self.get('coordinates'), + } + def to_json(self): + return json.dumps(self.to_dict()) + def __unicode__(self): + return u"%s" % self.to_json() + + +class MongoQueryOps(object): + + """ + A Pythonic API to build Mongo query statements + + Examples: + + # setup + x = MongoQueryOps() + d = dict + + # build queries + query = x.MATCH(d(foo='value', baz=x.CONTAINS('value'))) + groupby = x.GROUP(columns=[col1, col2]), count=x.COUNT()) + + result = coll.find(query) + result = coll.aggregate([query, groupby]) + """ + + UNARY = ('IN,LT,LTE,GT,GTE,NE,WHERE,GEOWITHIN,ALL,ELEMWITHIN,NIN' + 'EXISTS,TYPE,REGEX,EQ').split(',') + + def __getattr__(self, k): + if k.upper().replace('_', '') in MongoQueryOps.UNARY: + return self.__unary(k.lower()) + raise AttributeError('operator %s is not supported' % k) + def __unary(self, op): + """ + return a function to create unary operators + + e.g. MongoQueryOps().lt(val) will return an unary function + that on being called will return { "$lt" : val } + """ + def unary(val): + return {"$%s" % op.lower(): val} + return unary + def OR(self, sub): + return {"$or": sub} + def AND(self, sub): + return {"$and": sub} + def NOT(self, sub): + return {"$not": sub} + def GROUP(self, v=None, columns=None, **kwargs): + from collections import OrderedDict + if not v: + v = OrderedDict() + if not columns: + v['_id'] = None + else: + v.setdefault('_id', {}) + v['_id'].update({k: '$%s' % (k.replace('__', '.')) + for k in columns}) + if kwargs: + for _k, _v in six.iteritems(kwargs): + v.setdefault(_k, {}) + if isinstance(_v, dict): + v[_k].update(_v) + else: + v[_k] = _v + return {"$group": v} + def SUM(self, v): + return {"$sum": v} + def COUNT(self): + return self.SUM(1) + def IS(self, **kwargs): + return kwargs + def as_dataframe(self, result, autoflat=True, flatten=None, groupby=None): + """ transform a resultset into a dataframe""" + import pandas as pd + def do_flatten(seq): + """ extract composed keys into columns """ + for r in seq: + row = {} + row.update(r) + if flatten in list(row.keys()): + row.update(row.get(flatten)) + yield row + if autoflat or flatten == True: + flatten = '_id' + df = pd.DataFrame(do_flatten(result)) + if groupby and len(df.index) > 0: + if isinstance(groupby, bool): + cols = list(df.iloc[0]['_id'].keys()) + else: + cols = groupby + df.set_index(cols, inplace=True) + return df + def MATCH(self, *args, **kwargs): + if args: + v = args[0] + else: + v = kwargs + return {"$match": v} + def SEARCH(self, v): + return {"$text": {"$search": v}} + def CONTAINS(self, v): + return {"$regex": '.*%s.*' % v} + def SORT(self, **columns): + """ + sort by columns + """ + return {"$sort": columns} + def d(self, **kwargs): + return dict(**kwargs) + def to_latex(self, df, fout=None): + fout = fout or sys.stdout + fout.write(df.to_latex()) + return fout + def PROJECT(self, fields, include=True): + fields = make_tuple(fields) + return { + '$project': { key: 1 if include else 0 for key in fields} + } + def LOOKUP(self, other, key=None, left_key=None, right_key=None, + target=None): + """ + return a $lookup statement. + + :param other: the other collection + :param key: the key field (applies to both left and right) + :param left_key: the left key field + :param right_key: the right key field + :param target: the target array to store the matching other-documents + """ + return { + "$lookup": { + "from": other, + "localField": left_key or key, + "foreignField": right_key or key, + "as": target or ("%s_%s" % (other, key or right_key)) + } + } + def UNWIND(self, field, preserve=True, index=None): + """ + returns $unwind for the given array field. the index in the + array will be output as _index_. + + :param field: the array field to unwind from + :param preserve: if True, the document is output even if the + array field is empty. + :param index: if given the index field is taken from this field + """ + op = { + "$unwind": { + "path": "${}".format(field) + } + } + if preserve is not None: + op['$unwind'].update({ + "preserveNullAndEmptyArrays": preserve + }) + if index is not None: + op['$unwind'].update({ + "includeArrayIndex": "%s_%s" % ('_index_', index), + }) + return op + def OUT(self, name): + return {"$out": name} + def SET(self, column, value): + return {"$set": {column: value}} + def NEAR(self, lon=None, lat=None, location=None, maxd=None, mind=None): + """ + return a $near expression from an explicit lon/lat coordinate, a + GeoJSON object, a GeoJSON dictionary or a string + """ + if isinstance(lon, six.string_types): + location = GeoJSON(lon) + elif isinstance(lon, (list, tuple)): + if len(lon) == 4: + lon, lat, mind, maxd = lon + elif len(lon) == 3: + lon, lat, maxd = lon + else: + lon, lat = lon + location = GeoJSON(lon, lat) + elif isinstance(lon, GeoJSON): + location = lon + elif isinstance(lon, dict): + location = GeoJSON(lon.get('location')) + maxd = lon.get('maxd') + mind = lon.get('mind') + elif not location: + assert "invalid arguments. Specify coordinates=GeoJSON(lon, lat)" + else: + pass + if isinstance(location, (list, tuple)): + lon, lat = location + else: + lon, lat = location.get('coordinates') + assert lon, "invalid coordinate lon=%s lat=%s" % (lon, lat) + assert lat, "invalid coordinate lon=%s lat=%s" % (lon, lat) + nearq = { + '$near': { + '$geometry': { + 'type': 'Point', + 'coordinates': [lon, lat], + }, + } + } + if maxd: + nearq['$near']['$maxDistance'] = maxd + if mind: + nearq['$near']['$minDistance'] = mind + return nearq + def REPLACEROOT(self, field): + return { + '$replaceRoot': { + 'newRoot': "${}".format(field) + } + } + def make_index(self, columns, **kwargs): + """ + return an index specification suitable for collection.create_index() + + using columns specs like ['+A', '-A'] returns (key, index) + pairs suitable for passing on to create_index. also generates + a name for the index based on the columns and ordering. Use + '@coord' to create a geospecial index. The coord column must + be in GeoJSON format + + :param columns: a single index column, or a list of columns + :param kwargs: optional kwargs to merge. if kwargs contains the + 'name' key it will be preserved + :return: (idx, **kwargs) tuple, pass as create_index(idx, **kwargs) + """ + SORTPREFIX = ['-', '+', '@'] + DIRECTIONMAP = { + '-': pymongo.DESCENDING, + '+': pymongo.ASCENDING, + '@': pymongo.GEOSPHERE, + 'default': pymongo.ASCENDING, + } + columns = make_tuple(columns) + direction_default = DIRECTIONMAP.get('default') + sort_cols = ['+' + col + if col[0] not in SORTPREFIX else col for col in columns] + # get sort kwargs + def direction(col): + return DIRECTIONMAP.get(col[0], direction_default) + idx = [(col.replace('+', '').replace('-', '').replace('@', ''), + direction(col)) + for col in sort_cols] + name = '__'.join([(col + .replace('-', 'desc_') + .replace('+', 'asc_') + .replace('@', 'geo_')) + for col in sort_cols]) + kwargs.setdefault('name', name) + return idx, kwargs + def make_sortkey(self, columns): + """ + using columns specs like ['+A', '-A'] returns (key, index) + pairs suitable for passing on to collection.sort() + """ + sort_key, _ = self.make_index(columns) + return sort_key + + +def flatten_keys(d, keys=None): + """ + get all keys from a dictionary and nested dicts, as a flattened list + + :param d: a dictionary + :param keys: previously found keys. internal use. + :returns: list of flattened keys + """ + keys = keys or [] + keys.extend(list(d.keys())) + for sd in d.values(): + if isinstance(sd, dict): + flatten_keys(sd, keys=keys) + return keys + + +# convenience accessors +x = MongoQueryOps() +d = dict diff --git a/omegaml/tasks.py b/omegaml/tasks.py new file mode 100644 index 00000000..21dd66e4 --- /dev/null +++ b/omegaml/tasks.py @@ -0,0 +1,105 @@ +""" +omega runtime model tasks +""" +from __future__ import absolute_import + +import datetime +import os + +from celery import shared_task +from celery.signals import worker_process_init + +from omegaml.celery_util import OmegamlTask + + +@shared_task(base=OmegamlTask, bind=True) +def omega_predict(self, modelname, Xname, rName=None, pure_python=True, **kwargs): + result = self.get_delegate(modelname).predict(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_predict_proba(self, modelname, Xname, rName=None, pure_python=True, + **kwargs): + result = self.get_delegate(modelname).predict_proba(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_fit(self, modelname, Xname, Yname=None, pure_python=True, **kwargs): + result = self.get_delegate(modelname).fit(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_partial_fit(self, + modelname, Xname, Yname=None, pure_python=True, **kwargs): + result = self.get_delegate(modelname).partial_fit(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_score(self, modelname, Xname, Yname, rName=True, pure_python=True, + **kwargs): + result = self.get_delegate(modelname).score(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_fit_transform(self, modelname, Xname, Yname=None, rName=None, + pure_python=True, **kwargs): + result = self.get_delegate(modelname).fit_transform(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_transform(self, modelname, Xname, rName=None, **kwargs): + result = self.get_delegate(modelname).transform(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_decision_function(self, modelname, Xname, rName=None, **kwargs): + result = self.get_delegate(modelname).decision_function(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_gridsearch(self, modelname, Xname, Yname, parameters=None, **kwargs): + result = self.get_delegate(modelname).gridsearch(*self.delegate_args, **self.delegate_kwargs) + return result + + +@shared_task(base=OmegamlTask, bind=True) +def omega_settings(self): + if os.environ.get('OMEGA_DEBUG'): + defaults = self.om.defaults + return {k: getattr(defaults, k, '') + for k in dir(defaults) if k and k.isupper()} + return {'error': 'settings dump is disabled'} + + +@shared_task(base=OmegamlTask, bind=True) +def omega_ping(task, *args, **kwargs): + import socket + hostname = task.request.hostname or socket.gethostname() + return { + 'message': 'ping return message', + 'time': datetime.datetime.now().isoformat(), + 'args': args, + 'kwargs': kwargs, + 'worker': hostname, + } + + +@worker_process_init.connect +def fix_multiprocessing(**kwargs): + # allow celery to start sub processes + # this is required for sklearn joblib unpickle support + # issue see https://github.com/celery/billiard/issues/168 + # fix source https://github.com/celery/celery/issues/1709 + from multiprocessing import current_process + try: + current_process()._config + except AttributeError: + current_process()._config = {'semprefix': '/mp'} diff --git a/omegaml/tests/__init__.py b/omegaml/tests/__init__.py new file mode 100644 index 00000000..c3961685 --- /dev/null +++ b/omegaml/tests/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import diff --git a/omegaml/tests/test_backendbase.py b/omegaml/tests/test_backendbase.py new file mode 100644 index 00000000..bab31c78 --- /dev/null +++ b/omegaml/tests/test_backendbase.py @@ -0,0 +1,89 @@ +from unittest.case import TestCase + +import omegaml as om +from omegaml.backends.basemodel import BaseModelBackend +from omegaml.documents import Metadata +from omegaml.backends.basedata import BaseDataBackend + + +class CustomBackendTests(TestCase): + + """ + Test custom backends with new kinds + + Note we are not implemented actually working backends. What we're + interested here is that the backend API and Metadata storage work. + """ + def tearDown(self): + # remove custom backend from implementation not to disturb other tests + try: + del om.defaults.OMEGA_STORE_BACKENDS['custom.foo'] + del om.defaults.OMEGA_STORE_BACKENDS['custom.bar'] + Metadata.KINDS.remove('custom.foo') + Metadata.KINDS.remove('custom.bar') + except: + pass + + def test_custom_model_backend(self): + """ + test custom model type + """ + om.models.register_backend('custom.foo', CustomModelBackend) + foo = dict(foo='bar') + meta = om.models.put(foo, 'footest') + self.assertIsInstance(meta, Metadata) + self.assertEqual(meta.kind, 'custom.foo') + meta_stored = om.models.metadata('footest') + self.assertIn('footest', om.models.list()) + self.assertEqual(meta, meta_stored) + with self.assertRaises(NotImplementedError): + om.models.get('footest') + + def test_custom_dataset_backend(self): + """ + test custom dataset type + """ + om.datasets.register_backend('custom.bar', CustomDataBackend) + foo = dict(bar='foo') + meta = om.datasets.put(foo, 'bartest') + self.assertIsInstance(meta, Metadata) + self.assertEqual(meta.kind, 'custom.bar') + meta_stored = om.datasets.metadata('bartest') + self.assertIn('bartest', om.datasets.list()) + self.assertEqual(meta, meta_stored) + with self.assertRaises(NotImplementedError): + om.datasets.get('bartest') + + +class CustomModelBackend(BaseModelBackend): + + """ + Minimalist model backend + """ + + @classmethod + def supports(self, obj, name, **kwargs): + return isinstance(obj, dict) and 'foo' in obj + + def put_model(self, obj, name, attributes=None): + kind = 'custom.foo' + return self.model_store.make_metadata(name, kind, bucket=None, + prefix=None, + attributes=attributes).save() + + +class CustomDataBackend(BaseDataBackend): + + """ + Minimalist dataset backend + """ + + @classmethod + def supports(self, obj, name, **kwargs): + return isinstance(obj, dict) and 'bar' in obj + + def put(self, obj, name, attributes=None): + kind = 'custom.bar' + return self.model_store.make_metadata(name, kind, bucket=None, + prefix=None, + attributes=attributes).save() diff --git a/omegaml/tests/test_filtered_collection.py b/omegaml/tests/test_filtered_collection.py new file mode 100644 index 00000000..a04d650f --- /dev/null +++ b/omegaml/tests/test_filtered_collection.py @@ -0,0 +1,113 @@ +from __future__ import absolute_import +import random +from unittest.case import TestCase + +from pymongo.collection import Collection + +from omegaml import Omega +from omegaml.store.filtered import FilteredCollection +import pandas as pd +from six.moves import range + + +class FilteredCollectionTests(TestCase): + + def setUp(self): + TestCase.setUp(self) + df = pd.DataFrame({'x': list(range(0, 10)) + list(range(0, 10)), + 'y': random.sample(list(range(0, 100)), 20)}) + om = Omega() + om.datasets.put(df, 'sample', append=False) + self.coll = om.datasets.collection('sample') + + def tearDown(self): + TestCase.tearDown(self) + + def test_find(self): + query = {'x': 1} + fcoll = FilteredCollection(self.coll, query=query) + result = list(fcoll.find()) + self.assertTrue(len(result) == 2) + + def test_find_one(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.find_one() + self.assertIsInstance(result, dict) + self.assertEqual(result.get('x'), 9) + + def test_find_one_and_delete(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.find_one_and_delete() + self.assertIsInstance(result, dict) + self.assertEqual(result.get('x'), 9) + result = list(fcoll.find()) + self.assertEqual(len(result), 1) + + def test_find_one_and_replace(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.find_one_and_replace({'x': 9, 'xy': 9000}) + self.assertIsInstance(result, dict) + self.assertEqual(result.get('x'), 9) + result_n = fcoll.find_one() + self.assertEqual(result_n.get('xy'), 9000) + self.assertNotIn('y', result_n) + + def test_find_one_and_update(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.find_one_and_update({'$set': {'xy': 9000}}) + # make sure we get what we wanted + self.assertIsInstance(result, dict) + self.assertEqual(result.get('x'), 9) + # be sure to get the same as before, then test it was updated + result_n = fcoll.find_one({'_id': result.get('_id')}) + self.assertEqual(result_n.get('xy'), 9000) + self.assertEqual(result_n.get('y'), result.get('y')) + + def test_count(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.count() + self.assertEqual(result, 2) + + def test_distinct(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.distinct('x') + self.assertEqual(result, [9]) + + def test_group(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + result = fcoll.group({'x': 1}, + {'count': 0}, + 'function(curr, result) { result.count += 1; }') + self.assertIsInstance(result, list) + self.assertEqual(result[0].get('count'), 2) + + def test_map_reduce(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + # calculate sum as a test value (values are random) + ysum = sum(v.get('y') for v in fcoll.find()) + # use map reduce for the same sum calculation + mapf = 'function() { emit(this.x, this.y); }' + reducef = 'function(x, values) { return Array.sum(values); }' + result = list(fcoll.map_reduce(mapf, reducef, 'mr_out').find()) + self.assertIsInstance(result, list) + self.assertEqual(result[0].get('value'), ysum) + + def test_map_reduce_inline(self): + query = {'x': 9} + fcoll = FilteredCollection(self.coll, query=query) + # calculate sum as a test value (values are random) + ysum = sum(v.get('y') for v in fcoll.find()) + # use map reduce for the same sum calculation + mapf = 'function() { emit(this.x, this.y); }' + reducef = 'function(x, values) { return Array.sum(values); }' + result = fcoll.inline_map_reduce(mapf, reducef) + self.assertIsInstance(result, list) + self.assertEqual(result[0].get('value'), ysum) diff --git a/omegaml/tests/test_jobs.py b/omegaml/tests/test_jobs.py new file mode 100644 index 00000000..a5b66acf --- /dev/null +++ b/omegaml/tests/test_jobs.py @@ -0,0 +1,122 @@ + + +from __future__ import absolute_import + +import os +import tempfile +from unittest import TestCase + +import gridfs +from nbformat import write, v4 + +from omegaml import Omega +from omegaml.documents import Metadata +from omegaml.util import settings as omegaml_settings + + +class JobTests(TestCase): + + def setup(self): + TestCase.setUp(self) + + def tearDown(self): + TestCase.tearDown(self) + for fn in self.om.jobs.list(): + self.om.jobs.drop(fn) + + @property + def om(self): + om = Omega() + return om + + @property + def fs(self): + om = self.om + defaults = omegaml_settings() + fs = om.jobs.get_fs(defaults.OMEGA_NOTEBOOK_COLLECTION) + return fs + + def test_job_put_get(self): + """ + test job put and get + """ + om = self.om + # create a notebook + cells = [] + code = "print 'hello'" + cells.append(v4.new_code_cell(source=code)) + notebook = v4.new_notebook(cells=cells) + # put the notebook + meta = om.jobs.put(notebook, 'testjob') + self.assertEqual(meta.name, 'testjob.ipynb') + # read it back and see what's in it + notebook2 = om.jobs.get('testjob') + self.assertDictEqual(notebook2, notebook) + + def test_job_list(self): + """ + test job listing + """ + fs = self.fs + om = self.om + # create a notebook + cells = [] + code = "print 'hello'" + cells.append(v4.new_code_cell(source=code)) + notebook = v4.new_notebook(cells=cells) + # put the notebook + meta = om.jobs.put(notebook, 'testjob') + self.assertEqual(meta.name, 'testjob.ipynb') + nb = v4.new_notebook(cells=cells) + job_list = self.om.jobs.list() + expected = 'testjob.ipynb' + self.assertIn(expected, job_list) + + def test_run_job_valid(self): + """ + test running a valid job + """ + om = self.om + # create a notebook + cells = [] + code = "print('hello')" + cells.append(v4.new_code_cell(source=code)) + notebook = v4.new_notebook(cells=cells) + # put the notebook + meta = om.jobs.put(notebook, 'testjob') + self.assertEqual(meta.name, 'testjob.ipynb') + meta_job = om.jobs.run('testjob') + self.assertIn('job_results', meta_job.attributes) + self.assertIn('job_runs', meta_job.attributes) + runs = meta_job.attributes['job_runs'] + results = meta_job.attributes['job_results'] + self.assertEqual(len(runs), 1) + self.assertEqual(len(results), 1) + resultnb = results[0] + self.assertTrue(om.jobs.exists(resultnb)) + self.assertIn(list(runs.keys())[0], resultnb) + + def test_run_job_invalid(self): + """ + test running an invalid job + """ + fs = self.fs + om = self.om + # create a notebook + cells = [] + code = "INVALID PYTHON CODE" + cells.append(v4.new_code_cell(source=code)) + notebook = v4.new_notebook(cells=cells) + # put the notebook + meta = om.jobs.put(notebook, 'testjob') + self.assertEqual(meta.name, 'testjob.ipynb') + nb = v4.new_notebook(cells=cells) + meta_job = om.jobs.run('testjob') + runs = meta_job.attributes['job_runs'] + self.assertEqual(len(runs), 1) + self.assertIn('An error occurred', list(runs.values())[0]) + + def test_run_nonexistent_job(self): + om = self.om + self.assertRaises( + gridfs.errors.NoFile, om.jobs.run_notebook, 'dummys.ipynb') diff --git a/omegaml/tests/test_mdataframe.py b/omegaml/tests/test_mdataframe.py new file mode 100644 index 00000000..eea90976 --- /dev/null +++ b/omegaml/tests/test_mdataframe.py @@ -0,0 +1,451 @@ +from __future__ import absolute_import + +import os +import random +import string +import unittest +from unittest.case import TestCase, skip + +import numpy as np +import pandas as pd +from omegaml import Omega +from omegaml.mdataframe import MDataFrame +from omegaml.util import flatten_columns +from pandas.util.testing import assert_frame_equal, assert_series_equal +from six.moves import range + +class MDataFrameTests(TestCase): + + def setUp(self): + TestCase.setUp(self) + df = self.df = pd.DataFrame({'x': list(range(0, 10)) + list(range(0, 10)), + 'y': random.sample(list(range(0, 100)), 20)}) + om = self.om = Omega() + om.datasets.put(df, 'sample', append=False) + self.coll = om.datasets.collection('sample') + + def tearDown(self): + TestCase.tearDown(self) + + def test_groupby(self): + coll = self.coll + df = self.df + keys = [] + for key, groupdf in MDataFrame(coll).groupby(['x']): + x = key.get('x') + keys.append(x) + subdf = df[df.x == x] + assert_frame_equal(subdf, groupdf.value) + self.assertEqual(set(keys), set(df.x)) + + def test_count(self): + coll = self.coll + df = self.df + counts = MDataFrame(coll).groupby(['x']).count() + test_counts = df.groupby('x').count() + self.assertTrue(test_counts.equals(counts)) + + def test_count_multi_columns(self): + coll = self.coll + df = self.df + # add a column + mdf = MDataFrame(coll) + mdf['z'] = 5 + df['z'] = 5 + # group by and count + counts = mdf.groupby(['x']).count() + test_counts = df.groupby('x').count() + self.assertTrue(test_counts.equals(counts)) + + def test_count_column(self): + coll = self.coll + df = self.df + result = MDataFrame(coll).groupby(['x']).x.count() + testgroup = df.groupby('x').x.count() + self.assertTrue(result.equals(testgroup)) + + def test_aggregate(self): + coll = self.coll + df = self.df + result = MDataFrame(coll).groupby(['x']).agg({'x': 'sum'}) + testagg = df.groupby('x').agg({'x': 'sum'}) + testagg = testagg.rename(columns=dict(x='x_sum')) + self.assertTrue(result.equals(testagg)) + + def test_aggregate_multi_stats(self): + coll = self.coll + df = self.df + stats = {'x': ['sum', 'mean', 'max', 'min', 'std']} + result = MDataFrame(coll).groupby(['x']).agg(stats) + testagg = df.groupby('x').agg(stats) + testagg.columns = testagg.columns.map(flatten_columns) + testagg = testagg[result.columns] + assert_frame_equal(testagg, result, check_dtype=False) + + def test_mdataframe(self): + coll = self.coll + df = self.df + mdf = MDataFrame(coll) + result = mdf.value + self.assertEqual(set(MDataFrame(coll).columns), + set(list(df.columns))) + self.assertTrue(result.equals(df)) + self.assertEqual(mdf.shape, df.shape) + + def test_mdataframe_count(self): + coll = self.coll + df = self.df + mdf = MDataFrame(coll) + assert_series_equal(df.count(), mdf.count()) + self.assertEqual(len(mdf), len(mdf)) + + def test_mdataframe_xlarge(self): + df = pd.DataFrame({ + 'a': list(range(0, int(1e4 + 1))), + 'b': list(range(0, int(1e4 + 1))) + }) + store = self.om.datasets + store.put(df, 'mydata-xlarge', append=False) + coll = store.collection('mydata-xlarge') + result = MDataFrame(coll).value + self.assertEqual(set(MDataFrame(coll).columns), + set(list(df.columns))) + self.assertTrue(result.equals(df)) + + def test_mdataframe_column_attribute(self): + coll = self.coll + df = self.df + result = MDataFrame(coll).y.value + self.assertTrue(df.y.equals(result)) + + def test_mdataframe_column_slice(self): + coll = self.coll + df = self.df + result = MDataFrame(coll)['y'].value + self.assertTrue(df['y'].equals(result)) + + def test_mdataframe_columns_slice(self): + coll = self.coll + df = self.df + result = MDataFrame(coll)[['x', 'y']].value + self.assertTrue(df[['x', 'y']].equals(result)) + + def test_mdataframe_sort(self): + coll = self.coll + df = self.df + result = MDataFrame(coll).sort(['-x', '-y']).value + df = df.sort_values( + ['x', 'y'], ascending=[False, False]) + assert_frame_equal(df, result) + + def test_mdataframe_merge(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(0, 20)), + 'y': list(range(0, 20)), + 'z': list(range(0, 20))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='left').value + testdf = df.merge(other, on='x', how='left') + self.assertTrue(result.equals(testdf)) + + def test_mdataframe_merge_differing_columns(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'s': list(range(0, 20)), + 'y': list(range(0, 20)), + 'z': list(range(0, 20))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, left_on='x', + right_on='s', how='left').value + testdf = df.merge(other, left_on='x', right_on='s', how='left') + testdf = testdf[result.columns] + self.assertTrue(result.equals(testdf)) + + def test_mdataframe_merge_nomatch(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(50, 55)), + 'y': list(range(0, 5)), + 'z': list(range(0, 5))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='left').value + testdf = df.merge(other, on='x', how='left') + testdf = testdf[result.columns] + self.assertTrue(result.equals(testdf)) + + @unittest.skip("disalbed because MDataFrame.append fails and is very slow") + def test_mdataframe_merge_append(self): + ## FIXME this does not work + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(0, 5)), + 'y': list(range(0, 5)), + 'z': list(range(0, 5))}) + om.datasets.put(other, 'samplez', append=False) + mdf = om.datasets.getl('samplez') + mdf.append(mdf) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='left', suffixes=('','')).value + testdf = df.append(other, ignore_index=True) + testdf = testdf[result.columns] + assert_frame_equal(result, testdf) + + def test_mdataframe_merge_partial_match(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(0, 5)), + 'y': list(range(0, 5)), + 'z': list(range(0, 5))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='left').value + testdf = df.merge(other, on='x', how='left') + testdf = testdf[result.columns] + self.assertTrue(result.equals(testdf)) + + def test_mdataframe_merge_inner(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(0, 5)), + 'y': list(range(0, 5)), + 'z': list(range(0, 5))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='inner', + sort=True).value + testdf = df.merge(other, on='x', how='inner', sort=True) + testdf = testdf[result.columns] + self.assertTrue(result.equals(testdf)) + + def test_mdataframe_merge_right(self): + coll = self.coll + df = self.df + om = self.om + other = pd.DataFrame({'x': list(range(0, 5)), + 'y': list(range(0, 5)), + 'z': list(range(0, 5))}) + om.datasets.put(other, 'samplez', append=False) + coll2 = om.datasets.collection('samplez') + result = MDataFrame(coll).merge(coll2, on='x', how='left', + sort=True).value + testdf = df.merge(other, on='x', how='left', sort=True) + testdf = testdf[result.columns] + self.assertTrue(result.equals(testdf)) + + def test_verylarge_dataframe(self): + if not os.environ.get('TEST_LARGE'): + return + other = pd.DataFrame({'x': list(range(0, int(10e6))), + 'y': list(range(0, int(10e6))), + 'z': list(range(0, int(10e6)))}) + coll = self.coll + df = self.df + result = MDataFrame(coll).value + self.assertEqual(set(MDataFrame(coll).columns), + set(list(df.columns))) + self.assertTrue(result.equals(df)) + + def test_unique_series(self): + coll = self.coll + df = self.df + om = self.om + om.datasets.put(df, 'uniques', append=False) + coll = om.datasets.collection('uniques') + result = MDataFrame(coll).x.unique().value + self.assertListEqual(list(result), list(df.x.unique())) + + def test_query_null(self): + om = self.om + df = pd.DataFrame({'x': list(range(0, 5)), + 'y': [1, 2, 3, None, None]}) + om.datasets.put(df, 'foox', append=False) + result = om.datasets.get('foox', y__isnull=True, lazy=True).value + test = df[df.isnull().any(axis=1)] + assert_frame_equal(result, test) + + def test_locindexer_numeric_index(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + om.datasets.put(df, 'foo', append=False) + # by label + dfx = om.datasets.getl('foo').loc[4].value + assert_series_equal(df.loc[4], dfx) + # by slice + dfx = om.datasets.getl('foo').loc[2:4].value + assert_frame_equal(df.loc[2:4], dfx) + # by list + dfx = om.datasets.getl('foo').loc[[2, 4]].value + assert_frame_equal(df.loc[[2, 4]], dfx) + # by ndarray + sel = np.array([1, 2]) + dfx = om.datasets.getl('foo').loc[sel, :].value + assert_frame_equal(df.loc[sel, :], dfx, check_names=False) + + def test_locindexer_character_index(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + idx = string.ascii_lowercase[0:9] + df = pd.DataFrame(data, index=(c for c in idx)) + om.datasets.put(df, 'foo', append=False) + # by label + dfx = om.datasets.getl('foo').loc['c'].value + assert_series_equal(df.loc['c'], dfx) + # by slice + dfx = om.datasets.getl('foo').loc['c':'f'].value + assert_frame_equal(df.loc['c':'f'], dfx) + # by list + dfx = om.datasets.getl('foo').loc[['c', 'f']].value + assert_frame_equal(df.loc[['c', 'f']], dfx) + + def test_locindexer_timeseries_index(self): + om = self.om + # create some dataframe + tsidx = pd.date_range(pd.datetime(2016, 1, 1), pd.datetime(2016, 4, 1)) + df = pd.DataFrame({ + 'a': list(range(0, len(tsidx))), + 'b': list(range(0, len(tsidx))) + }, index=tsidx) + om.datasets.put(df, 'foo', append=False) + # by label + dfx = om.datasets.getl('foo').loc[pd.datetime(2016, 2, 3)].value + assert_series_equal(dfx, df.loc[pd.datetime(2016, 2, 3)]) + # by slice + start, end = pd.datetime(2016, 2, 3), pd.datetime(2016, 2, 8) + dfx = om.datasets.getl('foo').loc[start:end].value + assert_frame_equal(df.loc[start:end], dfx) + + def test_locindexer_multiindex(self): + # create some dataframe + om = self.om + midx = pd.MultiIndex(levels=[[u'bar', u'baz', u'foo', u'qux'], + [u'one', u'two']], + labels=[ + [0, 0, 1, 1, 2, 2, 3, 3], + [0, 1, 0, 1, 0, 1, 0, 1]], + names=[u'first', u'second']) + df = pd.DataFrame({'x': range(0, len(midx))}, index=midx) + om.datasets.put(df, 'foomidx', append=False) + dfx = om.datasets.getl('foomidx').loc['bar', 'one'].value + assert_series_equal(dfx, df.loc['bar', 'one']) + + def test_locindexer_series(self): + """ test storing a pandas series with it's own index """ + om = self.om + series = pd.Series(range(10), + name='foo', + index=pd.date_range(pd.datetime(2016, 1, 1), + pd.datetime(2016, 1, 10))) + om.datasets.put(series, 'fooseries', append=False) + # try data range + daterange = slice(pd.datetime(2016, 1, 5), pd.datetime(2016, 1, 10)) + series2 = om.datasets.getl('fooseries').loc[daterange].value + assert_series_equal(series2, series.loc[daterange]) + # try single date + daterange = pd.datetime(2016, 1, 5) + series2 = om.datasets.getl('fooseries').loc[daterange].value + self.assertEqual(series2, series.loc[daterange]) + + def test_ilocindexer(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + idx = string.ascii_lowercase[0:9] + df = pd.DataFrame(data, index=(c for c in idx)) + om.datasets.put(df, 'foo', append=False) + # by single location + dfx = om.datasets.getl('foo').iloc[0].value + assert_series_equal(df.iloc[0], dfx) + # by slice + dfx = om.datasets.getl('foo').iloc[0:1].value + assert_frame_equal(df.iloc[0:1], dfx) + # by list + dfx = om.datasets.getl('foo').iloc[[1, 2]].value + assert_frame_equal(df.iloc[[1, 2]], dfx) + + + def test_ilocindexer_single_column(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + idx = string.ascii_lowercase[0:9] + df = pd.DataFrame(data, index=(c for c in idx)) + om.datasets.put(df, 'foo', append=False) + # by single location + dfx = om.datasets.getl('foo').iloc[0, 1].value + self.assertEqual(df.iloc[0, 1], dfx) + # by slice + # FIXME column access by iloc is not guaranteed to return in order + dfx = om.datasets.getl('foo').iloc[0:2, 1].value + assert_series_equal(df.iloc[0:2, 1], dfx, check_names=False) + # by list + dfx = om.datasets.getl('foo').iloc[[1, 2], 1].value + assert_series_equal(df.iloc[[1, 2], 1], dfx, check_names=False) + + def test_ilocindexer_columns(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + idx = string.ascii_lowercase[0:9] + df = pd.DataFrame(data, index=(c for c in idx)) + om.datasets.put(df, 'foo', append=False) + # by single location + dfx = om.datasets.getl('foo').iloc[0, :].value + assert_series_equal(df.iloc[0, :], dfx) + # by slice + # FIXME column access by iloc is not guaranteed to return in order + dfx = om.datasets.getl('foo').iloc[0:2, :].value + assert_frame_equal(df.iloc[0:2, :], dfx, check_names=False) + # by list + dfx = om.datasets.getl('foo').iloc[[1, 2], :].value + assert_frame_equal(df.iloc[[1, 2], :], dfx, check_names=False) + # by ndarray + sel = np.array([1,2]) + dfx = om.datasets.getl('foo').iloc[sel, :].value + assert_frame_equal(df.iloc[sel, :], dfx, check_names=False) + + def test_ilocindexer_array(self): + om = self.om + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + idx = string.ascii_lowercase[0:9] + df = pd.DataFrame(data, index=(c for c in idx)) + om.datasets.put(df, 'foo', append=False) + # by ndarray with immediate loc + sel = np.array([1,2]) + dfx = om.datasets.getl('foo') + dfx.immediate_loc = True + dfx = dfx[['a']].iloc[sel] + assert_frame_equal(df[['a']].iloc[sel], dfx, check_names=False) + # by ndarray with delayed loc + sel = np.array([1, 2]) + dfx = om.datasets.getl('foo') + dfx.immediate_loc = False + dfx = dfx[['a']].iloc[sel].value + assert_frame_equal(df[['a']].iloc[sel], dfx, check_names=False) + + diff --git a/omegaml/tests/test_mdfmixins.py b/omegaml/tests/test_mdfmixins.py new file mode 100644 index 00000000..554da069 --- /dev/null +++ b/omegaml/tests/test_mdfmixins.py @@ -0,0 +1,508 @@ +import random +from unittest import TestCase + +from omegaml import Omega +import pandas as pd +from omegaml.store import Filter +from pandas.util.testing import assert_frame_equal, assert_series_equal + + +class MDataFrameMixinTests(TestCase): + def setUp(self): + TestCase.setUp(self) + df = self.df = pd.DataFrame({'x': list(range(0, 10)) + list(range(0, 10)), + 'y': list(range(0, 10)) + list(range(0, 10))}) + om = self.om = Omega() + om.datasets.put(df, 'sample', append=False) + self.coll = om.datasets.collection('sample') + + def test_filterops_frame(self): + om = self.om + df = self.df + mdf = om.datasets.getl('sample') + # equality selection on frame + flt_mdf = mdf == 5 + # note MDataFrame does not actually return a mask but applies the filter + # to the real data. makes more sense even though the results + # are somewhat different this way, but match better semantically + flt_df = df == 5 # this returns a multi-column mask with all rows(!) + expected = df[flt_df.iloc[:, 0]] # actual meaning in in MDataFrame + assert_frame_equal(mdf[flt_mdf].value, expected) + ## unequality selection + flt_mdf = mdf < 5 + flt_df = df < 5 # this returns a multi-column mask with all rows(!) + expected = df[flt_df.iloc[:, 0]] # actual meaning in in MDataFrame + assert_frame_equal(mdf[flt_mdf].value, expected) + # combined + flt_mdf = mdf < 5 + flt_df = df < 5 # this returns a multi-column mask with all rows(!) + expected = df[flt_df.iloc[:, 0]] # actual meaning in in MDataFrame + assert_frame_equal(mdf[flt_mdf].value, expected) + + def test_filterops_series(self): + om = self.om + df = self.df + mdf = om.datasets.getl('sample') + # equality selection on frame + flt_mdf = mdf['x'] == 5 + # note MDataFrame does not actually return a mask but applies the filter + # to the real data. makes more sense even though the results + # are somewhat different this way, but match better semantically + flt_df = df['x'] == 5 # this returns a multi-column mask with all rows(!) + expected = df['x'][flt_df] # actual meaning in in MDataFrame + assert_series_equal(mdf['x'][flt_mdf].value, expected) + ## unequality selection + flt_mdf = mdf < 5 + flt_df = df['x'] < 5 + expected = df['x'][flt_df] + assert_series_equal(mdf['x'][flt_mdf].value, expected) + # combined + flt_mdf = mdf['x'] < 5 + flt_df = df['x'] < 5 + expected = df['x'][flt_df] # actual meaning in in MDataFrame + assert_series_equal(mdf['x'][flt_mdf].value, expected) + + def test_applyoperators(self): + om = self.om + df = self.df + # mult + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v * 5) + expected = df * 5 + assert_frame_equal(expected, mdf.value) + # add + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v + 5) + expected = df + 5 + assert_frame_equal(expected, mdf.value) + # div + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v / 2) + expected = df / 2 + assert_frame_equal(expected, mdf.value) + # truediv + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v // 2) + expected = df // 2 + assert_frame_equal(expected.astype(float), mdf.value) + # minus + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v - 2) + expected = df - 2 + assert_frame_equal(expected, mdf.value) + # complex + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: (v - 2) * 5) + expected = (df - 2) * 5 + assert_frame_equal(expected, mdf.value) + + def test_apply_custom_project(self): + om = self.om + df = self.df + + def complexfn(ctx): + ctx.add({ + '$project': { + 'x': { + '$multiply': ['$x', 5] + }, + 'y': 1, + } + }) + return ctx + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(complexfn) + df['x'] = df['x'] * 5 + expected = df + assert_frame_equal(expected, mdf.value) + + def test_apply_custom_project_simple(self): + om = self.om + df = self.df + + def complexfn(ctx): + ctx.project(x={ + '$multiply': ['$x', 5] + }, b={'$divide': ['$x', 5]}) + return ctx + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(complexfn) + # simulate parallel ops + df['x'], df['b'] = df['x'] * 5, df['x'] / 5 + value = mdf.value + expected = df[value.columns] + self.assertEquals(sorted(['x', 'b']), sorted(expected)) + assert_frame_equal(expected, value) + + def test_apply_dict(self): + om = self.om + + df = pd.DataFrame({ + 'x': pd.date_range(start=pd.to_datetime('09.05.2018', + format='%m.%d.%Y'), periods=5, tz=None), + 'y': range(5), + }) + om.datasets.put(df, 'sample', append=False) + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: dict(a=v['x'].dt.dayofweek, + b=v['x'].dt.year)).value + + self.assertEqual(list(mdf['a'].values), [4, 5, 6, 7, 1]) + self.assertTrue(all(v == 2018 for v in mdf['b'].values)) + + def test_apply_dt(self): + om = self.om + + df = pd.DataFrame({ + 'x': pd.date_range(start='now', periods=5, tz=None), + 'y': range(5), + }) + om.datasets.put(df, 'sample', append=False) + + mdf = om.datasets.getl('sample') + # FIXME we have to convert MongoDB dayofweek to Pandas dt.dayofweek + # they start at different days of the week with different base index + value = mdf.apply(lambda v: ((v['x'].dt.dayofweek) + 5) % 7).value + expected = df['x'].dt.dayofweek + assert_series_equal(expected, value['x']) + + def test_apply_str_concat(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xyz', 'nop'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.concat(['*', 'b'])).value + self.assertEqual(list(mdf['x'].values), ['abc*xyz', 'def*nop']) + self.assertEqual(list(mdf['b'].values), ['xyz*xyz', 'nop*nop']) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.concat(['foo'])).value + self.assertEqual(list(mdf.values), ['abcfoo', 'deffoo']) + + def test_apply_str_split(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xyz', 'nop'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.split('a')).value + self.assertEqual(list(mdf['x'].values), ['', 'bc', 'def']) + self.assertEqual(list(mdf['b'].values), ['xyz', 'xyz', 'nop']) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.split(['a'])).value + self.assertEqual(mdf.name, 'x') + self.assertEqual(list(mdf.values), ['', 'bc', 'def']) + + def test_apply_str_lower(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['ABC', 'DEF'], + 'b': ['XYZ', 'NOP'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.lower()).value + self.assertEqual(list(mdf['x'].values), ['abc', 'def']) + self.assertEqual(list(mdf['b'].values), ['xyz', 'nop']) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.lower()).value + self.assertEqual(list(mdf.values), ['abc', 'def']) + self.assertEqual(mdf.name, 'x') + mdf = om.datasets.getl('sample') + mdf = mdf['b'].apply(lambda v: v.str.lower()).value + self.assertEqual(list(mdf.values), ['xyz', 'nop']) + self.assertEqual(mdf.name, 'b') + + def test_apply_str_substr(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xyz', 'nop'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.substr(0, 2)).value + self.assertEqual(list(mdf['x'].values), ['ab', 'de']) + self.assertEqual(list(mdf['b'].values), ['xy', 'no']) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.substr(0, 2)).value + self.assertEqual(list(mdf.values), ['ab', 'de']) + self.assertEqual(mdf.name, 'x') + mdf = om.datasets.getl('sample') + mdf = mdf['b'].apply(lambda v: v.str.substr(0, 2)).value + self.assertEqual(list(mdf.values), ['xy', 'no']) + self.assertEqual(mdf.name, 'b') + + def test_apply_str_isequal(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xyz', 'nop'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.isequal('abc')).value + self.assertEqual(list(mdf['x'].values), [True, False]) + self.assertEqual(list(mdf['b'].values), [False, False]) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.isequal('abc')).value + self.assertEqual(list(mdf.values), [True, False]) + self.assertEqual(mdf.name, 'x') + mdf = om.datasets.getl('sample') + mdf = mdf['b'].apply(lambda v: v.str.isequal('abc')).value + self.assertEqual(list(mdf.values), [False, False]) + self.assertEqual(mdf.name, 'b') + + def test_apply_str_len(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xxyz', 'noppp'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.len()).value + self.assertEqual(list(mdf['x'].values), [3, 3]) + self.assertEqual(list(mdf['b'].values), [4, 5]) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.len()).value + self.assertEqual(list(mdf.values), [3, 3]) + self.assertEqual(mdf.name, 'x') + mdf = om.datasets.getl('sample') + mdf = mdf['b'].apply(lambda v: v.str.len()).value + self.assertEqual(list(mdf.values), [4, 5]) + self.assertEqual(mdf.name, 'b') + + def test_apply_str_index(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'], + 'b': ['xxbc', 'bcxz'], + }) + om.datasets.put(df, 'sample', append=False) + + # test on dataframe + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.str.index('bc')).value + self.assertEqual(list(mdf['x'].values), [1, -1]) + self.assertEqual(list(mdf['b'].values), [2, 0]) + # test on series + mdf = om.datasets.getl('sample') + mdf = mdf['x'].apply(lambda v: v.str.index('bc')).value + self.assertEqual(list(mdf.values), [1, -1]) + self.assertEqual(mdf.name, 'x') + mdf = om.datasets.getl('sample') + mdf = mdf['b'].apply(lambda v: v.str.index('bc')).value + self.assertEqual(list(mdf.values), [2, 0]) + self.assertEqual(mdf.name, 'b') + + def test_apply_groupby_math(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(7, 17), + }) + + om.datasets.put(df, 'sample', append=False) + + def groupby(ctx): + ctx.groupby('x', v={'$sum': '$v'})['v'] * 2 + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(groupby) + expected = df.groupby('x').agg(dict(v='sum')) * 2 + assert_frame_equal(expected, mdf.value) + + def test_apply_groupby_inline(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(7, 17), + }) + + om.datasets.put(df, 'sample', append=False) + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.groupby('x').sum('v')) + expected = df.groupby('x').agg(dict(v='sum')).rename(columns=dict(v='v_sum')) + assert_frame_equal(expected, mdf.value) + + def test_apply_groupby_agg(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(7, 17), + }) + + om.datasets.put(df, 'sample', append=False) + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).value + expected = df.groupby('x').agg(dict(v=['sum', 'mean', 'std'])) + self.assertEqual(list(expected[('v', 'sum')].values), list(mdf['v_sum'].values)) + + def test_apply_groupby_agg(self): + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(7, 17), + }) + om.datasets.put(df, 'sample', append=False) + + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).value + expected = df.groupby('x').agg(dict(v=['sum', 'mean', 'std'])) + self.assertEqual(list(expected[('v', 'sum')].values), list(mdf['v_sum'].values)) + self.assertEqual(list(expected[('v', 'mean')].values), list(mdf['v_avg'].values)) + self.assertEqual(list(expected[('v', 'std')].values), list(mdf['v_std'].values)) + + def test_apply_cache(self): + """ + test apply cache + + note this tests the functionality of the cache, not the performance + """ + om = self.om + + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(7, 17), + }) + om.datasets.put(df, 'sample', append=False) + + # cache a groupby, check the cache got created + mdf = om.datasets.getl('sample') + cache_key = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).reset_cache().persist() + cursor = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std']))._get_cached_cursor() + expected = df.groupby('x').agg(dict(v=['sum', 'mean', 'std'])) + self.assertIsNotNone(cache_key) + self.assertIsNotNone(cursor) + # replace the data to supersede the cache, without resetting the cache + df = pd.DataFrame({ + 'x': ['abc', 'def'] * 5, + 'v': range(17, 27), + }) + df['v'] = df['v'] / 2 + om.datasets.put(df, 'sample', append=False) + # evaluate the groupby, expected results are taken from cache since we did not reset the cache + mdf = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).value + self.assertEqual(list(expected[('v', 'sum')].values), list(mdf['v_sum'].values)) + self.assertEqual(list(expected[('v', 'mean')].values), list(mdf['v_avg'].values)) + self.assertEqual(list(expected[('v', 'std')].values), list(mdf['v_std'].values)) + # finally reset cache + mdf = om.datasets.getl('sample') + mdf = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).reset_cache() + # check reset was successful + cursor = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std']))._get_cached_cursor() + mdf = mdf.value + self.assertIsNone(cursor) + self.assertNotEqual(list(expected[('v', 'sum')].values), list(mdf['v_sum'].values)) + self.assertNotEqual(list(expected[('v', 'mean')].values), list(mdf['v_avg'].values)) + self.assertNotEqual(list(expected[('v', 'std')].values), list(mdf['v_std'].values)) + # try again with a full cache reset + mdf = om.datasets.getl('sample') + mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std'])).persist() + cursor = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std']))._get_cached_cursor() + self.assertIsNotNone(cursor) + # make sure we get a fresh mdf without any apply groups so we can test cache reset in full on collection + mdf = om.datasets.getl('sample') + mdf.reset_cache(full=True) + cursor = mdf.apply(lambda v: v.groupby('x').agg(v=['sum', 'mean', 'std']))._get_cached_cursor() + self.assertIsNone(cursor) + + def test_apply_quantile(self): + """ + test covariance + """ + om = self.om + df = pd.DataFrame({ + 'x': range(1000), + 'y': range(1000), + }) + om.datasets.put(df, 'qtest', append=False) + mdf = om.datasets.getl('qtest') + result = mdf.quantile([.1, .2]).value + # FIXME this is actually wrong, see df.quantile([.1, .2]) + self.assertListEqual(list(result.loc['p0.1'].values), [100, 100]) + self.assertListEqual(list(result.loc['p0.2'].values), [200, 200]) + + def test_apply_covariance(self): + """ + test covariance + """ + om = self.om + df = pd.DataFrame({ + 'x': range(10), + 'y': range(10, 20), + }) + om.datasets.put(df, 'covtest', append=False) + expected = df.cov() + mdf = om.datasets.getl('covtest') + result = mdf.cov().value + assert_frame_equal(result, expected) + + def test_apply_correlation(self): + """ + test covariance + """ + om = self.om + df = pd.DataFrame({ + 'x': range(10), + 'y': range(10, 20), + }) + om.datasets.put(df, 'corrtest', append=False) + expected = df.corr() + mdf = om.datasets.getl('corrtest') + result = mdf.corr().value + assert_frame_equal(result, expected) + + + + + + + + + + + + + + + diff --git a/omegaml/tests/test_query.py b/omegaml/tests/test_query.py new file mode 100644 index 00000000..f69be436 --- /dev/null +++ b/omegaml/tests/test_query.py @@ -0,0 +1,105 @@ +from __future__ import absolute_import +import random +from unittest.case import TestCase + +from omegaml import Omega +from omegaml.mdataframe import MDataFrame +from omegaml.store import qops +from omegaml.store.query import Filter +from omegaml.store.queryops import GeoJSON +import pandas as pd +from six.moves import range +from pandas.util.testing import assert_frame_equal +# see https://gist.github.com/miraculixx/f01304186fc47d041da5a712774ac487 +locations = [{'location': {'coordinates': [-74.0059413, 40.7127837], + 'type': 'Point'}, + 'place': 'New York'}, + {'location': {'coordinates': [6.1431577, 46.2043907], + 'type': 'Point'}, + 'place': 'Geneva'}, + {'location': {'coordinates': [7.4474468, 46.9479739], + 'type': 'Point'}, + 'place': 'Bern'}, + {'location': {'coordinates': [8.541694, 47.3768866], + 'type': 'Point'}, + 'place': 'Zurich'}] + + +class FilterQueryTests(TestCase): + + def setUp(self): + TestCase.setUp(self) + df = self.df = pd.DataFrame({'x': list(range(0, 10)) + list(range(0, 10)), + 'y': random.sample(list(range(0, 100)), 20)}) + om = self.om = Omega() + om.datasets.put(df, 'sample', append=False) + self.coll = om.datasets.collection('sample') + + def tearDown(self): + TestCase.tearDown(self) + + def test_filter(self): + coll = self.coll + df = self.df + result = Filter(coll, x=0).value + testdf = df[df.x == 0] + self.assertTrue(result.equals(testdf)) + + def test_filter_and(self): + coll = self.coll + df = self.df + y = int(df.y.unique()[0]) + result = Filter(coll, x=0, y=y).value + testdf = df[(df.x == 0) & (df.y == y)] + self.assertTrue(result.equals(testdf)) + + def test_filter_or(self): + coll = self.coll + df = self.df + result = Filter(coll, x=0, y__gt=5).value + testdf = df[(df.x == 0) & (df.y > 5)] + self.assertTrue(result.equals(testdf)) + + def test_filter_near(self): + om = self.om + # create a dataframe with geo locations + geodf = pd.DataFrame(locations) + geodf['location'] = geodf.location.apply(lambda v: GeoJSON(v)) + om.datasets.put(geodf, 'geosample', append=False, index='@location') + coll = om.datasets.collection('geosample') + # closest place + result = Filter(coll, + location__near=dict(location=(8.541694, 47.3768866), maxd=1)) + places = result.value.place.unique() + self.assertEqual(places, ['Zurich']) + # ordered by distance + result = Filter(coll, + location__near=dict(location=(8.541694, 47.3768866))) + places = list(result.value.place.unique()) + self.assertListEqual(places, 'Zurich,Bern,Geneva,New York'.split(',')) + # use tuple (lon, lat, maxd) + result = Filter(coll, + location__near=(8.541694, 47.3768866, 1)) + places = list(result.value.place.unique()) + self.assertListEqual(places, 'Zurich'.split(',')) + # use tuple (lon, lat, mind, maxd) + result = Filter(coll, + location__near=(8.541694, 47.3768866, 0, 100)) + places = list(result.value.place.unique()) + self.assertListEqual(places, 'Zurich'.split(',')) + + def test_filter_subdoc(self): + coll = self.coll + coll.update_many(qops.IS(x=qops.LT(5)), qops.SET('subdoc.a', 99)) + coll.update_many(qops.IS(x=qops.GTE(5)), qops.SET('subdoc.a', 0)) + result = Filter(coll, subdoc__a__lt=10).value + self.assertEqual(set(result.x.unique()), set(range(5, 10))) + + def test_query_null(self): + om = self.om + df = pd.DataFrame({'x': list(range(0, 5)), + 'y': [1, 2, 3, None, None]}) + om.datasets.put(df, 'foox', append=False) + result = om.datasets.get('foox', y__isnull=True) + test = df[df.isnull().any(axis=1)] + assert_frame_equal(result, test) diff --git a/omegaml/tests/test_runtime.py b/omegaml/tests/test_runtime.py new file mode 100644 index 00000000..12efcdd3 --- /dev/null +++ b/omegaml/tests/test_runtime.py @@ -0,0 +1,325 @@ +from __future__ import absolute_import + +import os +from unittest import TestCase, skipUnless + +from sklearn.datasets import make_classification +from sklearn.exceptions import NotFittedError +from sklearn.linear_model import LogisticRegression +from sklearn.linear_model.base import LinearRegression +from sklearn.linear_model.stochastic_gradient import SGDRegressor +from sklearn.metrics.regression import mean_squared_error +from sklearn.model_selection import GridSearchCV +from sklearn.pipeline import Pipeline +from sklearn.utils.validation import DataConversionWarning + +import numpy as np +from omegaml import Omega +from omegaml.util import delete_database, reshaped, settings +import pandas as pd +from six.moves import range + + +class RuntimeTests(TestCase): + + def setUp(self): + TestCase.setUp(self) + delete_database() + + def tearDown(self): + TestCase.tearDown(self) + + def test_predict(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df[['x']] + Y = df[['y']] + # put into Omega + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + om.datasets.get('datax') + om.datasets.get('datay') + # create a model locally, fit it, store in Omega + lr = LinearRegression() + lr.fit(X, Y) + pred = lr.predict(X) + om.models.put(lr, 'mymodel') + self.assertIn('mymodel', om.models.list('*')) + # have Omega predict it + # -- using data already in Omega + result = om.runtime.model('mymodel').predict('datax') + pred1 = result.get() + # -- using data provided locally + # note this is the same as + # om.datasets.put(X, 'foo') + # om.runtimes.model('mymodel').predict('foo') + result = om.runtime.model('mymodel').predict(X) + pred2 = result.get() + self.assertTrue( + (pred == pred1).all(), "runtimes prediction is different(1)") + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(2)") + + def test_fit(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df[['x']] + Y = df[['y']] + # put into Omega + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + om.datasets.get('datax') + om.datasets.get('datay') + # create a model locally, store (unfitted) in Omega + lr = LinearRegression() + om.models.put(lr, 'mymodel2') + self.assertIn('mymodel2', om.models.list('*')) + # predict locally for comparison + lr.fit(X, Y) + pred = lr.predict(X) + # try predicting without fitting + with self.assertRaises(NotFittedError): + result = om.runtime.model('mymodel2').predict('datax') + result.get() + # have Omega fit the model then predict + result = om.runtime.model('mymodel2').fit('datax', 'datay') + result.get() + # check the new model version metadata includes the datax/y references + meta = om.models.metadata('mymodel2') + self.assertIn('metaX', meta.attributes) + self.assertIn('metaY', meta.attributes) + # -- using data already in Omega + result = om.runtime.model('mymodel2').predict('datax') + pred1 = result.get() + # -- using data provided locally + # note this is the same as + # om.datasets.put(X, 'foo') + # om.runtimes.model('mymodel2').predict('foo') + result = om.runtime.model('mymodel2').fit(X, Y) + result = om.runtime.model('mymodel2').predict(X) + pred2 = result.get() + # -- check the local data provided to fit was stored as intended + meta = om.models.metadata('mymodel2') + self.assertIn('metaX', meta.attributes) + self.assertIn('metaY', meta.attributes) + self.assertIn('_fitX', meta.attributes.get('metaX').get('collection')) + self.assertIn('_fitY', meta.attributes.get('metaY').get('collection')) + self.assertTrue( + (pred == pred1).all(), "runtimes prediction is different(1)") + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(2)") + + def test_partial_fit(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df[['x']][0:2] + Y = df[['y']][0:2] + # put into Omega + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(df[['x']], 'datax-full') + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + om.datasets.get('datax') + om.datasets.get('datay') + # create a model locally, store (unfitted) in Omega + # -- ignore warnings on y shape + import warnings + warnings.filterwarnings("ignore", category=DataConversionWarning) + lr = SGDRegressor() + om.models.put(lr, 'mymodel2') + # have Omega fit the model to get a start, then predict + result = om.runtime.model('mymodel2').fit('datax', 'datay') + result.get() + # check the new model version metadata includes the datax/y references + result = om.runtime.model('mymodel2').predict('datax-full') + pred1 = result.get() + mse = mean_squared_error(df.y, pred1) + self.assertGreater(mse, 90) + # fit mini batches add better training data, update model + batch_size = 2 + for i, start in enumerate(range(0, len(df))): + previous_mse = mse + X = df[['x']][start:start + batch_size] + Y = df[['y']][start:start + batch_size] + om.datasets.put(X, 'datax-update', append=False) + om.datasets.put(Y, 'datay-update', append=False) + result = om.runtime.model('mymodel2').partial_fit( + 'datax-update', 'datay-update') + result.get() + # check the new model version metadata includes the datax/y + # references + result = om.runtime.model('mymodel2').predict('datax-full') + pred1 = result.get() + mse = mean_squared_error(df.y, pred1) + self.assertLess(mse, previous_mse) + # mse == 0 is most accurate the best + self.assertLess(mse, 1.0) + + def test_predict_pure_python(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}).astype('O') + X = [[x] for x in list(df.x)] + Y = [[y] for y in list(df.y)] + # put into Omega -- assume a client with pandas, scikit learn + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.pure_python = True + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + Xhat = om.datasets.get('datax') + Yhat = om.datasets.get('datay') + self.assertEqual([X], Xhat) + self.assertEqual([Y], Yhat) + # have Omega fit the model then predict + lr = LinearRegression() + lr.fit(X, Y) + pred = lr.predict(X) + om.models.put(lr, 'mymodel2') + # -- using data provided locally + # note this is the same as + # om.datasets.put(X, 'foo') + # om.runtimes.model('mymodel2').predict('foo') + result = om.runtime.model('mymodel2').predict(reshaped(X)) + pred2 = result.get() + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(1)") + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(2)") + + def test_predict_hdf_dataframe(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df['x'] + Y = df['y'] + # put into Omega -- assume a client with pandas, scikit learn + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.pure_python = True + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax', as_hdf=True) + om.datasets.put(Y, 'datay', as_hdf=True) + # have Omega fit the model then predict + lr = LinearRegression() + lr.fit(reshaped(X), reshaped(Y)) + pred = lr.predict(reshaped(X)) + om.models.put(lr, 'mymodel2') + # -- using data provided locally + # note this is the same as + # om.datasets.put(X, 'foo') + # om.runtimes.model('mymodel2').predict('foo') + result = om.runtime.model('mymodel2').predict('datax') + pred2 = result.get() + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(1)") + self.assertTrue( + (pred == pred2).all(), "runtimes prediction is different(2)") + + def test_fit_pipeline(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df[['x']] + Y = df[['y']] + # put into Omega + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + om.datasets.get('datax') + om.datasets.get('datay') + # create a pipeline locally, store (unfitted) in Omega + p = Pipeline([ + ('lr', LinearRegression()), + ]) + om.models.put(p, 'mymodel2') + self.assertIn('mymodel2', om.models.list('*')) + # predict locally for comparison + p.fit(reshaped(X), reshaped(Y)) + pred = p.predict(reshaped(X)) + # have Omega fit the model then predict + result = om.runtime.model('mymodel2').fit('datax', 'datay') + result.get() + result = om.runtime.model('mymodel2').predict('datax') + pred1 = result.get() + self.assertTrue( + (pred == pred1).all(), "runtimes prediction is different(1)") + + def test_score(self): + # create some data + x = np.array(list(range(0, 10))) + y = x * 2 + df = pd.DataFrame({'x': x, + 'y': y}) + X = df[['x']] + Y = df[['y']] + # put into Omega + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.datasets.put(X, 'datax') + om.datasets.put(Y, 'datay') + om.datasets.get('datax') + om.datasets.get('datay') + # create a model locally, fit it, store in Omega + lr = LinearRegression() + lr.fit(X, Y) + scores = lr.score(X, Y) + om.models.put(lr, 'mymodel') + + def test_gridsearch(self): + X, y = make_classification() + logreg = LogisticRegression() + os.environ['DJANGO_SETTINGS_MODULE'] = '' + om = Omega() + om.runtime.celeryapp.conf.CELERY_ALWAYS_EAGER = True + om.models.put(logreg, 'logreg') + params = { + 'C': [0.1, 0.5, 1.0] + } + # gridsearch on runtimes + om.runtime.model('logreg').gridsearch(X, y, parameters=params) + meta = om.models.metadata('logreg') + # check gridsearch was saved + self.assertIn('gridsearch', meta.attributes) + self.assertEqual(len(meta.attributes['gridsearch']), 1) + self.assertIn('gsModel', meta.attributes['gridsearch'][0]) + # check we can get back the gridsearch model + gs_model = om.models.get(meta.attributes['gridsearch'][0]['gsModel']) + self.assertIsInstance(gs_model, GridSearchCV) + + def test_ping(self): + om = Omega() + result = om.runtime.ping('foo', fox='bar') + self.assertIn('message', result) + self.assertIn('worker', result) + self.assertEqual(result['args'], ('foo',)) + self.assertEqual(result['kwargs'], dict(fox='bar')) + + diff --git a/omegaml/tests/test_store.py b/omegaml/tests/test_store.py new file mode 100644 index 00000000..fa0e5d0d --- /dev/null +++ b/omegaml/tests/test_store.py @@ -0,0 +1,622 @@ +from __future__ import absolute_import + +from datetime import timedelta +import unittest +import uuid +from zipfile import ZipFile + +import gridfs +from mongoengine.connection import disconnect +from mongoengine.errors import DoesNotExist +from pandas.util import testing +from pandas.util.testing import assert_frame_equal, assert_series_equal +from six import StringIO, BytesIO +from sklearn.datasets import load_iris +from sklearn.linear_model import LogisticRegression + +from omegaml import backends +from omegaml.documents import Metadata +from omegaml.store import OmegaStore +from omegaml.util import override_settings, delete_database +import pandas as pd +from six.moves import range + + +class StoreTests(unittest.TestCase): + + def setUp(self): + unittest.TestCase.setUp(self) + delete_database() + + def tearDown(self): + unittest.TestCase.tearDown(self) + delete_database() + disconnect('omega') + + def test_package_model(self): + # create a test model + store = OmegaStore() + iris = load_iris() + X = iris.data + Y = iris.target + lr = LogisticRegression() + lr.fit(X, Y) + result = lr.predict(X) + # package locally + backend = backends.ScikitLearnBackend(model_store=store, + data_store=store) + zipfname = backend._package_model(lr, 'models/foo') + # load it, try predicting + lr2 = backend._extract_model(zipfname) + self.assertIsInstance(lr2, LogisticRegression) + result2 = lr2.predict(X) + self.assertTrue((result == result2).all()) + + def test_put_model(self): + # create a test model + iris = load_iris() + X = iris.data + Y = iris.target + lr = LogisticRegression() + lr.fit(X, Y) + result = lr.predict(X) + # store it remote + store = OmegaStore() + store.put(lr, 'models/foo') + # get it back, try predicting + lr2 = store.get('models/foo') + self.assertIsInstance(lr2, LogisticRegression) + result2 = lr2.predict(X) + self.assertTrue((result == result2).all()) + + def test_prefix_store(self): + """ + this is to test if store prefixes work + """ + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + datasets = OmegaStore(prefix='teststore') + models = OmegaStore(prefix='models', kind=Metadata.SKLEARN_JOBLIB) + datasets.put(df, 'test') + self.assertEqual(len(datasets.list()), 1) + self.assertEqual(len(models.list()), 0) + + def test_custom_levels(self): + """ + this is to test if custom path and levels can be provided ok + """ + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + datasets = OmegaStore(prefix='data') + models = OmegaStore(prefix='models', kind=Metadata.SKLEARN_JOBLIB) + # directory-like levels + datasets.put(df, 'data/is/mypath/test') + datasets.put(df, 'data/is/mypath/test2') + self.assertEqual(len(datasets.list('data/*/mypath/*')), 2) + self.assertEqual(len(datasets.list('data/*/test')), 1) + # namespace-like levels + datasets.put(df, 'my.namespace.module.test') + datasets.put(df, 'my.namespace.module.test2') + self.assertEqual(len(datasets.list('*.module.*')), 2) + self.assertEqual(len(datasets.list('*.module.test2')), 1) + + def test_put_model_with_prefix(self): + # create a test model + iris = load_iris() + X = iris.data + Y = iris.target + lr = LogisticRegression() + lr.fit(X, Y) + result = lr.predict(X) + # store it remote + store = OmegaStore(prefix='models/') + store.put(lr, 'foo') + # get it back, try predicting + lr2 = store.get('foo') + self.assertIsInstance(lr2, LogisticRegression) + result2 = lr2.predict(X) + self.assertTrue((result == result2).all()) + + def test_put_dataframe(self): + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + df2 = store.get('mydata') + self.assertTrue(df.equals(df2), "expected dataframes to be equal") + + def test_put_dataframe_xtra_large(self): + # create some dataframe + # force fast insert + df = pd.DataFrame({ + 'a': list(range(0, int(1e4 + 1))), + 'b': list(range(0, int(1e4 + 1))) + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + df2 = store.get('mydata') + self.assertTrue(df.equals(df2), "expected dataframes to be equal") + + def test_put_dataframe_timestamp(self): + # create some dataframe + from datetime import datetime + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store = OmegaStore(prefix='') + # -- check default timestamp + now = datetime.utcnow() + store.put(df, 'mydata', append=False, timestamp=True) + df2 = store.get('mydata') + _created = df2['_created'].astype(datetime).unique()[0].to_pydatetime() + self.assertEqual(_created.replace(second=0, microsecond=0), + now.replace(second=0, microsecond=0)) + # -- check custom timestamp column, default value + now = datetime.utcnow() + store.put(df, 'mydata', append=False, timestamp='CREATED') + df2 = store.get('mydata') + _created = df2['CREATED'].astype(datetime).unique()[0].to_pydatetime() + self.assertEqual(_created.replace(second=0, microsecond=0), + now.replace(second=0, microsecond=0)) + # -- check custom timestamp column, value as tuple + now = datetime.utcnow() - timedelta(days=1) + store.put(df, 'mydata', append=False, timestamp=('CREATED', now)) + df2 = store.get('mydata') + _created = df2['CREATED'].astype(datetime).unique()[0].to_pydatetime() + self.assertEqual(_created.replace(second=0, microsecond=0), + now.replace(second=0, microsecond=0)) + # set a day in the past to avoid accidentally creating the current + # datetime in mongo + now = datetime.now() - timedelta(days=1) + store.put(df, 'mydata', timestamp=now, append=False) + df2 = store.get('mydata') + # compare the data + _created = df2['_created'].astype(datetime).unique()[0].to_pydatetime() + self.assertEqual(_created.replace(microsecond=0), + now.replace(microsecond=0)) + + def test_get_dataframe_filter(self): + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + # filter in mongodb + df2 = store.get('mydata', filter=dict(a__gt=1, a__lt=10)) + # filter local dataframe + df = df[(df.a > 1) & (df.a < 10)] + self.assertTrue(df.equals(df2), "expected dataframes to be equal") + + def test_get_dataframe_project(self): + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + # filter in mongodb + df2 = store.get('mydata', columns=['a']) + # filter local dataframe + df = df[['a']] + self.assertTrue(df.equals(df2), "expected dataframes to be equal") + + def test_get_dataframe_projected_mixin(self): + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)), + 'c': list(range(1, 10)), + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + # filter in mongodb + specs = ['a', ':b', ':', 'b:', '^c'] + for spec in specs: + name_spec = 'mydata[{}]'.format(spec) + df2 = store.get(name_spec) + # filter local dataframe + if spec == ':': + dfx = df.loc[:, :] + elif ':' in spec: + from_col, to_col = spec.split(':') + slice_ = slice(from_col or None, to_col or None) + dfx = df.loc[:, slice_] + elif spec.startswith('^'): + spec_cols = spec[1:].split(',') + cols = [col for col in df.columns if col not in spec_cols] + dfx = df[cols] + else: + dfx = df[[spec]] + self.assertTrue(dfx.equals(df2), "expected dataframes to be equal") + + def test_put_dataframe_with_index(self): + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store = OmegaStore(prefix='') + store.put(df, 'mydata', index=['a', '-b']) + idxs = list(store.collection('mydata').list_indexes()) + idx_names = map(lambda v: dict(v).get('name'), idxs) + self.assertIn('asc_a__desc_b', idx_names) + + def test_put_dataframe_timeseries(self): + # create some dataframe + tsidx = pd.date_range(pd.datetime(2016, 1, 1), pd.datetime(2016, 4, 1)) + df = pd.DataFrame({ + 'a': list(range(0, len(tsidx))), + 'b': list(range(0, len(tsidx))) + }, index=tsidx) + store = OmegaStore(prefix='') + store.put(df, 'mydata') + dfx = store.get('mydata') + assert_frame_equal(df, dfx) + idxs = list(store.collection('mydata').list_indexes()) + idx_names = [dict(v).get('name') for v in idxs] + self.assertIn('asc__idx#0_0', idx_names) + + def test_put_dataframe_multiindex(self): + # create some dataframe + store = OmegaStore(prefix='') + midx = pd.MultiIndex(levels=[[u'bar', u'baz', u'foo', u'qux'], + [u'one', u'two']], + labels=[ + [0, 0, 1, 1, 2, 2, 3, 3], + [0, 1, 0, 1, 0, 1, 0, 1]], + names=[u'first', u'second']) + df = pd.DataFrame({'x': range(0, len(midx))}, index=midx) + store.put(df, 'mydata') + dfx = store.get('mydata') + assert_frame_equal(df, dfx) + idxs = list(store.collection('mydata').list_indexes()) + idx_names = [dict(v).get('name') for v in idxs] + self.assertIn('asc__idx#0_first__asc__idx#1_second', idx_names) + + def test_put_python_dict(self): + # create some data + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + store = OmegaStore(prefix='') + store.put(data, 'mydata') + data2 = store.get('mydata') + self.assertEquals([data], data2) + + def test_put_python_dict_multiple(self): + # create some data + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + store = OmegaStore(prefix='') + store.put(data, 'mydata') + store.put(data, 'mydata') + data2 = store.get('mydata') + # we will have stored the same object twice + self.assertEquals(data, data2[0]) + self.assertEquals(data, data2[1]) + + def test_get_forced_python(self): + """ + this tests we can retrieve data as python values + + the purpose is to test the basic mode of OmegaStore in + case pandas and scikit learn are not available + """ + store = OmegaStore(prefix='') + # pure data + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + meta = store.put(data, 'data') + data2 = store.get('data', force_python=True) + self.assertEqual(data, data2) + # dataframe + # create some dataframe + df = pd.DataFrame({ + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + }) + store.put(df, 'mydata') + df2 = store.get('mydata', force_python=True) + df2 = pd.DataFrame(df2) + real_cols = [col for col in df2.columns + if (col != '_id' + and not col.startswith('_idx') + and not col.startswith('_om'))] + df2 = df2[real_cols] + self.assertTrue(df.equals(df2), "expected dataframes to be equal") + # model + iris = load_iris() + X = iris.data + Y = iris.target + lr = LogisticRegression() + lr.fit(X, Y) + # store it remote + store.put(lr, 'foo') + # get it back as a zipfile + lr2file = store.get('foo', force_python=True) + contents = lr2file.read() + with ZipFile(BytesIO(contents)) as zipf: + self.assertIn('foo', zipf.namelist()) + + def test_store_with_metadata(self): + om = OmegaStore(prefix='') + # dict + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + attributes = {'foo': 'bar'} + meta = om.put(data, 'data', attributes=attributes) + self.assertEqual(meta.kind, 'python.data') + self.assertEqual(meta.attributes, attributes) + data2 = om.get('data') + self.assertEqual([data], data2) + # dataframe + df = pd.DataFrame(data) + meta = om.put(df, 'datadf', attributes=attributes) + self.assertEqual(meta.kind, 'pandas.dfrows') + self.assertEqual(meta.attributes, attributes) + df2 = om.get('datadf') + assert_frame_equal(df, df2) + # model + lr = LogisticRegression() + meta = om.put(lr, 'mymodel', attributes=attributes) + self.assertEqual(meta.kind, 'sklearn.joblib') + self.assertEqual(meta.attributes, attributes) + lr2 = om.get('mymodel') + self.assertIsInstance(lr2, LogisticRegression) + + def test_store_dataframe_as_dfgroup(self): + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + result_data = { + 'a': list(range(1, 2)), + 'b': 1, + } + df = pd.DataFrame(data) + result_df = pd.DataFrame(result_data) + store = OmegaStore() + groupby_columns = ['b'] + meta = store.put(df, 'dfgroup', groupby=groupby_columns) + self.assertEqual(meta.kind, 'pandas.dfgroup') + # make sure the collection is created + self.assertIn( + 'omegaml.dfgroup.datastore', store.mongodb.collection_names()) + df2 = store.get('dfgroup', kwargs={'b': 1}) + self.assertTrue(df2.equals(result_df)) + df3 = store.get('dfgroup') + self.assertTrue(df3.equals(df)) + df4 = store.get('dfgroup', kwargs={'a': 1}) + self.assertTrue(df4.equals(result_df)) + + def test_store_dataframe_as_hdf(self): + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + meta = store.put(df, 'foo', as_hdf=True) + self.assertEqual(meta.kind, 'pandas.hdf') + # make sure the hdf file is actually there + self.assertIn('omegaml.foo.hdf', store.fs.list()) + df2 = store.get('foo') + self.assertTrue(df.equals(df2), "dataframes differ") + # test for non-existent file raises exception + meta = store.put(df2, 'foo_will_be_removed', as_hdf=True) + file_id = store.fs.get_last_version( + 'omegaml.foo_will_be_removed.hdf')._id + store.fs.delete(file_id) + self.assertRaises( + gridfs.errors.NoFile, store.get, 'foo_will_be_removed') + store2 = OmegaStore() + # test hdf file is not there + self.assertNotIn('hdfdf.hdf', store2.fs.list()) + + def test_put_same_name(self): + """ test if metadata is updated instead of a new created """ + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + # store the object + meta = store.put(df, 'foo') + # store it again + meta2 = store.put(df, 'foo', append=False) + # we should still have a new object in metadata + # and the old should be gone + self.assertNotEqual(meta.pk, meta2.pk) + # Meta is to silence lint on import error + Meta = Metadata + metas = Meta.objects(name='foo', prefix=store.prefix, + bucket=store.bucket).all() + self.assertEqual(len(metas), 1) + df2 = store.get('foo') + self.assertTrue(df.equals(df2)) + + def test_put_append_false(self): + """ test if we can create a new dataframe without previous metadata """ + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + # store the object + unique_name = uuid.uuid4().hex + meta = store.put(df, unique_name, append=False) + self.assertEqual(meta['name'], unique_name) + + def test_store_with_attributes(self): + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + # store the object, no attributes + meta = store.put(df, 'foo', append=False) + meta = store.metadata('foo') + self.assertEqual(meta.attributes, {}) + # update attributes + meta = store.put(df, 'foo', append=False, attributes={'foo': 'bar'}) + meta = store.metadata('foo') + self.assertEqual(meta.attributes, {'foo': 'bar'}) + meta = store.put( + df, 'foo', append=False, attributes={'foo': 'bax', + 'foobar': 'barbar'}) + meta = store.metadata('foo') + self.assertEqual(meta.attributes, {'foo': 'bax', + 'foobar': 'barbar'}) + + def test_drop(self): + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + meta = store.put(df, 'hdfdf', as_hdf=True) + self.assertTrue(store.drop('hdfdf')) + meta = store.put(df, 'datadf') + self.assertTrue(store.drop('datadf')) + self.assertEqual( + store.list('datadf'), [], 'expected the store to be empty') + with self.assertRaises(DoesNotExist): + store.drop('nxstore', force=False) + try: + store.drop('nxstore', force=True) + raised = False + except: + raised = True + self.assertFalse(raised) + + def test_list_raw(self): + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + meta = store.put(df, 'hdfdf', as_hdf=True) + # list with pattern + entries = store.list(pattern='hdf*', raw=True) + self.assertTrue(isinstance(entries[0], Metadata)) + self.assertEqual('hdfdf', entries[0].name) + self.assertEqual(len(entries), 1) + # list with regexp + entries = store.list(regexp='hdf.*', raw=True) + self.assertTrue(isinstance(entries[0], Metadata)) + self.assertEqual('hdfdf', entries[0].name) + self.assertEqual(len(entries), 1) + # list without pattern nor regexp + entries = store.list('hdfdf', kind=Metadata.PANDAS_HDF, raw=True) + self.assertTrue(isinstance(entries[0], Metadata)) + self.assertEqual('hdfdf', entries[0].name) + self.assertEqual(len(entries), 1) + # subset kind + entries = store.list('hdfdf', raw=True, kind=Metadata.PANDAS_DFROWS) + self.assertEqual(len(entries), 0) + entries = store.list('hdfdf', raw=True, kind=Metadata.PANDAS_HDF) + self.assertEqual(len(entries), 1) + + def test_lazy_unique(self): + """ test getting a MDataFrame and unique values """ + data = { + 'a': list(range(1, 10)), + 'b': list(range(1, 10)) + } + df = pd.DataFrame(data) + store = OmegaStore() + meta = store.put(df, 'foo', append=False) + val = store.get('foo', lazy=True).a.unique().value + self.assertListEqual(data['a'], list(val)) + + def test_store_series(self): + """ test storing a pandas series with it's own index """ + from string import ascii_lowercase + series = pd.Series(range(10), index=(c for c in ascii_lowercase[0:10])) + store = OmegaStore() + store.put(series, 'fooseries', append=False) + series2 = store.get('fooseries') + assert_series_equal(series, series2) + + def test_store_named_series(self): + """ test storing a pandas series with it's own index """ + from string import ascii_lowercase + series = pd.Series(range(10), + name='foo', + index=(c for c in ascii_lowercase[0:10])) + store = OmegaStore() + store.put(series, 'fooseries', append=False) + series2 = store.get('fooseries') + assert_series_equal(series, series2) + + def test_store_series_timeindex(self): + """ test storing a pandas series with it's own index """ + series = pd.Series(range(10), + name='foo', + index=pd.date_range(pd.datetime(2016, 1, 1), + pd.datetime(2016, 1, 10))) + store = OmegaStore() + store.put(series, 'fooseries', append=False) + series2 = store.get('fooseries') + assert_series_equal(series, series2) + + def test_store_irregular_column_names(self): + """ test storing irregular column names """ + df = pd.DataFrame({'x_1': range(10)}) + store = OmegaStore() + store.put(df, 'foo', append=False) + df2 = store.get('foo') + self.assertEqual(df.columns, df2.columns) + + def test_store_datetime(self): + """ test storing naive datetimes """ + df = pd.DataFrame({ + 'x': pd.date_range(pd.datetime(2016, 1, 1), + pd.datetime(2016, 1, 10)) + }) + store = OmegaStore() + store.put(df, 'test-date', append=False) + df2 = store.get('test-date') + testing.assert_frame_equal(df, df2) + + def test_store_tz_datetime(self): + """ test storing timezoned datetimes """ + df = pd.DataFrame({ + 'y': pd.date_range('now', periods=10, tz='US/Eastern', normalize=True) + }) + store = OmegaStore() + store.put(df, 'test-date', append=False) + df2 = store.get('test-date') + testing.assert_frame_equal(df, df2) + + def test_store_dict_in_df(self): + df = pd.DataFrame({ + 'x': [{'foo': 'bar '}], + }) + store = OmegaStore() + store.put(df, 'test-dict', append=False) + df2 = store.get('test-dict') + testing.assert_frame_equal(df, df2) diff --git a/omegaml/util.py b/omegaml/util.py new file mode 100644 index 00000000..b7d24c2b --- /dev/null +++ b/omegaml/util.py @@ -0,0 +1,448 @@ +from __future__ import absolute_import + +import logging +import re + +import six +from six import string_types + +try: + import urlparse +except: + from urllib import parse as urlparse + +__settings = None + + +def is_dataframe(obj): + try: + import pandas as pd + return isinstance(obj, pd.DataFrame) + except: + return False + + +def is_series(obj): + try: + import pandas as pd + return isinstance(obj, pd.Series) and not isinstance(obj, pd.DataFrame) + except: + return False + + +def is_estimator(obj): + try: + from sklearn.base import BaseEstimator + from sklearn.pipeline import Pipeline + return isinstance(obj, (BaseEstimator, Pipeline)) + except: + False + + +def is_ndarray(obj): + try: + import numpy as np + return isinstance(obj, np.ndarray) + except: + False + + +def is_spark_mllib(obj): + """ + # unlike scikit learn obj is not the actual model, but a specification of + # the model for the spark server to create. so obj is the name of the + # python class, e.g. obj=pyspark.mllib.clustering.KMeans + """ + if isinstance(obj, string_types): + return 'pyspark.mllib' in obj + return False + + +def settings(reload=False): + """ wrapper to get omega settings from either django or omegamldefaults """ + import omegaml.defaults as omdefaults + global __settings + if not reload and __settings is not None: + return __settings + try: + # see if we're running as a django app + from django.utils import six + from django.utils.functional import empty + from django.conf import settings as djsettings # @UnresolvedImport + defaults = djsettings + # this is to test if django was initialized. if not revert + # to using omdefaults + try: + if defaults._wrapped is empty: + # django is not initialized, use omega defaults + raise ValueError() + getattr(defaults, 'SECRET_KEY') + except Exception as e: + from warnings import warn + warn("Using omegaml.defaults because Django was not initialized." + "Try importing omegaml within a method instead of at the " + "module level") + raise + except Exception as e: + defaults = omdefaults + else: + # get default omega settings into django settings if not set + # there already + import omegaml.defaults as omdefaults + for k in dir(omdefaults): + if not hasattr(defaults, k): + setattr(defaults, k, getattr(omdefaults, k)) + __settings = defaults + return __settings + + +def override_settings(**kwargs): + """ test support """ + cfgvars = settings() + for k, v in six.iteritems(kwargs): + setattr(cfgvars, k, v) + # -- OMEGA_CELERY_CONFIG updates + celery_config = getattr(cfgvars, 'OMEGA_CELERY_CONFIG', {}) + for k in [k for k in kwargs.keys() if k.startswith('OMEGA_CELERY')]: + celery_k = k.replace('OMEGA_', '') + celery_config[celery_k] = kwargs[k] + setattr(cfgvars, 'OMEGA_CELERY_CONFIG', celery_config) + + +def delete_database(): + """ test support """ + from pymongo import MongoClient + + mongo_url = settings().OMEGA_MONGO_URL + parsed_url = urlparse.urlparse(mongo_url) + database_name = parsed_url.path[1:] + # authenticate via admin db + # see https://stackoverflow.com/a/20554285 + c = MongoClient(mongo_url, authSource='admin') + c.drop_database(database_name) + + +def make_tuple(arg): + if not isinstance(arg, (list, tuple)): + arg = (arg,) + return tuple(arg) + + +def make_list(arg): + if not isinstance(arg, (list)): + arg = list(arg) + return arg + + +def flatten_columns(col, sep='_'): + # source http://stackoverflow.com/a/29437514 + if not type(col) is tuple: + return col + else: + new_col = '' + for leveli, level in enumerate(col): + if not level == '': + if not leveli == 0: + new_col += sep + new_col += level + return new_col + + +CLASS_CACHE = {} + + +def load_class(requested_class): + """ + Check if requested_class is a string, if so attempt to load + class from module, otherwise return requested_class as is + """ + import importlib + if isinstance(requested_class, six.string_types): + if requested_class in CLASS_CACHE: + return CLASS_CACHE.get(requested_class) + module_name, class_name = requested_class.rsplit(".", 1) + try: + m = importlib.import_module(module_name) + cls = getattr(m, class_name) + CLASS_CACHE[requested_class] = cls + return cls + except: + logging.debug( + 'could not load module %s for class %s' % ( + module_name, class_name)) + raise + return requested_class + + +def get_rdd_from_df(df): + """ + takes a pandas df and returns a spark RDD + """ + from pyspark import SparkContext, SQLContext + from pyspark.mllib.linalg import Vectors + sc = SparkContext.getOrCreate() + from warnings import warn + warn( + "get_rdd_from_df creates a spark context, it is recommended" + " that you use SparkContext.getOrCreate() to prevent multiple context" + " creation") + sqlContext = SQLContext(sc) + spark_df = sqlContext.createDataFrame(df) + rdd = spark_df.rdd.map(lambda data: Vectors.dense( + [float(x) for x in data])) + return rdd + + +def get_labeledpoints(Xname, Yname): + """ + returns a labeledpoint RDD from the datasets provided + """ + import omegaml as om + from pyspark.mllib.regression import LabeledPoint + # import from datastore + X = om.datasets.get(Xname) + Y = om.datasets.get(Yname) + result = Y.join(X) + # create labeled point + rdd = get_rdd_from_df(result) + labeled_point = rdd.map(lambda x: LabeledPoint(float(x[0]), x[1:])) + return labeled_point + + +def get_labeled_points_from_rdd(rdd): + """ + returns a labeledpoint from the RDD provided + """ + from pyspark.mllib.regression import LabeledPoint + return rdd.map(lambda x: LabeledPoint(float(x[0]), x[1:])) + + +def unravel_index(df, row_count=0): + """ + convert index columns into dataframe columns + + index columns are stored in the dataframe, named '_idx#_' + where n is the sequence and name is the nth index name. + + Use restore_index_columns_order to get back the original index columns + in sequence. + + :param df: the dataframe + :param row_count: the row_count base, ensures subsequent stores + get differnt row ids + :return: the unravelled dataframe, meta + """ + # remember original names + idx_meta = { + 'names': df.index.names, + } + # convert index names so we can restore them later + store_idxnames = ['_idx#{}_{}'.format(i, name or i) + for i, name in enumerate(idx_meta['names'])] + df.index.names = store_idxnames + unravelled_df, idx_meta = df.reset_index(), idx_meta + # store row ids + unravelled_df['_om#rowid'] = unravelled_df.index.values + row_count + # restore index names on original dataframe + df.index.names = idx_meta['names'] + return unravelled_df, idx_meta + + +def restore_index_columns_order(columns): + """ + from an iterable of column names get the index columns in sequence order + + index columns are named '_idx#_' where n is the sequence + of the original index column and name is the name + """ + + def get_index_order(col): + if '_idx#' in col: + n = col.split('_')[1].split('#')[1] + else: + n = 0 + return n + + index_cols = (col for col in columns if col and col.startswith('_idx')) + index_cols = sorted(index_cols, key=get_index_order) + return index_cols + + +def restore_index(df, idx_meta, rowid_sort=True): + """ + restore index proper + + :param df: the dataframe + :param idx_meta: index metadata + :param rowid_sort: whether to sort by row id. defaults to True + If your query is already sorted in some specific way, + specify False to keep the sort order. + """ + # -- establish row order proper + if rowid_sort and '_om#rowid' in df: + df.sort_values('_om#rowid', inplace=True) + del df['_om#rowid'] + # -- get index columns + index_cols = restore_index_columns_order(df.columns) + # -- set index columns + result = df.set_index(index_cols) if index_cols else df + if index_cols: + result.index.names = idx_meta.get('names', [None] * len(index_cols)) + return result + + +def jsonescape(s): + return str(s).replace('.', '_') + + +def grouper(n, iterable): + # https://stackoverflow.com/a/8998040 + import itertools + it = iter(iterable) + while True: + chunk_it = itertools.islice(it, n) + try: + first_el = next(chunk_it) + except StopIteration: + return + yield itertools.chain((first_el,), chunk_it) + + +def cursor_to_dataframe(cursor, chunk_size=10000): + # a faster and less memory hungry variant of DataFrame.from_records + # works by building a set of smaller dataframes to reduce memory + # consumption. Note chunks are of size max. chunk_size. + import pandas as pd + frames = [] + if hasattr(cursor, 'count'): + count = cursor.count() + chunk_size = max(chunk_size, int(count * .1)) + else: + # CommandCursors don't have .count, go as long as we can + count = None + if count is None or count > 0: + for chunk in grouper(chunk_size, cursor): + frames.append(pd.DataFrame.from_records(chunk)) + if frames: + df = pd.concat(frames) + else: + df = pd.DataFrame() + else: + df = pd.DataFrame() + return df + + +def reshaped(data): + """ + check if data is 1d and if so reshape to a column vector + """ + import pandas as pd + import numpy as np + if isinstance(data, (pd.Series, pd.DataFrame)): + if len(data.shape) == 1: + data = data.values.reshape(-1, 1) + else: + if len(data.shape) == 2 and data.shape[1] == 1: + data = data.values.reshape(-1, 1) + elif isinstance(data, np.ndarray): + if len(data.shape) == 1: + data = data.reshape(-1, 1) + elif isinstance(data, list): + data = np.array(data) + if len(data.shape) == 1: + data = data.reshape(-1, 1) + return data + + +def gsreshaped(data): + """ + gridsearch reshape values according to GridSearchCV.fit + + see https://stackoverflow.com/a/49241326 + """ + import pandas as pd + import numpy as np + if isinstance(data, (pd.Series, pd.DataFrame)): + if len(data.shape) == 2 and data.shape[1] == 1: + data = data.values.reshape(-1) + elif isinstance(data, np.ndarray): + if len(data.shape) == 2 and data.shape[1] == 1: + data = data.reshape(-1) + return data + + +def convert_dtypes(df, dtypes): + """ + get back original dtypes + + :param df: the dataframe to apply conversion to + :param dtypes: the dict mapping column to dtype (use kind_meta['dtypes']) + """ + # tz pattern used in convert_dtypes + tzinfo_pattern = re.compile('datetime64\[ns, (.*)\]') + for col, dtype in six.iteritems(dtypes): + if dtype.startswith('datetime'): + if not hasattr(df, 'dtypes'): + continue + try: + match = tzinfo_pattern.match(dtype) + if match: + tzname = match.groups()[0] + df[col] = df[col].dt.tz_localize('UTC').dt.tz_convert(tzname) + except: + # TODO ignore errors, issue warning + pass + + return df + + +class PickableCollection(object): + def __init__(self, collection): + super(PickableCollection, self).__setattr__('collection', collection) + + def __getattr__(self, k): + return getattr(self.collection, k) + + def __setattr__(self, k, v): + return setattr(self.collection, k, v) + + def __getitem__(self, k): + return self.collection[k] + + def __setitem__(self, k, v): + self.collection[k] = v + + def __getstate__(self): + client = self.collection._Collection__database._Database__client + host, port = list(client.nodes)[0] + return { + 'name': self.name, + 'database': self.database.name, + 'host': host, + 'port': port, + 'credentials': self.database.client._MongoClient__all_credentials[self.database.name], + } + + def __setstate__(self, state): + from pymongo import MongoClient + url = 'mongodb://{credentials.username}:{credentials.password}@{host}:{port}/{database}'.format(**state) + client = MongoClient(url, authSource=state['credentials'].source) + db = client.get_database() + collection = db[state['name']] + super(PickableCollection, self).__setattr__('collection', collection) + + def __repr__(self): + return 'PickableCollection({})'.format(repr(self.collection)) + + +def extend_instance(obj, cls, *args, **kwargs): + """Apply mixins to a class instance after creation""" + # source https://stackoverflow.com/a/31075641 + from omegaml import load_class + cls = load_class(cls) + if cls not in obj.__class__.mro(): + base_cls = obj.__class__ + base_cls_name = 'Extended{}'.format(obj.__class__.__name__.split('.')[0]) + obj.__class__ = type(base_cls_name, (cls, base_cls), {}) + if hasattr(obj, '_init_mixin'): + obj._init_mixin(*args, **kwargs) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..3c886d86 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +nose==1.3.7 +nosexcover==1.0.11 +Flask==1.0.2 +flask-restplus==0.12.1 +honcho==1.0.1 +croniter==0.3.12 diff --git a/scripts/mongoinit.js b/scripts/mongoinit.js new file mode 100644 index 00000000..342b1442 --- /dev/null +++ b/scripts/mongoinit.js @@ -0,0 +1,6 @@ +/* cat scripts/mongoinit.js | docker exec -i omegaml-cemongo_1 mongo */ +db.adminCommand({ + createUser: 'admin', + pwd : 'foobar', + roles : ['root'], +}); diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..e33c1218 --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +import os +from setuptools import setup, find_packages +from omegaml._version import version + +README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() + +# allow setup.py to be run from any path +os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) + +setup( + name='omegaml', + version=version, + packages=find_packages(), + include_package_data=True, + license='http://www.apache.org/licenses/LICENSE-2.0', + description='data science platform that scales from laptop to teams to enterprise. Batteries included.', + long_description=README, + url='https://omegaml.io/', + author='Patrick Senti', + author_email='patrick.senti@omegaml.io', + classifiers=[ + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + ], + install_requires=[ + 'celery>=3.1.20,<3.1.24', + 'joblib>=0.9.4', + 'jupyter-client>=4.1.1', + 'pymongo>=3.2.2', + 'mongoengine>=0.10.6', + 'pandas>=0.17.1', + 'numpy>=1.10.4', + 'scipy>=0.17.0', + 'scikit-learn>=0.17.1', + 'tables>=3.2.2', + 'PyYAML>=3.11', + 'flask-restplus==0.12.1', + ], + dependency_links=[ + ] +) diff --git a/shippable.yml b/shippable.yml new file mode 100644 index 00000000..3d2e6da7 --- /dev/null +++ b/shippable.yml @@ -0,0 +1,40 @@ +language: python + +python: + - 3.5 + +services: + - mysql + +build: + ci: + - env + - > + apt-get update --fix-missing && + apt-get install apt-transport-https && + apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 && + echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list && + apt-get update && + apt-get install texlive-xetex texlive-fonts-recommended texlive-latex-recommended texlive-pictures texlive-latex-extra && + apt-get install -y --allow-change-held-packages --allow-downgrades mongodb-org=3.6.8 mongodb-org-server=3.6.8 mongodb-org-shell=3.6.8 mongodb-org-mongos=3.6.8 mongodb-org-tools=3.6.8 + - /usr/bin/mongod --auth > /dev/null & + - sleep 5 && cat scripts/mongoinit.js | /usr/bin/mongo + - wget -q https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh + - chmod +x ./Miniconda3-latest-Linux-x86_64.sh && ./Miniconda3-latest-Linux-x86_64.sh -bfp /usr/local + - conda install -y conda=4.3 + - conda install python=3.6 + - conda install -y --file conda-requirements.txt + - conda install -y -c conda-forge jupyterhub + - PATH=/usr/local/bin:$PATH sudo apt-get -y update + - PATH=/usr/local/bin:$PATH sudo apt-get -y install build-essential libssl-dev libffi-dev python-dev + - PATH=/usr/local/bin:$PATH pip install -U setuptools + - PATH=/usr/local/bin:$PATH pip install -r requirements.txt + - PATH=/usr/local/bin:$PATH pip install -e . + - > + PATH=/usr/local/bin:$PATH MONGO_URL=mongodb://admin:foobar@localhost:27017/omega + nosetests + --with-xunit --xunit-file=shippable/testresults/nosetests.xml + --with-xcoverage --cover-package=omegaml + --cover-tests --xcoverage-file=shippable/codecoverage/coverage.xml + +