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..e7942f9fa 100644
--- a/mlrun/db/httpd.py
+++ b/mlrun/db/httpd.py
@@ -294,6 +294,46 @@ 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():
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 = {}

Add functions to the database
We'd like to store functions in the database. We're going to store YAML/JSON.

@yaronha said:
- YAML, metadata spec, labels
- Key is  project + name + version tag
- put/get/list/delete
- search by project + name + labels

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..32daac4a3 100644
--- a/tests/test_httpdb.py
+++ b/tests/test_httpdb.py
@@ -209,3 +209,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'

pip install -e .[dev]
parse_log_pytest
requirements.txt
kfp kubernetes==10.0.0 pytest
apt-get update apt-get install -y gcc
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiosignal==1.3.2
anyio==4.9.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==3.0.0
async-timeout==5.0.1
attrs==25.3.0
beautifulsoup4==4.13.3
bleach==6.2.0
blinker==1.9.0
boto3==1.37.23
botocore==1.37.23
cachetools==5.5.2
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
comm==0.2.2
debugpy==1.8.13
decorator==5.2.1
defusedxml==0.7.1
docstring_parser==0.16
exceptiongroup==1.2.2
executing==2.2.0
fastjsonschema==2.21.1
Flask==3.1.0
fqdn==1.5.1
frozenlist==1.5.0
gitdb==4.0.12
GitPython==3.1.44
google-api-core==2.24.2
google-auth==2.38.0
google-cloud-core==2.4.3
google-cloud-storage==2.19.0
google-crc32c==1.7.1
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
idna==3.10
importlib_metadata==7.2.1
iniconfig==2.1.0
ipykernel==6.29.5
ipython==8.18.1
isoduration==20.11.0
itsdangerous==2.2.0
jedi==0.19.2
Jinja2==3.1.6
jmespath==1.0.1
jsonpointer==3.0.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
jupyter-events==0.12.0
jupyter_client==8.6.3
jupyter_core==5.7.2
jupyter_server==2.15.0
jupyter_server_terminals==0.5.3
jupyterlab_pygments==0.3.0
kfp==2.12.1
kfp-pipeline-spec==0.6.0
kfp-server-api==2.4.0
kubernetes==10.0.0
MarkupSafe==3.0.2
matplotlib-inline==0.1.7
mistune==3.1.3
-e git+https://github.com/mlrun/mlrun.git@eb907ee9dcf18e219b5f279b1dc8fe6674d52c57#egg=mlrun
multidict==6.2.0
nbclient==0.10.2
nbconvert==7.16.6
nbformat==5.10.4
nest-asyncio==1.6.0
nuclio-jupyter==0.11.1
nuclio_sdk==0.5.15
numpy==2.0.2
oauthlib==3.2.2
overrides==7.7.0
packaging==24.2
pandas==2.2.3
pandocfilters==1.5.1
parso==0.8.4
pexpect==4.9.0
platformdirs==4.3.7
pluggy==1.5.0
prometheus_client==0.21.1
prompt_toolkit==3.0.50
propcache==0.3.1
proto-plus==1.26.1
protobuf==4.25.6
psutil==7.0.0
ptyprocess==0.7.0
pure_eval==0.2.3
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
Pygments==2.19.1
pytest==8.3.5
python-dateutil==2.9.0.post0
python-json-logger==3.3.0
pytz==2025.2
PyYAML==6.0.2
pyzmq==26.3.0
referencing==0.36.2
requests==2.32.3
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.24.0
rsa==4.9
s3transfer==0.11.4
Send2Trash==1.8.3
six==1.17.0
smmap==5.0.2
sniffio==1.3.1
soupsieve==2.6
stack-data==0.6.3
tabulate==0.9.0
terminado==0.18.1
tinycss2==1.4.0
tomli==2.2.1
tornado==6.4.2
traitlets==5.14.3
types-python-dateutil==2.9.0.20241206
typing_extensions==4.13.0
tzdata==2025.2
uri-template==1.3.0
urllib3==1.26.20
wcwidth==0.2.13
webcolors==24.11.1
webencodings==0.5.1
websocket-client==1.8.0
Werkzeug==3.1.3
yarl==1.18.3
zipp==3.21.0

