Repository: mlrun/mlrun

Issue:
Docker for HTTP DB with gunicorn
Write a docker to run the HTTP DB with gunicorn.

Currently ignore the fact that the underlying `filedb` is not thread/process safe.

Discussion:

Patch:
diff --git a/Dockerfile.db b/Dockerfile.db
index fb2db4f62..d1c749014 100644
--- a/Dockerfile.db
+++ b/Dockerfile.db
@@ -20,7 +20,7 @@ RUN pip install pipenv
 COPY Pipfile* ./
 RUN pipenv sync
 COPY . .
-ENV MLRUN_httpd__dirpath=/mlrun/db
+ENV MLRUN_httpdb__dirpath=/mlrun/db
 VOLUME /mlrun/db
 # TODO: gunicorn/uwsgi? (underlying filedb & concurrency issues)
 CMD ["pipenv", "run", "python", "-m", "mlrun", "db"]
diff --git a/Dockerfile.db-gunicorn b/Dockerfile.db-gunicorn
new file mode 100644
index 000000000..df716b011
--- /dev/null
+++ b/Dockerfile.db-gunicorn
@@ -0,0 +1,30 @@
+# Copyright 2018 Iguazio
+#
+# 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.
+
+FROM python:3.6-slim
+RUN apt-get update && apt-get install -y gcc
+RUN pip install --upgrade --no-cache pip pipenv
+WORKDIR /mlrun
+RUN pip install pipenv
+COPY Pipfile* ./
+RUN pipenv sync
+COPY . .
+ENV MLRUN_httpdb__dirpath=/mlrun/db
+ENV MLRUN_httpdb__port=8080
+VOLUME /mlrun/db
+# TODO: gunicorn/uwsgi? (underlying filedb & concurrency issues)
+CMD pipenv run gunicorn \
+    --workers=8 \
+    --bind=0.0.0.0:$MLRUN_httpdb__port \
+    mlrun.db.httpd:app
diff --git a/Makefile b/Makefile
index a44d332e3..0af32c42b 100644
--- a/Makefile
+++ b/Makefile
@@ -58,3 +58,10 @@ docker-db:
 	    -f Dockerfile.db \
 	    --tag mlrun/mlrun-db \
 	    .
+
+.PHONY: docker-db-gunicorn
+docker-db-gunicorn:
+	docker build \
+	    -f Dockerfile.db-gunicorn \
+	    --tag mlrun/mlrun-db-gunicorn \
+	    .
diff --git a/Pipfile b/Pipfile
index 8c6900773..3b3f72f0b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -17,6 +17,7 @@ GitPython = ">=2.1.0"
 aiohttp = ">=3.5.0"
 boto3 = ">=1.9"
 click = ">=7.0"
+gunicorn = "==19.9.0"
 kubernetes = ">=9.0.0"
 nest-asyncio = ">=1.0.0"
 nuclio-jupyter = ">=0.7.6"