name: mlrun
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiohappyeyeballs==2.6.1
      - aiohttp==3.11.14
      - aiosignal==1.3.2
      - anyio==4.9.0
      - argon2-cffi==23.1.0
      - argon2-cffi-bindings==21.2.0
      - arrow==1.3.0
      - asttokens==3.0.0
      - async-timeout==5.0.1
      - attrs==25.3.0
      - beautifulsoup4==4.13.3
      - bleach==6.2.0
      - blinker==1.9.0
      - boto3==1.37.23
      - botocore==1.37.23
      - cachetools==5.5.2
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - comm==0.2.2
      - debugpy==1.8.13
      - decorator==5.2.1
      - defusedxml==0.7.1
      - docstring-parser==0.16
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - fastjsonschema==2.21.1
      - flask==3.1.0
      - fqdn==1.5.1
      - frozenlist==1.5.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - google-api-core==2.24.2
      - google-auth==2.38.0
      - google-cloud-core==2.4.3
      - google-cloud-storage==2.19.0
      - google-crc32c==1.7.1
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - idna==3.10
      - importlib-metadata==7.2.1
      - iniconfig==2.1.0
      - ipykernel==6.29.5
      - ipython==8.18.1
      - isoduration==20.11.0
      - itsdangerous==2.2.0
      - jedi==0.19.2
      - jinja2==3.1.6
      - jmespath==1.0.1
      - jsonpointer==3.0.0
      - jsonschema==4.23.0
      - jsonschema-specifications==2024.10.1
      - jupyter-client==8.6.3
      - jupyter-core==5.7.2
      - jupyter-events==0.12.0
      - jupyter-server==2.15.0
      - jupyter-server-terminals==0.5.3
      - jupyterlab-pygments==0.3.0
      - kfp==2.12.1
      - kfp-pipeline-spec==0.6.0
      - kfp-server-api==2.4.0
      - kubernetes==10.0.0
      - markupsafe==3.0.2
      - matplotlib-inline==0.1.7
      - mistune==3.1.3
      - multidict==6.2.0
      - nbclient==0.10.2
      - nbconvert==7.16.6
      - nbformat==5.10.4
      - nest-asyncio==1.6.0
      - nuclio-jupyter==0.11.1
      - nuclio-sdk==0.5.15
      - numpy==2.0.2
      - oauthlib==3.2.2
      - overrides==7.7.0
      - packaging==24.2
      - pandas==2.2.3
      - pandocfilters==1.5.1
      - parso==0.8.4
      - pexpect==4.9.0
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - prometheus-client==0.21.1
      - prompt-toolkit==3.0.50
      - propcache==0.3.1
      - proto-plus==1.26.1
      - protobuf==4.25.6
      - psutil==7.0.0
      - ptyprocess==0.7.0
      - pure-eval==0.2.3
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pycparser==2.22
      - pygments==2.19.1
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - python-json-logger==3.3.0
      - pytz==2025.2
      - pyyaml==6.0.2
      - pyzmq==26.3.0
      - referencing==0.36.2
      - requests==2.32.3
      - requests-oauthlib==2.0.0
      - requests-toolbelt==1.0.0
      - rfc3339-validator==0.1.4
      - rfc3986-validator==0.1.1
      - rpds-py==0.24.0
      - rsa==4.9
      - s3transfer==0.11.4
      - send2trash==1.8.3
      - six==1.17.0
      - smmap==5.0.2
      - sniffio==1.3.1
      - soupsieve==2.6
      - stack-data==0.6.3
      - tabulate==0.9.0
      - terminado==0.18.1
      - tinycss2==1.4.0
      - tomli==2.2.1
      - tornado==6.4.2
      - traitlets==5.14.3
      - types-python-dateutil==2.9.0.20241206
      - typing-extensions==4.13.0
      - tzdata==2025.2
      - uri-template==1.3.0
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webcolors==24.11.1
      - webencodings==0.5.1
      - websocket-client==1.8.0
      - werkzeug==3.1.3
      - yarl==1.18.3
      - zipp==3.21.0
prefix: /opt/conda/envs/mlrun

tests/test_filedb.py::test_save_get_function tests/test_filedb.py::test_list_fuctions tests/test_httpdb.py::test_set_get_function tests/test_httpdb.py::test_list_functions
tests/test_httpdb.py::test_log tests/test_httpdb.py::test_run tests/test_httpdb.py::test_runs tests/test_httpdb.py::test_artifact tests/test_httpdb.py::test_artifacts tests/test_httpdb.py::test_basic_auth tests/test_httpdb.py::test_bearer_auth
mlrun/db/httpdb.py mlrun/db/httpd.py mlrun/lists.py mlrun/db/filedb.py examples/mlrun_sparkk8s.ipynb mlrun/db/base.py examples/spark-jdbc.py mlrun/runtimes/sparkjob.py
mlrun/db/httpdb.py mlrun/db/httpd.py mlrun/lists.py mlrun/db/filedb.py examples/mlrun_sparkk8s.ipynb mlrun/db/base.py examples/spark-jdbc.py mlrun/runtimes/sparkjob.py