diff --git a/Pipfile.lock b/Pipfile.lock
index 47ba5e2e5..982ca4f31 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "33faf639116f187c0ba1f93ce3ff6ee671d9a948b8d71d2186282b8f9663887c"
+            "sha256": "1134f1a1ebaf9039fb3ac1e65a9e0cd5b09663c288e7d039b59daa76e67c57f3"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -18,22 +18,21 @@
     "default": {
         "aiohttp": {
             "hashes": [
-                "sha256:022c400e30848b1994236e31fb38db1dc4b551efe049f737cbac690ab2cdf5c4",
-                "sha256:10f9316ef068536dec0b9f09531fa1cb6bfa8394f278022cb96e789c77811ad2",
-                "sha256:2599b93fd5ba1120b3bd1366d67a7e26bd45b3d5d5548069e00b2fbef7f20ab0",
-                "sha256:2a1c71e7fb8c50e60fb4c9bab8bd5cf7c07f91a6b27dc2556d7354cd2ebb3689",
-                "sha256:6a19d34cc01414d94dd5a4466f8f397293fcb8929df8eeb8989119cc5ef928bb",
-                "sha256:7aab39c2a61a5c6b15bb7e561218ef64770ca1fbf4cc1878c96e630e2b7cc3cc",
-                "sha256:8959e28bc1b87542b0ee4a8302128f633bee296252f261bf03e118c4dff725f0",
-                "sha256:89820f7c488f4e9b1f74371da33403181e11e006663ddf074317aacd690838a6",
-                "sha256:ab761cf0f0b0b90887e276b4a7918f11e323f2228bbb30814bbd538c122028bf",
-                "sha256:cc648ecaca79e37c6e26f370e802e7ae640a069913f661f66c0421084bef219a",
-                "sha256:d6f26e80cd55ac88e1f0397fc8d547933225a5dc1add040e27788c2a028c64c6",
-                "sha256:e7d6ae4a36bfe6d7f93c6f42a0bfa1659f7d011006cb6e8207c85ef5acdb2986",
-                "sha256:fc55b1fec0e4cc1134ffb09ea3970783ee2906dc5dfd7cd16917913f2cfed65b"
+                "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
+                "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
+                "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
+                "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
+                "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
+                "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
+                "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
+                "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
+                "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
+                "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
+                "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
+                "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
             ],
             "index": "pypi",
-            "version": "==3.6.1"
+            "version": "==3.6.2"
         },
         "async-timeout": {
             "hashes": [
@@ -44,10 +43,10 @@
         },
         "attrs": {
             "hashes": [
-                "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
-                "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
+                "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
+                "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
             ],
-            "version": "==19.1.0"
+            "version": "==19.3.0"
         },
         "backcall": {
             "hashes": [
@@ -65,18 +64,18 @@
         },
         "boto3": {
             "hashes": [
-                "sha256:012dee0d170d9ee637831a2766c3d5a9cddbe3b18bf688b4c348ec591411bd21",
-                "sha256:2fd733957e83546547cede51ebf7356a14eeb369293e1e918e7eabcc7cf30e2c"
+                "sha256:2edee79d0e78c08b6d14d4dd91c0e4b3438dd4c90c859f06a397268b1cac17b2",
+                "sha256:3cd2078144c10417eb04e4bb263ea8e50a21c4aceafb52db33e3fe71e73b48aa"
             ],
             "index": "pypi",
-            "version": "==1.9.233"
+            "version": "==1.10.0"
         },
         "botocore": {
             "hashes": [
-                "sha256:9b0bf5614a0e6a29838dc1bc3d38405c57ec6dc3e25709776f9d2d7f7e84b0e0",
-                "sha256:9b93ca5743e9209daaece3e682626d8370f04c9693760c7c1e7fd95b746e45ff"
+                "sha256:507b8f13583a64ec2c9c112ff6e3dd8b548060adc7e1f57f25fda9fa34c2dfdb",
+                "sha256:c4b2ffb0f6ed7169beb260485bf5a42ee72a0a02f49f48b0557ed5e32bcf9e79"
             ],
-            "version": "==1.12.233"
+            "version": "==1.13.0"
         },
         "cachetools": {
             "hashes": [
@@ -146,18 +145,18 @@
         },
         "gitdb2": {
             "hashes": [
-                "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
-                "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
+                "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350",
+                "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"
             ],
-            "version": "==2.0.5"
+            "version": "==2.0.6"
         },
         "gitpython": {
             "hashes": [
-                "sha256:947cc75913e7b6da108458136607e2ee0e40c20be1e12d4284e7c6c12956c276",
-                "sha256:d2f4945f8260f6981d724f5957bc076398ada55cb5d25aaee10108bcdc894100"
+                "sha256:3237caca1139d0a7aa072f6735f5fd2520de52195e0fa1d8b83a9b212a2498b2",
+                "sha256:a7d6bef0775f66ba47f25911d285bcd692ce9053837ff48a120c2b8cf3a71389"
             ],
             "index": "pypi",
-            "version": "==3.0.2"
+            "version": "==3.0.4"
         },
         "google-auth": {
             "hashes": [
@@ -166,6 +165,14 @@
             ],
             "version": "==1.6.3"
         },
+        "gunicorn": {
+            "hashes": [
+                "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
+                "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
+            ],
+            "index": "pypi",
+            "version": "==19.9.0"
+        },
         "idna": {
             "hashes": [
                 "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
@@ -180,12 +187,19 @@
             "markers": "python_version < '3.7'",
             "version": "==1.1.0"
         },
+        "importlib-metadata": {
+            "hashes": [
+                "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
+                "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
+            ],
+            "version": "==0.23"
+        },
         "ipykernel": {
             "hashes": [
-                "sha256:167c3ef08450f5e060b76c749905acb0e0fbef9365899377a4a1eae728864383",
-                "sha256:b503913e0b4cce7ed2de965457dfb2edd633e8234161a60e23f2fe2161345d12"
+                "sha256:1a7def9c986f1ee018c1138d16951932d4c9d4da01dad45f9d34e9899565a22f",
+                "sha256:b368ad13edb71fa2db367a01e755a925d7f75ed5e09fbd3f06c85e7a8ef108a8"
             ],
-            "version": "==5.1.2"
+            "version": "==5.1.3"
         },
         "ipython": {
             "hashes": [
@@ -217,10 +231,10 @@
         },
         "jinja2": {
             "hashes": [
-                "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
-                "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
+                "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
+                "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
             ],
-            "version": "==2.10.1"
+            "version": "==2.10.3"
         },
         "jmespath": {
             "hashes": [
@@ -238,24 +252,24 @@
         },
         "jsonschema": {
             "hashes": [
-                "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f",
-                "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d"
+                "sha256:2fa0684276b6333ff3c0b1b27081f4b2305f0a36cf702a23db50edb141893c3f",
+                "sha256:94c0a13b4a0616458b42529091624e66700a17f847453e52279e35509a5b7631"
             ],
-            "version": "==3.0.2"
+            "version": "==3.1.1"
         },
         "jupyter-client": {
             "hashes": [
-                "sha256:6a6d415c62179728f6d9295b37356d8f6833e9e01c2b6e1901dc555571f57b21",
-                "sha256:f406f214f9daa92be110d5b83d62f3451ffc73d3522db7350f0554683533ab18"
+                "sha256:60e6faec1031d63df57f1cc671ed673dced0ed420f4377ea33db37b1c188b910",
+                "sha256:d0c077c9aaa4432ad485e7733e4d91e48f87b4f4bab7d283d42bb24cbbba0a0f"
             ],
-            "version": "==5.3.3"
+            "version": "==5.3.4"
         },
         "jupyter-core": {
             "hashes": [
-                "sha256:2c6e7c1e9f2ac45b5c2ceea5730bc9008d92fe59d0725eac57b04c0edfba24f7",
-                "sha256:f4fa22d6cf25f34807c995f22d2923693575c70f02557bcbfbe59bd5ec8d8b84"
+                "sha256:1368a838bba378c3c99f54c2961489831ea929ec7689a1d59d9844e584bc27dc",
+                "sha256:85103cee6548992780912c1a0a9ec2583a4a18f1ef79a248ec0db4446500bce3"
             ],
-            "version": "==4.5.0"
+            "version": "==4.6.0"
         },
         "jupyterlab": {
             "hashes": [
@@ -319,6 +333,13 @@
             ],
             "version": "==0.8.4"
         },
+        "more-itertools": {
+            "hashes": [
+                "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
+                "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
+            ],
+            "version": "==7.2.0"
+        },
         "multidict": {
             "hashes": [
                 "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
@@ -384,11 +405,11 @@
         },
         "nuclio-jupyter": {
             "hashes": [
-                "sha256:52c862eac54546eeab8a930021577433717ccd77b2c21f001de75383be6daf72",
-                "sha256:aac1ed39b5a26548406f03c655d4658f678b8cc6df613a0b1334493f86ba01c9"
+                "sha256:2e9ac7fa0640a8a78a5b54d14e2eb70124ff7940e24754ff8da8f01a632070f3",
+                "sha256:cc8889d540b096e9f3bb7d26a451aa689cf6dd9c65765b7e7637e6796a5dbf8b"
             ],
             "index": "pypi",
-            "version": "==0.7.5"
+            "version": "==0.7.6"
         },
         "nuclio-sdk": {
             "hashes": [
@@ -399,24 +420,29 @@
         },
         "numpy": {
             "hashes": [
-                "sha256:05dbfe72684cc14b92568de1bc1f41e5f62b00f714afc9adee42f6311738091f",
-                "sha256:0d82cb7271a577529d07bbb05cb58675f2deb09772175fab96dc8de025d8ac05",
-                "sha256:10132aa1fef99adc85a905d82e8497a580f83739837d7cbd234649f2e9b9dc58",
-                "sha256:12322df2e21f033a60c80319c25011194cd2a21294cc66fee0908aeae2c27832",
-                "sha256:16f19b3aa775dddc9814e02a46b8e6ae6a54ed8cf143962b4e53f0471dbd7b16",
-                "sha256:3d0b0989dd2d066db006158de7220802899a1e5c8cf622abe2d0bd158fd01c2c",
-                "sha256:438a3f0e7b681642898fd7993d38e2bf140a2d1eafaf3e89bb626db7f50db355",
-                "sha256:5fd214f482ab53f2cea57414c5fb3e58895b17df6e6f5bca5be6a0bb6aea23bb",
-                "sha256:73615d3edc84dd7c4aeb212fa3748fb83217e00d201875a47327f55363cef2df",
-                "sha256:7bd355ad7496f4ce1d235e9814ec81ee3d28308d591c067ce92e49f745ba2c2f",
-                "sha256:7d077f2976b8f3de08a0dcf5d72083f4af5411e8fddacd662aae27baa2601196",
-                "sha256:a4092682778dc48093e8bda8d26ee8360153e2047826f95a3f5eae09f0ae3abf",
-                "sha256:b458de8624c9f6034af492372eb2fee41a8e605f03f4732f43fc099e227858b2",
-                "sha256:e70fc8ff03a961f13363c2c95ef8285e0cf6a720f8271836f852cc0fa64e97c8",
-                "sha256:ee8e9d7cad5fe6dde50ede0d2e978d81eafeaa6233fb0b8719f60214cf226578",
-                "sha256:f4a4f6aba148858a5a5d546a99280f71f5ee6ec8182a7d195af1a914195b21a2"
-            ],
-            "version": "==1.17.2"
+                "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5",
+                "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e",
+                "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b",
+                "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7",
+                "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c",
+                "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418",
+                "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39",
+                "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26",
+                "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2",
+                "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e",
+                "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c",
+                "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4",
+                "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98",
+                "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1",
+                "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e",
+                "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8",
+                "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462",
+                "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2",
+                "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9",
+                "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6",
+                "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"
+            ],
+            "version": "==1.17.3"
         },
         "oauthlib": {
             "hashes": [
@@ -427,24 +453,28 @@
         },
         "pandas": {
             "hashes": [
-                "sha256:18d91a9199d1dfaa01ad645f7540370ba630bdcef09daaf9edf45b4b1bca0232",
-                "sha256:3f26e5da310a0c0b83ea50da1fd397de2640b02b424aa69be7e0784228f656c9",
-                "sha256:4182e32f4456d2c64619e97c58571fa5ca0993d1e8c2d9ca44916185e1726e15",
-                "sha256:426e590e2eb0e60f765271d668a30cf38b582eaae5ec9b31229c8c3c10c5bc21",
-                "sha256:5eb934a8f0dc358f0e0cdf314072286bbac74e4c124b64371395e94644d5d919",
-                "sha256:717928808043d3ea55b9bcde636d4a52d2236c246f6df464163a66ff59980ad8",
-                "sha256:8145f97c5ed71827a6ec98ceaef35afed1377e2d19c4078f324d209ff253ecb5",
-                "sha256:8744c84c914dcc59cbbb2943b32b7664df1039d99e834e1034a3372acb89ea4d",
-                "sha256:c1ac1d9590d0c9314ebf01591bd40d4c03d710bfc84a3889e5263c97d7891dee",
-                "sha256:cb2e197b7b0687becb026b84d3c242482f20cbb29a9981e43604eb67576da9f6",
-                "sha256:d4001b71ad2c9b84ff18b182cea22b7b6cbf624216da3ea06fb7af28d1f93165",
-                "sha256:d8930772adccb2882989ab1493fa74bd87d47c8ac7417f5dd3dd834ba8c24dc9",
-                "sha256:dfbb0173ee2399bc4ed3caf2d236e5c0092f948aafd0a15fbe4a0e77ee61a958",
-                "sha256:eebfbba048f4fa8ac711b22c78516e16ff8117d05a580e7eeef6b0c2be554c18",
-                "sha256:f1b21bc5cf3dbea53d33615d1ead892dfdae9d7052fa8898083bec88be20dcd2"
+                "sha256:0f484f43658a72e7d586a74978259657839b5bd31b903e963bb1b1491ab51775",
+                "sha256:0ffc6f9e20e77f3a7dc8baaad9c7fd25b858b084d3a2d8ce877bc3ea804e0636",
+                "sha256:23e0eac646419c3057f15eb96ab821964848607bf1d4ea5a82f26565986ec5e9",
+                "sha256:27c0603b15b5c6fa24885253bbe49a0c289381e7759385c59308ba4f0b166cf1",
+                "sha256:397fe360643fffc5b26b41efdf608647e3334a618d185a07976cd2dc51c90bce",
+                "sha256:3dbb3aa41c01504255bff2bd56944bdb915f6c9ce4bac7e2868efbace0b2a639",
+                "sha256:4e07c63247c59d61c6ebdbbb50196143cec6c5044403510c4e1a9d31854a83d6",
+                "sha256:4fa6d9235c6d2fecbeca82c3d326abd255866cafbfd37f66a0e826544e619760",
+                "sha256:56cb88b3876363d410a9d7724f43641ff164e2c9902d3266a648213e2efd5e6d",
+                "sha256:7ce1be1614455f83710b9a5dc1fc602a755bdddbe4dda1d41515062923a37bbf",
+                "sha256:ae1c96ffdeec376895e533107e0b0f9da16225a2184fbb24a5abc866769db75e",
+                "sha256:b6f27c9231be8a23de846f2302373991467dd8e397a4804d2614e8c5aa8d5a90",
+                "sha256:c6056067f894f9355bedcd168dd740aa849908d41c0a74756f6e38f203e941b3",
+                "sha256:ca91a19d1f0a280874a24dca44aadce42da7f3a7edb7e9ab7c7baad8febee2be",
+                "sha256:cbe4985f5c82a173f8cff6b7fe92d551addf99fb4ea9ff4eb4b1fe606bb098ec",
+                "sha256:e3e9893bfe80a8b8e6d56d36ebb7afe1df77db7b4068a6e2ef3636a91f6f1caa",
+                "sha256:e7b218e8711910dac3fed0d19376cd1ef0e386be5175965d332fd0c65d02a43b",
+                "sha256:ec48d18b8b63a5dbb838e8ea7892ee1034299e03f852bd9b6dffe870310414dd",
+                "sha256:f4ab6280277e3208a59bfa9f2e51240304d09e69ffb65abfb4a21d678b495f74"
             ],
             "index": "pypi",
-            "version": "==0.25.1"
+            "version": "==0.25.2"
         },
         "pandocfilters": {
             "hashes": [
@@ -482,11 +512,11 @@
         },
         "prompt-toolkit": {
             "hashes": [
-                "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
-                "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
-                "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
+                "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
+                "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
+                "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
             ],
-            "version": "==2.0.9"
+            "version": "==2.0.10"
         },
         "ptyprocess": {
             "hashes": [
@@ -505,10 +535,10 @@
         },
         "pyasn1-modules": {
             "hashes": [
-                "sha256:43c17a83c155229839cc5c6b868e8d0c6041dba149789b6d6e28801c64821722",
-                "sha256:e30199a9d221f1b26c885ff3d87fd08694dbbe18ed0e8e405a2a7126d30ce4c0"
+                "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b",
+                "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3"
             ],
-            "version": "==0.2.6"
+            "version": "==0.2.7"
         },
         "pygments": {
             "hashes": [
@@ -533,10 +563,10 @@
         },
         "pytz": {
             "hashes": [
-                "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
-                "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
+                "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
+                "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
             ],
-            "version": "==2019.2"
+            "version": "==2019.3"
         },
         "pyyaml": {
             "hashes": [
@@ -639,10 +669,10 @@
         },
         "tabulate": {
             "hashes": [
-                "sha256:8af07a39377cee1103a5c8b3330a421c2d99b9141e9cc5ddd2e3263fea416943"
+                "sha256:d0097023658d4dea848d6ae73af84532d1e86617ac0925d1adf1dd903985dac3"
             ],
             "index": "pypi",
-            "version": "==0.8.3"
+            "version": "==0.8.5"
         },
         "terminado": {
             "hashes": [
@@ -672,10 +702,10 @@
         },
         "traitlets": {
             "hashes": [
-                "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
-                "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
+                "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
+                "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
             ],
-            "version": "==4.3.2"
+            "version": "==4.3.3"
         },
         "typing-extensions": {
             "hashes": [
@@ -688,11 +718,11 @@
         },
         "urllib3": {
             "hashes": [
-                "sha256:2f3eadfea5d92bc7899e75b5968410b749a054b492d5a6379c1344a1481bc2cb",
-                "sha256:9c6c593cb28f52075016307fc26b0a0f8e82bc7d1ff19aaaa959b91710a56c47"
+                "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
+                "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
             ],
             "markers": "python_version >= '3.4'",
-            "version": "==1.25.5"
+            "version": "==1.25.6"
         },
         "wcwidth": {
             "hashes": [
@@ -737,6 +767,13 @@
                 "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
             ],
             "version": "==1.3.0"
+        },
+        "zipp": {
+            "hashes": [
+                "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
+                "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
+            ],
+            "version": "==0.6.0"
         }
     },
     "develop": {
@@ -746,13 +783,6 @@
             ],
             "version": "==2.2.1a0"
         },
-        "asn1crypto": {
-            "hashes": [
-                "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
-                "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
-            ],
-            "version": "==0.24.0"
-        },
         "atomicwrites": {
             "hashes": [
                 "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
@@ -762,10 +792,10 @@
         },
         "attrs": {
             "hashes": [
-                "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
-                "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
+                "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
+                "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
             ],
-            "version": "==19.1.0"
+            "version": "==19.3.0"
         },
         "bleach": {
             "hashes": [
@@ -790,36 +820,38 @@
         },
         "cffi": {
             "hashes": [
-                "sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
-                "sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
-                "sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
-                "sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
-                "sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
-                "sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
-                "sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
-                "sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
-                "sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
-                "sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
-                "sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
-                "sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
-                "sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
-                "sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
-                "sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
-                "sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
-                "sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
-                "sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
-                "sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
-                "sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
-                "sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
-                "sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
-                "sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
-                "sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
-                "sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
-                "sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
-                "sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
-                "sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
-            ],
-            "version": "==1.12.3"
+                "sha256:00d890313797d9fe4420506613384b43099ad7d2b905c0752dbcc3a6f14d80fa",
+                "sha256:0cf9e550ac6c5e57b713437e2f4ac2d7fd0cd10336525a27224f5fc1ec2ee59a",
+                "sha256:0ea23c9c0cdd6778146a50d867d6405693ac3b80a68829966c98dd5e1bbae400",
+                "sha256:193697c2918ecdb3865acf6557cddf5076bb39f1f654975e087b67efdff83365",
+                "sha256:1ae14b542bf3b35e5229439c35653d2ef7d8316c1fffb980f9b7647e544baa98",
+                "sha256:1e389e069450609c6ffa37f21f40cce36f9be7643bbe5051ab1de99d5a779526",
+                "sha256:263242b6ace7f9cd4ea401428d2d45066b49a700852334fd55311bde36dcda14",
+                "sha256:33142ae9807665fa6511cfa9857132b2c3ee6ddffb012b3f0933fc11e1e830d5",
+                "sha256:364f8404034ae1b232335d8c7f7b57deac566f148f7222cef78cf8ae28ef764e",
+                "sha256:47368f69fe6529f8f49a5d146ddee713fc9057e31d61e8b6dc86a6a5e38cecc1",
+                "sha256:4895640844f17bec32943995dc8c96989226974dfeb9dd121cc45d36e0d0c434",
+                "sha256:558b3afef987cf4b17abd849e7bedf64ee12b28175d564d05b628a0f9355599b",
+                "sha256:5ba86e1d80d458b338bda676fd9f9d68cb4e7a03819632969cf6d46b01a26730",
+                "sha256:63424daa6955e6b4c70dc2755897f5be1d719eabe71b2625948b222775ed5c43",
+                "sha256:6381a7d8b1ebd0bc27c3bc85bc1bfadbb6e6f756b4d4db0aa1425c3719ba26b4",
+                "sha256:6381ab708158c4e1639da1f2a7679a9bbe3e5a776fc6d1fd808076f0e3145331",
+                "sha256:6fd58366747debfa5e6163ada468a90788411f10c92597d3b0a912d07e580c36",
+                "sha256:728ec653964655d65408949b07f9b2219df78badd601d6c49e28d604efe40599",
+                "sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8",
+                "sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8",
+                "sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa",
+                "sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78",
+                "sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc",
+                "sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e",
+                "sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2",
+                "sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0",
+                "sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71",
+                "sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891",
+                "sha256:ec2fa3ee81707a5232bf2dfbd6623fdb278e070d596effc7e2d788f2ada71a05",
+                "sha256:fd82eb4694be712fcae03c717ca2e0fc720657ac226b80bbb597e971fc6928c2"
+            ],
+            "version": "==1.13.1"
         },
         "chardet": {
             "hashes": [
@@ -845,31 +877,36 @@
         },
         "cryptography": {
             "hashes": [
-                "sha256:24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c",
-                "sha256:25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643",
-                "sha256:3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216",
-                "sha256:41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799",
-                "sha256:5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a",
-                "sha256:5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9",
-                "sha256:72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc",
-                "sha256:7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8",
-                "sha256:961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53",
-                "sha256:96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1",
-                "sha256:ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609",
-                "sha256:b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292",
-                "sha256:cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e",
-                "sha256:e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6",
-                "sha256:f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed",
-                "sha256:f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"
-            ],
-            "version": "==2.7"
+                "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
+                "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
+                "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
+                "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
+                "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
+                "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
+                "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
+                "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
+                "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
+                "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
+                "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
+                "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
+                "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
+                "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
+                "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
+                "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
+                "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
+                "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
+                "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
+                "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
+                "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
+            ],
+            "version": "==2.8"
         },
         "dask": {
             "hashes": [
-                "sha256:a62d3dd41cc1e4b24d450cdd9f2c37e8c56acb1f05e8b65c51307f8f1604d578",
-                "sha256:a6791908725930e0f418e52e9b059d53f8c66e3a41107fd5b779ea91755cd9f6"
+                "sha256:81c7891f0d2e7ac03d1f7fabf1f639360a1db52c03a7155ba9b08e9ee6280f2b",
+                "sha256:876aaae1b36e92353bc42503283981c6e54718f897746991f13ef31a87efe4f6"
             ],
-            "version": "==2.4.0"
+            "version": "==2.6.0"
         },
         "deprecated": {
             "hashes": [
@@ -880,11 +917,11 @@
         },
         "distributed": {
             "hashes": [
-                "sha256:30d16787e3e6fb5e8deada538a3a4d6a52d37480d96224341bfd35f408b2e7b3",
-                "sha256:926829a5e7019d61d0af937262c423548af3289b06b58f35d562c84b13a1abdb"
+                "sha256:30b0ca195ace1e39bdd278bf1ad257f7674b3e2b8e7a2a37ce7e2ade4aecccf3",
+                "sha256:5f1082c158e976e23b05fa4d22566fec16c013c9bba12a428ea7be3fd47be4e0"
             ],
             "index": "pypi",
-            "version": "==2.4.0"
+            "version": "==2.6.0"
         },
         "docutils": {
             "hashes": [
@@ -896,10 +933,10 @@
         },
         "google-api-core": {
             "hashes": [
-                "sha256:2c23fbc81c76b941ffb71301bb975ed66a610e9b03f918feacd1ed59cf43a6ec",
-                "sha256:b2b91107bcc3b981633c89602b46451f6474973089febab3ee51c49cb7ae6a1f"
+                "sha256:b95895a9398026bc0500cf9b4a3f82c3f72c3f9150b26ff53af40c74e91c264a",
+                "sha256:df8adc4b97f5ab4328a0e745bee77877cf4a7d4601cb1cd5959d2bbf8fba57aa"
             ],
-            "version": "==1.14.2"
+            "version": "==1.14.3"
         },
         "google-auth": {
             "hashes": [
@@ -917,10 +954,10 @@
         },
         "google-cloud-storage": {
             "hashes": [
-                "sha256:3ca40dcf31b86bab3355ee2bb4502aefd37b37798c6d0545cdc62ca99ea5d05a",
-                "sha256:a82b5d60a2ec366a14746f17910dd8348c36a4aeec36da10021307e1951fcc7f"
+                "sha256:13a6a820311662eb91a99810568c2bca5ddc7e44e2163fed4cb3f4d47da132cf",
+                "sha256:2e7e2435978bda1c209b70a9a00b8cbc53c3b00d6f09eb2c991ebba857babf24"
             ],
-            "version": "==1.19.1"
+            "version": "==1.20.0"
         },
         "google-resumable-media": {
             "hashes": [
@@ -954,29 +991,28 @@
                 "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
                 "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
             ],
-            "markers": "python_version < '3.8'",
             "version": "==0.23"
         },
         "joblib": {
             "hashes": [
-                "sha256:21e0c34a69ad7fde4f2b1f3402290e9ec46f545f15f1541c582edfe05d87b63a",
-                "sha256:315d6b19643ec4afd4c41c671f9f2d65ea9d787da093487a81ead7b0bac94524"
+                "sha256:006108c7576b3eb6c5b27761ddbf188eb6e6347696325ab2027ea1ee9a4b922d",
+                "sha256:6fcc57aacb4e89451fd449e9412687c51817c3f48662c3d8f38ba3f8a0a193ff"
             ],
-            "version": "==0.13.2"
+            "version": "==0.14.0"
         },
         "jsonschema": {
             "hashes": [
-                "sha256:5f9c0a719ca2ce14c5de2fd350a64fd2d13e8539db29836a86adc990bb1a068f",
-                "sha256:8d4a2b7b6c2237e0199c8ea1a6d3e05bf118e289ae2b9d7ba444182a2959560d"
+                "sha256:2fa0684276b6333ff3c0b1b27081f4b2305f0a36cf702a23db50edb141893c3f",
+                "sha256:94c0a13b4a0616458b42529091624e66700a17f847453e52279e35509a5b7631"
             ],
-            "version": "==3.0.2"
+            "version": "==3.1.1"
         },
         "kfp": {
             "hashes": [
-                "sha256:8db0639a7ac0ce1a998f7c9244bb1de187047f36abfaf1328056f5191ad3a101"
+                "sha256:d58fe7ff00328cac0793211f8852b0909249fb53415e1b38a7ff45e73572eade"
             ],
             "index": "pypi",
-            "version": "==0.1.30"
+            "version": "==0.1.32"
         },
         "kfp-server-api": {
             "hashes": [
@@ -1004,8 +1040,10 @@
                 "sha256:0cc7ca04e575ba34fea7cfcd76039f55def570e6950e4155a4174368142c8e1b",
                 "sha256:187794cd1eb73acccd528247e3565f6760bd842d7dc299241f830024a7dd5610",
                 "sha256:1904b7cb65342d0998b75908304a03cb004c63ef31e16c8c43fee6b989d7f0d7",
+                "sha256:229a0ccdc39e9b6c6d1033cd8aecd9c296823b6c87f0de3943c59b8bc7c64bee",
                 "sha256:24149a75643aeaa81ece4259084d11b792308a6cf74e796cbb35def94c89a25a",
                 "sha256:30b88c47e0cdb6062daed88ca283b0d84fa0d2ad6c273aa0788152a1c643e408",
+                "sha256:32fea0ea3cd1ef820286863a6202dcfd62a539b8ec3edcbdff76068a8c2cc6ce",
                 "sha256:355f7fd0f90134229eaeefaee3cf42e0afc8518e8f3cd4b25f541a7104dcb8f9",
                 "sha256:4abdb88a9b67e64810fb54b0c24a1fd76b12297b4f7a1467d85a14dd8367191a",
                 "sha256:757bd71a9b89e4f1db0622af4436d403e742506dbea978eba566815dc65ec895",
@@ -1013,6 +1051,7 @@
                 "sha256:774f5edc3475917cd95fe593e625d23d8580f9b48b570d8853d06cac171cd170",
                 "sha256:8a3ada8401736df2bf497f65589293a86c56e197a80ae7634ec2c3150a2f5082",
                 "sha256:a06efd0482a1942aad209a6c18321b5e22d64eb531ea20af138b28172d8f35ba",
+                "sha256:b24afc52e18dccc8c175de07c1d680bdf315844566f4952b5bedb908894bec79",
                 "sha256:b8b4bd3dafc7b92608ae5462add1c8cc881851c2d4f5d8977fdea5b081d17f21",
                 "sha256:c6e5024fc0cdf7f83b6624850309ddd7e06c48a75fa0d1c5173de4d93300eb19",
                 "sha256:db7ff14abc73577b0bcbcf73ecff97d3580ecaa0fc8724babce21fdf3fe08ef6",
@@ -1024,24 +1063,29 @@
         },
         "numpy": {
             "hashes": [
-                "sha256:05dbfe72684cc14b92568de1bc1f41e5f62b00f714afc9adee42f6311738091f",
-                "sha256:0d82cb7271a577529d07bbb05cb58675f2deb09772175fab96dc8de025d8ac05",
-                "sha256:10132aa1fef99adc85a905d82e8497a580f83739837d7cbd234649f2e9b9dc58",
-                "sha256:12322df2e21f033a60c80319c25011194cd2a21294cc66fee0908aeae2c27832",
-                "sha256:16f19b3aa775dddc9814e02a46b8e6ae6a54ed8cf143962b4e53f0471dbd7b16",
-                "sha256:3d0b0989dd2d066db006158de7220802899a1e5c8cf622abe2d0bd158fd01c2c",
-                "sha256:438a3f0e7b681642898fd7993d38e2bf140a2d1eafaf3e89bb626db7f50db355",
-                "sha256:5fd214f482ab53f2cea57414c5fb3e58895b17df6e6f5bca5be6a0bb6aea23bb",
-                "sha256:73615d3edc84dd7c4aeb212fa3748fb83217e00d201875a47327f55363cef2df",
-                "sha256:7bd355ad7496f4ce1d235e9814ec81ee3d28308d591c067ce92e49f745ba2c2f",
-                "sha256:7d077f2976b8f3de08a0dcf5d72083f4af5411e8fddacd662aae27baa2601196",
-                "sha256:a4092682778dc48093e8bda8d26ee8360153e2047826f95a3f5eae09f0ae3abf",
-                "sha256:b458de8624c9f6034af492372eb2fee41a8e605f03f4732f43fc099e227858b2",
-                "sha256:e70fc8ff03a961f13363c2c95ef8285e0cf6a720f8271836f852cc0fa64e97c8",
-                "sha256:ee8e9d7cad5fe6dde50ede0d2e978d81eafeaa6233fb0b8719f60214cf226578",
-                "sha256:f4a4f6aba148858a5a5d546a99280f71f5ee6ec8182a7d195af1a914195b21a2"
-            ],
-            "version": "==1.17.2"
+                "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5",
+                "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e",
+                "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b",
+                "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7",
+                "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c",
+                "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418",
+                "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39",
+                "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26",
+                "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2",
+                "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e",
+                "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c",
+                "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4",
+                "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98",
+                "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1",
+                "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e",
+                "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8",
+                "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462",
+                "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2",
+                "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9",
+                "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6",
+                "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"
+            ],
+            "version": "==1.17.3"
         },
         "oauthlib": {
             "hashes": [
@@ -1073,24 +1117,24 @@
         },
         "protobuf": {
             "hashes": [
-                "sha256:00a1b0b352dc7c809749526d1688a64b62ea400c5b05416f93cfb1b11a036295",
-                "sha256:01acbca2d2c8c3f7f235f1842440adbe01bbc379fa1cbdd80753801432b3fae9",
-                "sha256:0a795bca65987b62d6b8a2d934aa317fd1a4d06a6dd4df36312f5b0ade44a8d9",
-                "sha256:0ec035114213b6d6e7713987a759d762dd94e9f82284515b3b7331f34bfaec7f",
-                "sha256:31b18e1434b4907cb0113e7a372cd4d92c047ce7ba0fa7ea66a404d6388ed2c1",
-                "sha256:32a3abf79b0bef073c70656e86d5bd68a28a1fbb138429912c4fc07b9d426b07",
-                "sha256:55f85b7808766e5e3f526818f5e2aeb5ba2edcc45bcccede46a3ccc19b569cb0",
-                "sha256:64ab9bc971989cbdd648c102a96253fdf0202b0c38f15bd34759a8707bdd5f64",
-                "sha256:64cf847e843a465b6c1ba90fb6c7f7844d54dbe9eb731e86a60981d03f5b2e6e",
-                "sha256:917c8662b585470e8fd42f052661fc66d59fccaae450a60044307dcbf82a3335",
-                "sha256:afed9003d7f2be2c3df20f64220c30faec441073731511728a2cb4cab4cd46a6",
-                "sha256:bf8e05d638b585d1752c5a84247134a0350d3a8b73d3632489a014a9f6f1e758",
-                "sha256:d831b047bd69becaf64019a47179eb22118a50dd008340655266a906c69c6417",
-                "sha256:de2760583ed28749ff885789c1cbc6c9c06d6de92fc825740ab99deb2f25ea4d",
-                "sha256:eabc4cf1bc19689af8022ba52fd668564a8d96e0d08f3b4732d26a64255216a4",
-                "sha256:fcff6086c86fb1628d94ea455c7b9de898afc50378042927a59df8065a79a549"
-            ],
-            "version": "==3.9.1"
+                "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f",
+                "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77",
+                "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657",
+                "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896",
+                "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf",
+                "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6",
+                "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b",
+                "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300",
+                "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a",
+                "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789",
+                "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe",
+                "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc",
+                "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1",
+                "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe",
+                "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09",
+                "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de"
+            ],
+            "version": "==3.10.0"
         },
         "psutil": {
             "hashes": [
@@ -1122,10 +1166,10 @@
         },
         "pyasn1-modules": {
             "hashes": [
-                "sha256:43c17a83c155229839cc5c6b868e8d0c6041dba149789b6d6e28801c64821722",
-                "sha256:e30199a9d221f1b26c885ff3d87fd08694dbbe18ed0e8e405a2a7126d30ce4c0"
+                "sha256:0c35a52e00b672f832e5846826f1fb7507907f7d52fba6faa9e3c4cbe874fe4b",
+                "sha256:b6ada4f840fe51abf5a6bd545b45bf537bea62221fa0dde2e8a553ed9f06a4e3"
             ],
-            "version": "==0.2.6"
+            "version": "==0.2.7"
         },
         "pycparser": {
             "hashes": [
@@ -1162,11 +1206,11 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:813b99704b22c7d377bbd756ebe56c35252bb710937b46f207100e843440b3c2",
-                "sha256:cc6620b96bc667a0c8d4fa592a8c9c94178a1bd6cc799dbb057dfd9286d31a31"
+                "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
+                "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
             ],
             "index": "pypi",
-            "version": "==5.1.3"
+            "version": "==5.2.1"
         },
         "python-dateutil": {
             "hashes": [
@@ -1178,10 +1222,10 @@
         },
         "pytz": {
             "hashes": [
-                "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
-                "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
+                "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
+                "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
             ],
-            "version": "==2019.2"
+            "version": "==2019.3"
         },
         "pyyaml": {
             "hashes": [
@@ -1303,17 +1347,17 @@
         },
         "tabulate": {
             "hashes": [
-                "sha256:8af07a39377cee1103a5c8b3330a421c2d99b9141e9cc5ddd2e3263fea416943"
+                "sha256:d0097023658d4dea848d6ae73af84532d1e86617ac0925d1adf1dd903985dac3"
             ],
             "index": "pypi",
-            "version": "==0.8.3"
+            "version": "==0.8.5"
         },
         "tblib": {
             "hashes": [
-                "sha256:49188d1ed69938811e654a8f6e6a3cfca8a578d8fa95318d8a9861c7f4fccd19",
-                "sha256:bd1ad564564a158ff62c290687f3db446038f9ac11a0bf6892712e3601af3bcd"
+                "sha256:1735ff8fd6217446384b5afabead3b142cf1a52d242cfe6cab4240029d6d131a",
+                "sha256:c1c8f4edf820a8703d64838804ed3bb37ee35722231c26760cef292ec9d154e4"
             ],
-            "version": "==1.4.0"
+            "version": "==1.5.0"
         },
         "toolz": {
             "hashes": [
@@ -1342,19 +1386,19 @@
         },
         "twine": {
             "hashes": [
-                "sha256:630fadd6e342e725930be6c696537e3f9ccc54331742b16245dab292a17d0460",
-                "sha256:a3d22aab467b4682a22de4a422632e79d07eebd07ff2a7079effb13f8a693787"
+                "sha256:5319dd3e02ac73fcddcd94f035b9631589ab5d23e1f4699d57365199d85261e1",
+                "sha256:9fe7091715c7576df166df8ef6654e61bada39571783f2fd415bdcba867c6993"
             ],
             "index": "pypi",
-            "version": "==1.15.0"
+            "version": "==2.0.0"
         },
         "urllib3": {
             "hashes": [
-                "sha256:2f3eadfea5d92bc7899e75b5968410b749a054b492d5a6379c1344a1481bc2cb",
-                "sha256:9c6c593cb28f52075016307fc26b0a0f8e82bc7d1ff19aaaa959b91710a56c47"
+                "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
+                "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
             ],
             "markers": "python_version >= '3.4'",
-            "version": "==1.25.5"
+            "version": "==1.25.6"
         },
         "wcwidth": {
             "hashes": [
diff --git a/examples/mlrun_sparkk8s.ipynb b/examples/mlrun_sparkk8s.ipynb
new file mode 100644
index 000000000..88a0e1aff
--- /dev/null
+++ b/examples/mlrun_sparkk8s.ipynb
@@ -0,0 +1,156 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Execute pyspak script using Spark K8s\n",
+    "This example reads from a MySQL table and writes to an Iguazio KV tables"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "!pip install git+https://github.com/mlrun/mlrun\n",
+    "!pip install --upgrade nuclio-jupyter\n",
+    "!pip install kfp"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from mlrun import new_function\n",
+    "import os"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Run script localy (Notebook container)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "user = os.getenv('V3IO_USERNAME','admin')\n",
+    "working_dir  = os.getcwd()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fn = new_function(command='local://'+ working_dir + '/spark-jdbc.py')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "list_run = fn.run()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Run script on Spark K8S"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Script location.\n",
+    "\n",
+    "Change this location where you cloned mlrun.\n",
+    "\n",
+    "path should be under to /v3io/<container>"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "script_v3io_path = '/v3io/users/admin/sparkk8s'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fn = new_function(command='spark://urihoenig/spark-app-25:b2782#local://'+ script_v3io_path +'/spark-jdbc.py')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fn.with_limits(mem=\"512m\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fn.spec.wait_for_completion = 1\n",
+    "fn.with_requests(cpu=1)\n",
+    "fn.with_igz_spark('2.5_b2782_20190922162238')\n",
+    "#sj.with_igz_spark()\n",
+    "fn.spec.deps['jars'] += ['/spark/3rd_party/mysql-connector-java-8.0.13.jar']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sr = fn.run()"
+   ]
+  }
+ ],
+ "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.6.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/examples/spark-jdbc.py b/examples/spark-jdbc.py
new file mode 100644
index 000000000..e4d996e3c
--- /dev/null
+++ b/examples/spark-jdbc.py
@@ -0,0 +1,22 @@
+# Pyspark example called by mlrun_sparkk8s.ipynb
+import os
+# Iguazio env
+V3IO_USER = os.getenv('V3IO_USERNAME')
+V3IO_HOME = os.getenv('V3IO_HOME')
+V3IO_HOME_URL = os.getenv('V3IO_HOME_URL')
+
+os.environ["PYSPARK_SUBMIT_ARGS"] = "--packages mysql:mysql-connector-java:5.1.39 pyspark-shell"
+
+from pyspark.sql import SparkSession
+
+
+spark = SparkSession.builder.    appName("Spark JDBC to Databases - ipynb").getOrCreate()
+
+
+
+#Loading data from a JDBC source
+dfMySQL = spark.read     .format("jdbc")     .option("url", "jdbc:mysql://mysql-rfam-public.ebi.ac.uk:4497/Rfam")     .option("dbtable", "Rfam.family")     .option("user", "rfamro")     .option("password", "")     .option("driver", "com.mysql.jdbc.Driver")     .load()
+
+
+dfMySQL.write.format("io.iguaz.v3io.spark.sql.kv").mode("overwrite").option("key", "rfam_id").save("v3io://users/admin/frommysql")
+
diff --git a/mlrun/db/base.py b/mlrun/db/base.py
index c1ea83750..be3ac8791 100644
--- a/mlrun/db/base.py
+++ b/mlrun/db/base.py
@@ -89,3 +89,15 @@ class RunDBInterface(ABC):
 
     def read_metric(self, keys, project='', query=''):
         warnings.warn('store_metric not implemented yet')
+
+    @abstractmethod
+    def store_function(self, func, name, project='', tag=''):
+        pass
+
+    @abstractmethod
+    def get_function(self, name, project='', tag=''):
+        pass
+
+    @abstractmethod
+    def list_functions(self, name, project='', tag='', labels=None):
+        pass
diff --git a/mlrun/db/filedb.py b/mlrun/db/filedb.py
index 665c51d22..bdcd9ed58 100644
--- a/mlrun/db/filedb.py
+++ b/mlrun/db/filedb.py
@@ -21,9 +21,14 @@ from datetime import datetime, timedelta
 
 from ..utils import get_in, match_labels, dict_to_yaml, update_in, dict_to_json
 from ..datastore import StoreManager
-from ..render import run_to_html
 from .base import RunDBError, RunDBInterface
-from ..lists import RunList, ArtifactList
+from ..lists import RunList, ArtifactList, FunctionList
+from ..utils import logger
+
+run_logs = 'runs'
+artifacts_dir = 'artifacts'
+functions_dir = 'functions'
+_missing = object()
 
 
 class FileRunDB(RunDBInterface):
@@ -41,11 +46,12 @@ class FileRunDB(RunDBInterface):
         return self
 
     def store_log(self, uid, project='', body=None, append=True):
-        filepath = self._filepath('runs', project, uid, '') + '.log'
-        self._datastore.put(filepath, body, append=append)
+        filepath = self._filepath(run_logs, project, uid, '') + '.log'
+        # TODO: handle append
+        self._datastore.put(filepath, body)
 
     def get_log(self, uid, project='', offset=0):
-        filepath = self._filepath('runs', project, uid, '') + '.log'
+        filepath = self._filepath(run_logs, project, uid, '') + '.log'
         if pathlib.Path(filepath).is_file():
             with open(filepath, 'rb') as fp:
                 if offset:
@@ -54,11 +60,8 @@ class FileRunDB(RunDBInterface):
         return None
 
     def store_run(self, struct, uid, project='', commit=False):
-        if self.format == '.yaml':
-            data = dict_to_yaml(struct)
-        else:
-            data = dict_to_json(struct)
-        filepath = self._filepath('runs', project, uid, '') + self.format
+        data = self._dumps(struct)
+        filepath = self._filepath(run_logs, project, uid, '') + self.format
         self._datastore.put(filepath, data)
 
     def update_run(self, updates: dict, uid, project=''):
@@ -69,21 +72,21 @@ class FileRunDB(RunDBInterface):
         self.store_run(run, uid, project, True)
 
     def read_run(self, uid, project=''):
-        filepath = self._filepath('runs', project, uid, '') + self.format
+        filepath = self._filepath(run_logs, project, uid, '') + self.format
         data = self._datastore.get(filepath)
         return self._loads(data)
 
     def list_runs(self, name='', uid=None, project='', labels=[],
                   state='', sort=True, last=30):
-        filepath = self._filepath('runs', project)
+        filepath = self._filepath(run_logs, project)
         results = RunList()
         if isinstance(labels, str):
             labels = labels.split(',')
         for run, _ in self._load_list(filepath, '*'):
-            if (name == '' or name in get_in(run, 'metadata.name', ''))\
-                    and match_labels(get_in(run, 'metadata.labels', {}), labels)\
-                    and (state == '' or get_in(run, 'status.state', '') == state)\
-                    and (not uid or uid == get_in(run, 'metadata.uid', '')):
+            if match_value(name, run, 'metadata.name') and \
+               match_labels(get_in(run, 'metadata.labels', {}), labels) and \
+               match_value(state, run, 'status.state') and \
+               match_value(uid, run, 'metadata.uid'):
                 results.append(run)
 
         if sort or last:
@@ -94,14 +97,15 @@ class FileRunDB(RunDBInterface):
         return results
 
     def del_run(self, uid, project=''):
-        filepath = self._filepath('runs', project, uid, '') + self.format
+        filepath = self._filepath(run_logs, project, uid, '') + self.format
         self._safe_del(filepath)
 
     def del_runs(self, name='', project='', labels=[], state='', days_ago=0):
         if not name and not state and not days_ago:
-            raise RunDBError('filter is too wide, select name and/or state and/or days_ago')
+            raise RunDBError(
+                'filter is too wide, select name and/or state and/or days_ago')
 
-        filepath = self._filepath('runs', project)
+        filepath = self._filepath(run_logs, project)
         if isinstance(labels, str):
             labels = labels.split(',')
 
@@ -113,33 +117,33 @@ class FileRunDB(RunDBInterface):
                                      '%Y-%m-%d %H:%M:%S.%f') < days_ago
 
         for run, p in self._load_list(filepath, '*'):
-            if (name == '' or name == get_in(run, 'metadata.name', ''))\
-                    and match_labels(get_in(run, 'metadata.labels', {}), labels)\
-                    and (state == '' or get_in(run, 'status.state', '') == state)\
-                    and (not days_ago or date_before(run)):
-
+            if match_value(name, run, 'metadata.name') and \
+               match_labels(get_in(run, 'metadata.labels', {}), labels) and \
+               match_value(state, run, 'status.state') and \
+               (not days_ago or date_before(run)):
                 self._safe_del(p)
 
     def store_artifact(self, key, artifact, uid, tag='', project=''):
         artifact['updated'] = time.time()
-        if self.format == '.yaml':
-            data = dict_to_yaml(artifact)
-        else:
-            data = dict_to_json(artifact)
-        filepath = self._filepath('artifacts', project, key, uid) + self.format
+        data = self._dumps(artifact)
+        filepath = self._filepath(
+            artifacts_dir, project, key, uid) + self.format
         self._datastore.put(filepath, data)
-        filepath = self._filepath('artifacts', project, key, tag or 'latest') + self.format
+        filepath = self._filepath(
+            artifacts_dir, project, key, tag or 'latest') + self.format
         self._datastore.put(filepath, data)
 
     def read_artifact(self, key, tag='', project=''):
-        filepath = self._filepath('artifacts', project, key, tag) + self.format
+        filepath = self._filepath(
+            artifacts_dir, project, key, tag) + self.format
         data = self._datastore.get(filepath)
         return self._loads(data)
 
     def list_artifacts(self, name='', project='', tag='', labels=[]):
         tag = tag or 'latest'
-        print(f'reading artifacts in {project} name/mask: {name} tag: {tag} ...')
-        filepath = self._filepath('artifacts', project, tag=tag)
+        logger.info(
+            f'reading artifacts in {project} name/mask: {name} tag: {tag} ...')
+        filepath = self._filepath(artifacts_dir, project, tag=tag)
         results = ArtifactList()
         results.tag = tag
         if isinstance(labels, str):
@@ -160,12 +164,13 @@ class FileRunDB(RunDBInterface):
         return results
 
     def del_artifact(self, key, tag='', project=''):
-        filepath = self._filepath('artifacts', project, key, tag) + self.format
+        filepath = self._filepath(
+            artifacts_dir, project, key, tag) + self.format
         self._safe_del(filepath)
 
     def del_artifacts(self, name='', project='', tag='', labels=[]):
         tag = tag or 'latest'
-        filepath = self._filepath('artifacts', project, tag=tag)
+        filepath = self._filepath(artifacts_dir, project, tag=tag)
 
         if isinstance(labels, str):
             labels = labels.split(',')
@@ -182,21 +187,70 @@ class FileRunDB(RunDBInterface):
 
                 self._safe_del(p)
 
+    def store_function(self, func, name, project='', tag=''):
+        data = self._dumps(func)
+        filepath = self._filepath(functions_dir, project, name) + self.format
+        self._datastore.put(filepath, data)
+        filepath = self._filepath(
+            functions_dir, project, name, tag or 'latest') + self.format
+        self._datastore.put(filepath, data)
+
+    def get_function(self, name, project='', tag=''):
+        filepath = self._filepath(
+            functions_dir, project, name, tag) + self.format
+        data = self._datastore.get(filepath)
+        return self._loads(data)
+
+    def list_functions(self, name, project='', tag='', labels=None):
+        tag = tag or 'latest'
+        labels = labels or []
+        logger.info(
+            f'reading functions in {project} name/mask: {name} tag: {tag} ...')
+        filepath = self._filepath(functions_dir, project, tag=tag)
+        results = FunctionList(tag)
+        if isinstance(labels, str):
+            labels = labels.split(',')
+        if tag == '*':
+            mask = '**/*' + name
+            if name:
+                mask += '*'
+        else:
+            mask = '**/*'
+        for func, p in self._load_list(filepath, mask):
+            if (name == '' or name in get_in(func, 'name', ''))\
+                    and match_labels(get_in(func, 'labels', {}), labels):
+                if 'artifacts/latest' in p:
+                    func['tree'] = 'latest'
+                results.append(func)
+
+        return results
+
     def _filepath(self, table, project, key='', tag=''):
         if tag == '*':
             tag = ''
         if tag:
             key = '/' + key
         if project:
-            return path.join(self.dirpath, '{}/{}/{}{}'.format(table, project, tag, key))
+            return path.join(self.dirpath, '{}/{}/{}{}'.format(
+                table, project, tag, key))
         else:
             return path.join(self.dirpath, '{}/{}{}'.format(table, tag, key))
 
+    _encodings = {
+        '.yaml': ('to_yaml', dict_to_yaml),
+        '.json': ('to_json', dict_to_json),
+    }
+
     def _dumps(self, obj):
-        if self.format == '.yaml':
-            return obj.to_yaml()
-        else:
-            return obj.to_json()
+        meth_name, enc_fn = self._encodings.get(self.format, (None, None))
+        if meth_name is None:
+            raise ValueError(f'unsupported format - {self.format}')
+
+        meth = getattr(obj, meth_name, None)
+        if meth:
+            return meth()
+
+        return enc_fn(obj)
 
     def _loads(self, data):
         if self.format == '.yaml':
@@ -218,3 +272,9 @@ class FileRunDB(RunDBInterface):
             remove(filepath)
         else:
             raise RunDBError(f'run file is not found or valid ({filepath})')
+
+
+def match_value(value, obj, key):
+    if not value:
+        return True
+    return get_in(obj, key, _missing) == value
diff --git a/mlrun/db/httpd.py b/mlrun/db/httpd.py
index ac6e66976..3970628b7 100644
--- a/mlrun/db/httpd.py
+++ b/mlrun/db/httpd.py
@@ -294,13 +294,54 @@ def del_artifacts():
     _file_db.del_artifacts(name, project, tag, labels)
     return jsonify(ok=True)
 
+# curl -d@/path/to/func.json http://localhost:8080/func/prj/7?tag=0.3.2
+@app.route('/func/<project>/<name>', methods=['POST'])
+@catch_err
+def store_function(project, name):
+    try:
+        data = request.get_json(force=True)
+    except ValueError:
+        return json_error(HTTPStatus.BAD_REQUEST, reason='bad JSON body')
+
+    tag = request.args.get('tag', '')
+
+    _file_db.store_function(data, name, project, tag)
+    return jsonify(ok=True)
+
+
+# curl http://localhost:8080/log/prj/7?tag=0.2.3
+@app.route('/func/<project>/<name>', methods=['GET'])
+@catch_err
+def get_function(project, name):
+    tag = request.args.get('tag', '')
+    func = _file_db.get_function(name, project, tag)
+    return jsonify(ok=True, func=func)
+
+
+# curl http://localhost:8080/funcs?project=p1&name=x&label=l1&label=l2
+@app.route('/funcs', methods=['GET'])
+@catch_err
+def list_functions():
+    name = request.args.get('name', '')
+    project = request.args.get('project', 'default')
+    tag = request.args.get('tag', '')
+    labels = request.args.getlist('label')
+
+    out = _file_db.list_functions(name, project, tag, labels)
+    return jsonify(
+        ok=True,
+        funcs=list(out),
+        tag=out.tag,
+    )
+
 
 @app.route('/healthz', methods=['GET'])
 def health():
     return 'OK\n'
 
 
-def main():
+@app.before_first_request
+def init_app():
     global _file_db
 
     from mlrun.config import config
@@ -308,6 +349,10 @@ def main():
     logger.info('configuration dump\n%s', config.dump_yaml())
     _file_db = FileRunDB(config.httpdb.dirpath, '.yaml')
     _file_db.connect()
+
+
+# Don't remove this function, it's an entry point in setup.py
+def main():
     app.run(
         host='0.0.0.0',
         port=config.httpdb.port,
diff --git a/mlrun/db/httpdb.py b/mlrun/db/httpdb.py
index 75d6210b3..8bc7b349f 100644
--- a/mlrun/db/httpdb.py
+++ b/mlrun/db/httpdb.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
+
 import requests
 
 from ..utils import dict_to_json
@@ -197,6 +199,34 @@ class HTTPRunDB(RunDBInterface):
         error = 'del artifacts'
         self._api_call('DELETE', 'artifacts', error, params=params)
 
+    def store_function(self, func, name, project='', tag=''):
+        params = {'tag': tag}
+        project = project or default_project
+        path = self._path_of('func', project, name)
+
+        error = f'store function {project}/{name}'
+        self._api_call(
+            'POST', path, error, params=params, body=json.dumps(func))
+
+    def get_function(self, name, project='', tag=''):
+        params = {'tag': tag}
+        project = project or default_project
+        path = self._path_of('func', project, name)
+        error = f'get function {project}/{name}'
+        resp = self._api_call('GET', path, error, params=params)
+        return resp.json()['func']
+
+    def list_functions(self, name, project='', tag='', labels=None):
+        params = {
+            'project': project or default_project,
+            'name': name,
+            'tag': tag,
+            'label': labels or [],
+        }
+        error = f'list functions'
+        resp = self._api_call('GET', 'funcs', error, params=params)
+        return resp.json()['funcs']
+
 
 def _as_json(obj):
     fn = getattr(obj, 'to_json', None)
diff --git a/mlrun/lists.py b/mlrun/lists.py
index 138bd8f37..c50904685 100644
--- a/mlrun/lists.py
+++ b/mlrun/lists.py
@@ -80,3 +80,9 @@ class ArtifactList(list):
         html = artifacts_to_html(df, display, classes=classes)
         if not display:
             return html
+
+
+class FunctionList(list):
+    def __init__(self, tag):
+        self.tag = tag
+        # TODO
diff --git a/mlrun/runtimes/sparkjob.py b/mlrun/runtimes/sparkjob.py
index 996625dda..8e8e2e0a0 100644
--- a/mlrun/runtimes/sparkjob.py
+++ b/mlrun/runtimes/sparkjob.py
@@ -11,11 +11,12 @@
 # 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.
-
+import json
 from pprint import pprint
 import yaml
 import time
 from copy import deepcopy
+from .base import RunError
 from .kubejob import KubejobRuntime, KubejobSpec
 from ..model import ModelObj
 from ..utils import logger, get_in
@@ -97,6 +98,9 @@ class SparkJobSpec(KubejobSpec):
         self.spark_version = spark_version
         self.restart_policy = restart_policy
         self.deps = deps
+        self.wait_for_completion = 0   # Seconds to wait for job to complete 0 = no wait
+        self.job_check_interval = 10   # Check job status every N seconds (Only relevant if wait_for completion is set)
+        self.wait_timeout = 0          # Wait N seconds before killing the job
 
 class SparkRuntime(KubejobRuntime):
     group = 'sparkoperator.k8s.io'
@@ -108,7 +112,6 @@ class SparkRuntime(KubejobRuntime):
     def _run(self, runobj: RunObject, execution):
         job = deepcopy(_sparkjob_template)
         meta = self._get_meta(runobj, True)
-
         pod_labels = deepcopy(meta.labels)
         pod_labels['mlrun/job'] = meta.name
         update_in(job, 'metadata', meta.to_dict())
@@ -134,7 +137,35 @@ class SparkRuntime(KubejobRuntime):
         if self.spec.command:
             update_in(job, 'spec.mainApplicationFile', self.spec.command)
         update_in(job, 'spec.args', self.spec.args)
-        self._submit_job(job, meta.namespace)
+        resp = self._submit_job(job, meta.namespace)
+        name = get_in(resp, 'metadata.name', 'unknown')
+        job_response = resp
+        # If wait is set
+        if self.spec.wait_for_completion > 0:
+            import time
+
+            running = "STARTING"
+            job_name = get_in(job_response, 'metadata.name', 'unknown')
+            logger.info('Waiting for application to start')
+            while running not in ["RUNNING", "COMPLETED", "FAILED"]:
+                result = self._get_job_status(namespace=meta.namespace, job_name=job_name)
+                if 'status' in result:
+                    running = result['status']['applicationState']['state']
+                time.sleep(self.spec.job_check_interval)
+
+            logger.info('Application status:' + running)
+            if running == "FAILED":
+                raise RunError('Failed to execute application')
+
+            logger.info('Waiting for application to complete')
+            while running not in ["COMPLETED", "FAILED"]:
+                result = self._get_job_status(namespace=meta.namespace, job_name=job_name)
+                running = result['status']['applicationState']['state']
+                time.sleep(self.spec.job_check_interval)
+
+            if running == "FAILED":
+                raise RunError('Execution failed check the pod logs')
+            logger.info('Application completed:')
 
     def _submit_job(self, job, namespace=None):
         k8s = self._get_k8s()
@@ -149,6 +180,17 @@ class SparkRuntime(KubejobRuntime):
         except client.rest.ApiException as e:
             logger.error("Exception when creating SparkJob: %s" % e)
 
+    def _get_job_status(self, namespace=None, job_name=None):
+        k8s = self._get_k8s()
+        namespace = k8s.ns(namespace)
+        try:
+            resp = k8s.crdapi.get_namespaced_custom_object(
+                SparkRuntime.group, SparkRuntime.version, namespace, SparkRuntime.plural, job_name)
+        except client.rest.ApiException as e:
+            print("Exception when reading SparkJob: %s" % e)
+        return resp
+
+
     def _update_igz_jars(self, igz_version, deps=igz_deps):
         if not self.spec.deps:
             self.spec.deps = {}


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index b84e6cc37..f5ba77f24 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,10 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from pathlib import Path
 import shutil
-from copy import deepcopy
+from http import HTTPStatus
 from os import environ
+from pathlib import Path
+from time import monotonic, sleep
+from urllib.request import URLError, urlopen
+
 
 here = Path(__file__).absolute().parent
 results = here / 'test_results'
@@ -32,9 +35,8 @@ environ['MLRUN_DBPATH'] = rundb_path
 Path(f'{results}/kfp').mkdir(parents=True, exist_ok=True)
 environ['KFPMETA_OUT_DIR'] = f'{results}/kfp/'
 
-
-from mlrun.utils import update_in
-from mlrun import RunTemplate, RunObject
+# This must be *after* environment changes above
+from mlrun import RunObject, RunTemplate  # noqa
 
 
 def tag_test(spec: RunTemplate, name) -> RunTemplate:
@@ -50,4 +52,18 @@ def has_secrets():
 
 def verify_state(result: RunObject):
     state = result.status.state
-    assert state == 'completed', 'wrong state ({}) {}'.format(state, result.status.error)
+    assert state == 'completed', \
+        'wrong state ({}) {}'.format(state, result.status.error)
+
+
+def wait_for_server(url, timeout_sec):
+    start = monotonic()
+    while monotonic() - start <= timeout_sec:
+        try:
+            with urlopen(url) as resp:
+                if resp.status == HTTPStatus.OK:
+                    return True
+        except (URLError, ConnectionError):
+            pass
+        sleep(0.1)
+    return False
diff --git a/tests/test_docker.py b/tests/test_docker.py
new file mode 100644
index 000000000..1cc492e88
--- /dev/null
+++ b/tests/test_docker.py
@@ -0,0 +1,75 @@
+# Copyright 2018 Iguazio
+#
+# 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.
+
+from subprocess import PIPE, run
+from uuid import uuid4
+from sys import platform
+
+import pytest
+
+from conftest import here, wait_for_server
+
+prj_dir = here.parent
+
+
+def check_docker():
+    if not platform.startswith('linux'):
+        return False
+
+    with open('/proc/1/cgroup') as fp:
+        for line in fp:
+            if '/docker/' in line:
+                return True
+    return False
+
+
+in_docker = check_docker()
+
+
+@pytest.fixture
+def build_docker():
+    tag = f'mlrun/test-{uuid4().hex}'
+    cid = None
+
+    def build(dockerfile):
+        nonlocal cid
+
+        cmd = ['docker', 'build', '-f', dockerfile, '-t', tag, '.']
+        run(cmd, cwd=prj_dir, check=True)
+
+        port = 8080
+        cmd = ['docker', 'run', '--detach', '-p', f'{port}:{port}', tag]
+        out = run(cmd, stdout=PIPE, check=True)
+        cid = out.stdout.decode('utf-8').strip()
+
+        url = f'http://localhost:{port}/healthz'
+        timeout = 30
+        assert wait_for_server(url, timeout), \
+            f'server failed to start after {timeout} seconds, url={url}'
+        return cid
+
+    yield build
+
+    if cid is not None:
+        run(['docker', 'rm', '-f', cid])
+    run(['docker', 'rmi', '-f', tag])
+
+
+dockerfiles = ['Dockerfile.db', 'Dockerfile.db-gunicorn']
+
+
+@pytest.mark.skipif(in_docker, reason='in docker container')
+@pytest.mark.parametrize('dockerfile', dockerfiles)
+def test_docker(build_docker, dockerfile):
+    build_docker(dockerfile)
diff --git a/tests/test_filedb.py b/tests/test_filedb.py
new file mode 100644
index 000000000..6fc42910b
--- /dev/null
+++ b/tests/test_filedb.py
@@ -0,0 +1,33 @@
+from tempfile import mkdtemp
+
+import pytest
+
+from mlrun.db import FileRunDB
+
+
+@pytest.fixture
+def db():
+    path = mkdtemp(prefix='mlrun-test')
+    db = FileRunDB(dirpath=path)
+    db.connect()
+    return db
+
+
+def test_save_get_function(db: FileRunDB):
+    func, name, proj = {'x': 1, 'y': 2}, 'f1', 'p2'
+    db.store_function(func, name, proj)
+    db_func = db.get_function(name, proj)
+    assert db_func == func, 'wrong func'
+
+
+def test_list_fuctions(db: FileRunDB):
+    proj = 'p4'
+    count = 5
+    for i in range(count):
+        name = f'func{i}'
+        func = {'fid': i}
+        db.store_function(func, name, proj)
+    db.store_function({}, 'f2', 'p7')
+
+    out = db.list_functions('', proj)
+    assert len(out) == count, 'bad list'
diff --git a/tests/test_httpdb.py b/tests/test_httpdb.py
index 88b04ccd7..cd3b17264 100644
--- a/tests/test_httpdb.py
+++ b/tests/test_httpdb.py
@@ -19,15 +19,13 @@ from socket import socket
 from subprocess import Popen
 from sys import executable
 from tempfile import mkdtemp
-from time import monotonic, sleep
-from urllib.request import urlopen, URLError
-from http import HTTPStatus
 
 import pytest
 
 from mlrun.artifacts import Artifact
 from mlrun.db import HTTPRunDB, RunDBError
 from mlrun import RunObject
+from conftest import wait_for_server
 
 root = Path(__file__).absolute().parent.parent
 Server = namedtuple('Server', 'process url log_file conn')
@@ -39,19 +37,6 @@ def free_port():
         return sock.getsockname()[1]
 
 
-def wait_for_server(url, timeout_sec):
-    start = monotonic()
-    while monotonic() - start <= timeout_sec:
-        try:
-            with urlopen(url) as resp:
-                if resp.status == HTTPStatus.OK:
-                    return True
-        except URLError:
-            pass
-        sleep(0.1)
-    return False
-
-
 def start_server(dirpath, log_file, env_config):
     port = free_port()
     env = environ.copy()
@@ -209,3 +194,29 @@ def test_bearer_auth(create_server):
 
     db.token = token
     db.list_runs()
+
+
+def test_set_get_function(create_server):
+    server: Server = create_server()
+    db: HTTPRunDB = server.conn
+
+    func, name, proj = {'x': 1, 'y': 2}, 'f1', 'p2'
+    db.store_function(func, name, proj)
+    db_func = db.get_function(name, proj)
+    assert db_func == func, 'wrong func'
+
+
+def test_list_functions(create_server):
+    server: Server = create_server()
+    db: HTTPRunDB = server.conn
+
+    proj = 'p4'
+    count = 5
+    for i in range(count):
+        name = f'func{i}'
+        func = {'fid': i}
+        db.store_function(func, name, proj)
+    db.store_function({}, 'f2', 'p7')
+
+    out = db.list_functions('', proj)
+    assert len(out) == count, 'bad list'