├── pics ├── splash.png └── language_lab.png ├── bin ├── build_serverless_datascience_base.sh ├── build_serverless_datascience_dev.sh ├── forward_ports.sh ├── build_serverless_datascience_func.sh ├── setup_user.sh ├── start_minikube.sh └── deploy_cluster_components.sh ├── docker ├── environment.yml ├── Dockerfile.dev ├── Dockerfile.func └── Dockerfile.base ├── yml ├── sample-user.yml ├── sample-admin-user.yml ├── jupyter.yml └── registry.yml ├── notebooks ├── notebook_out_test.ipynb ├── execute_notebook.py ├── notebook_svd_R.ipynb ├── notebook_cosine_similarity_scala.ipynb ├── notebook_mandelbrot_clojure.ipynb └── deploy_notebook_function.ipynb └── README.md /pics/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmrfrd/serverless_notebooks/HEAD/pics/splash.png -------------------------------------------------------------------------------- /pics/language_lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmrfrd/serverless_notebooks/HEAD/pics/language_lab.png -------------------------------------------------------------------------------- /bin/build_serverless_datascience_base.sh: -------------------------------------------------------------------------------- 1 | docker build -t serverless_datascience:base -f ds_dev/Dockerfile.base ds_dev/ 2 | -------------------------------------------------------------------------------- /bin/build_serverless_datascience_dev.sh: -------------------------------------------------------------------------------- 1 | docker build --pull=false -t serverless_datascience:dev -f ds_dev/Dockerfile.dev ds_dev/ 2 | -------------------------------------------------------------------------------- /bin/forward_ports.sh: -------------------------------------------------------------------------------- 1 | sh -c '(kubectl port-forward -n default svc/jupyter 9999:8888 &\ 2 | kubectl port-forward -n openfaas svc/gateway-external 8080:8080)' 3 | -------------------------------------------------------------------------------- /bin/build_serverless_datascience_func.sh: -------------------------------------------------------------------------------- 1 | docker build \ 2 | --pull=false \ 3 | -t serverless_datascience:func-string-test \ 4 | -f ds_dev/Dockerfile.func \ 5 | --build-arg notebook_dir=./notebooks/ \ 6 | --build-arg notebook=notebook_string_test.ipynb \ 7 | . 8 | -------------------------------------------------------------------------------- /bin/setup_user.sh: -------------------------------------------------------------------------------- 1 | ## Apply users 2 | echo "Applying user ..." 3 | kubectl apply -f ./sample-admin-user.yml 4 | 5 | ## Get user token secret 6 | echo "Getting user token secret ..." 7 | kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}') -o yml 8 | -------------------------------------------------------------------------------- /bin/start_minikube.sh: -------------------------------------------------------------------------------- 1 | minikube profile serverless_datascience 2 | 3 | minikube start \ 4 | --memory=8192 \ 5 | --cpus=4 \ 6 | --disk-size=50GB \ 7 | --kubernetes-version=v1.11.0 \ 8 | --insecure-registry localhost:5000 \ 9 | --profile serverless_datascience 10 | 11 | minikube mount $(pwd):/mnt & 12 | 13 | eval $(minikube docker-env) 14 | -------------------------------------------------------------------------------- /docker/environment.yml: -------------------------------------------------------------------------------- 1 | name: serverless_datascience 2 | dependencies: 3 | - python=3.6.* 4 | - nodejs=11.9.0 5 | - pandas=0.24.1 6 | - openjdk=11.0.1 7 | - maven=3.6.0 8 | - py4j=0.10.7 9 | - jupyterlab=0.35.4 10 | - beakerx=1.3.0 11 | - ipykernel=5.1.0 12 | - pyviz/label/dev::pyviz=0.10.0 13 | - pip: 14 | - json2yaml==1.1.1 15 | - papermill==0.17.1 16 | -------------------------------------------------------------------------------- /yml/sample-user.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: admin-user 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: admin-user 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: admin-user 18 | namespace: kube-system 19 | 20 | -------------------------------------------------------------------------------- /yml/sample-admin-user.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: admin-user 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: admin-user 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: admin-user 18 | namespace: kube-system 19 | 20 | -------------------------------------------------------------------------------- /notebooks/notebook_out_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 6, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "hello i am stdout!\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "print (\"hello i am stdout!\")" 18 | ] 19 | } 20 | ], 21 | "metadata": { 22 | "kernelspec": { 23 | "display_name": "Python 3", 24 | "language": "python", 25 | "name": "python3" 26 | }, 27 | "language_info": { 28 | "codemirror_mode": { 29 | "name": "ipython", 30 | "version": 3 31 | }, 32 | "file_extension": ".py", 33 | "mimetype": "text/x-python", 34 | "name": "python", 35 | "nbconvert_exporter": "python", 36 | "pygments_lexer": "ipython3", 37 | "version": "3.6.8" 38 | } 39 | }, 40 | "nbformat": 4, 41 | "nbformat_minor": 2 42 | } 43 | -------------------------------------------------------------------------------- /docker/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM serverless_datascience:base 2 | 3 | ## 4 | ## Install docker in docker 5 | ## 6 | RUN curl -fsSL https://get.docker.com -o get-docker.sh &&\ 7 | sh get-docker.sh 8 | 9 | ## 10 | ## Install openfaas cli 11 | ## 12 | RUN curl -sL https://cli.openfaas.com | sh 13 | 14 | ## 15 | ## Jupyter extension installation 16 | ## 17 | RUN bash -c "\ 18 | source activate $(cat $ENVIRONMENT | yq -r .name );\ 19 | jupyter serverextension enable --py --sys-prefix jupyterlab && \ 20 | jupyter nbextension enable --py --sys-prefix widgetsnbextension && \ 21 | jupyter nbextension enable codefolding/main && \ 22 | jupyter labextension install @pyviz/jupyterlab_pyviz && \ 23 | jupyter labextension install @jupyter-widgets/jupyterlab-manager && \ 24 | jupyter labextension install @jupyterlab/celltags" 25 | 26 | ## 27 | ## Install tini as part of os dependencies 28 | ## 29 | ENV TINI_VERSION v0.3.4 30 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 31 | RUN chmod +x /tini 32 | 33 | ## 34 | ## docker run startup 35 | ## 36 | RUN usermod -a -G docker $NB_USER 37 | USER root 38 | RUN echo "source activate serverless_datascience" > ~/.bashrc 39 | ENTRYPOINT [ "/tini", "--", "bash", "-c"] 40 | -------------------------------------------------------------------------------- /yml/jupyter.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: jupyter 5 | labels: 6 | app: jupyter 7 | spec: 8 | ports: 9 | - port: 8888 10 | name: http 11 | targetPort: 8888 12 | selector: 13 | app: jupyter 14 | type: NodePort 15 | 16 | --- 17 | 18 | apiVersion: v1 19 | kind: Pod 20 | metadata: 21 | name: jupyter 22 | labels: 23 | app: jupyter 24 | spec: 25 | containers: 26 | - name: jupyter 27 | image: serverless_datascience:dev 28 | command: ["bash", "-c", "source activate $(cat $ENVIRONMENT | yq -r .name ) &&\ 29 | jupyter notebook \ 30 | --ip=0.0.0.0 \ 31 | --NotebookApp.iopub_data_rate_limit=10000000 \ 32 | --NotebookApp.token= \ 33 | --allow-root"] 34 | env: 35 | - name: OPENFAAS_URL 36 | value: gateway-external.openfaas.svc.cluster.local:8080 37 | ports: 38 | - containerPort: 8888 39 | protocol: TCP 40 | name: http 41 | volumeMounts: 42 | - mountPath: /home/jovyan/work/mnt/ 43 | name: host-mount 44 | - mountPath: "/var/run/docker.sock" 45 | name: dockersock 46 | volumes: 47 | - name: host-mount 48 | hostPath: 49 | path: /mnt 50 | - name: dockersock 51 | hostPath: 52 | path: /var/run/docker.sock 53 | -------------------------------------------------------------------------------- /docker/Dockerfile.func: -------------------------------------------------------------------------------- 1 | FROM serverless_datascience:base 2 | 3 | ## 4 | ## Download watchdog binary from openfaas 5 | ## 6 | RUN curl -sL https://github.com/openfaas/faas/releases/download/0.9.6/fwatchdog > /usr/bin/fwatchdog \ 7 | && chmod +x /usr/bin/fwatchdog 8 | 9 | ## 10 | ## Notebook execution environment variables 11 | ## for notebook placeholder naming 12 | ## 13 | ENV PLACEHOLDER_NOTEBOOK=/notebook.ipynb 14 | ENV OUTPUT_NOTEBOOK=/out.ipynb 15 | 16 | ## 17 | ## Add notebook from build argument. 18 | ## 19 | ARG notebook 20 | ENV INPUT_NOTEBOOK=$notebook 21 | COPY $INPUT_NOTEBOOK $PLACEHOLDER_NOTEBOOK 22 | 23 | ## 24 | ## Optional build argument 25 | ## to ignore input paramaters 26 | ## 27 | ARG accept_parameters 28 | ENV ACCEPT_PARAMETERS=$accept_parameters 29 | 30 | ## 31 | ## Inline script creation for notebook execution and response 32 | ## This is to minimize file artifacts for this docker image 33 | ## 34 | ## - activate the conda environment to execute the notebook in 35 | ## - convert stdin as json to yaml file 36 | ## - execute notebook with yaml file paramaters 37 | ## - parse executed notebook for stdout and return 38 | ## 39 | ENV PYTHONWARNINGS="ignore" 40 | ADD execute_notebook.py execute_notebook.py 41 | RUN echo 'source activate $(cat $ENVIRONMENT | yq -r .name ) &&\ 42 | python3 execute_notebook.py' > execute-notebook-script.sh &&\ 43 | chmod +x execute-notebook-script.sh 44 | 45 | ## 46 | ## Environment arguments for watchdog 47 | ## 48 | ENV write_timeout 30 49 | ENV read_timeout 30 50 | 51 | ENV fprocess="bash execute-notebook-script.sh" 52 | CMD ["fwatchdog"] 53 | HEALTHCHECK --interval=5s CMD [ -e /tmp/.lock ] || exit 1 54 | -------------------------------------------------------------------------------- /bin/deploy_cluster_components.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Setting up and init ..." 4 | kubectl create serviceaccount -n kube-system tiller 5 | kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller 6 | helm init --service-account=tiller --tiller-namespace=kube-system 7 | 8 | echo "Securing helm..." 9 | kubectl patch deployment tiller-deploy --namespace=kube-system --type=json --patch='[{"op": "add", "path": "/spec/template/spec/containers/0/command", "value": ["/tiller", "--listen=localhost:44134"]}]' 10 | 11 | echo "Deploying single user notebook server ..." 12 | if [[ "$(kubectl get pods | grep jupyter | wc -l)" -eq 0 ]]; then 13 | echo "Deleting existing jupyter deployment..." 14 | kubectl delete -f yml/jupyter.yml 15 | fi 16 | kubectl apply -f yml/jupyter.yml 17 | 18 | echo "Deploying faas namespace ..." 19 | kubectl apply -f https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.yml 20 | 21 | echo "Adding openfaas to helm repos ..." 22 | helm repo add openfaas https://openfaas.github.io/faas-netes/ 23 | 24 | echo "Adding openfaas secret ..." 25 | PASSWORD=$(head -c 12 /dev/urandom | shasum| cut -d' ' -f1) 26 | kubectl -n openfaas create secret generic basic-auth \ 27 | --from-literal=basic-auth-user=admin \ 28 | --from-literal=basic-auth-password="$PASSWORD" 29 | 30 | echo "Deploying openfaas ..." 31 | helm repo update && \ 32 | helm upgrade openfaas \ 33 | --install openfaas/openfaas \ 34 | --namespace openfaas \ 35 | --set basic_auth=false \ 36 | --set functionNamespace=openfaas 37 | 38 | echo "Upgrading openfaas image pull policy ..." 39 | helm upgrade openfaas openfaas/openfaas --install --set "faasnetesd.imagePullPolicy=IfNotPresent" 40 | -------------------------------------------------------------------------------- /docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | FROM jupyter/datascience-notebook 2 | 3 | ## 4 | ## os dependencies 5 | ## 6 | USER root 7 | RUN apt-get update &&\ 8 | apt-get install -y gnupg \ 9 | curl \ 10 | build-essential \ 11 | graphviz-dev \ 12 | libssl1.0.0 \ 13 | libssl-dev \ 14 | jq &&\ 15 | curl -sL https://deb.nodesource.com/setup_8.x | bash &&\ 16 | apt-get install -y nodejs &&\ 17 | apt-get clean &&\ 18 | pip install yq &&\ 19 | conda update conda 20 | 21 | ## 22 | ## Environment var config 23 | ## 24 | ENV CONDA_DIR /opt/conda 25 | ENV PATH $CONDA_DIR:$PATH:/home/$NB_USER/.local/bin 26 | RUN chown -R $NB_USER /home/$NB_USER/ 27 | 28 | ## 29 | ## Conda environment installation and kernal register 30 | ## 31 | ENV ENVIRONMENT=/environment.yml 32 | ADD environment.yml $ENVIRONMENT 33 | RUN conda create --name $(cat $ENVIRONMENT | yq -r .name ) --clone base && \ 34 | conda env update -f /environment.yml 35 | RUN python -m ipykernel install \ 36 | --user \ 37 | --name $(cat $ENVIRONMENT | yq -r .name ) \ 38 | --display-name "$(cat $ENVIRONMENT | yq -r .name )" 39 | 40 | ## 41 | ## Delete beakerx base log4j jar to stop javelin from logging to stdout 42 | ## see: https://github.com/twosigma/beakerx/blob/3b2a272ee2699bdba400d83d813fe6744c533e38/kernel/base/build.gradle#L42 43 | ## 44 | RUN bash -c "source activate $(cat $ENVIRONMENT | yq -r .name ) ;\ 45 | cd /opt/conda/envs/$(cat $ENVIRONMENT | yq -r .name )/lib/python3.6/site-packages/beakerx/kernel/base/lib/ ;\ 46 | wget http://central.maven.org/maven2/org/slf4j/slf4j-nop/1.7.25/slf4j-nop-1.7.25.jar ;\ 47 | rm slf4j-log4j12-1.7.25.jar" 48 | -------------------------------------------------------------------------------- /notebooks/execute_notebook.py: -------------------------------------------------------------------------------- 1 | import os,sys,json,logging,papermill,warnings 2 | 3 | ## Disable papermill logger 4 | ## to read stdout without logs 5 | logger = logging.getLogger('papermill') 6 | logger.disabled=True 7 | 8 | ## Read stdin as json to transform into paramaters 9 | ## if none stdin create empty paramaters 10 | paramaters = json.loads("{}") 11 | try: 12 | st = sys.stdin.read() 13 | paramaters = json.loads("".join(st)) 14 | except: 15 | paramaters = json.loads("{}") 16 | 17 | ## Execute notebook from environment variable notebook paths 18 | ## Pass in stdin as json dict and disable all output 19 | papermill.execute_notebook( 20 | os.environ["PLACEHOLDER_NOTEBOOK"], 21 | os.environ["OUTPUT_NOTEBOOK"], 22 | paramaters if os.environ["ACCEPT_PARAMETERS"] else None, 23 | progress_bar=False, 24 | log_output=False, 25 | report_mode=False 26 | ) 27 | 28 | ## Get return type for notebook from 'Http_Accept' header 29 | ## if none return empty string (stdout) 30 | notebook_return_type = os.environ.get('Http_Accept',"").split(",")[0] 31 | 32 | ## Get all the cell outputs from notebook 33 | ## iterate backwards and retrieve output that 34 | ## meets 'Http_Accept' header, if none match 35 | ## return last output 36 | ## 37 | ## If 'Http_Accept' is */* or "" default to stdout 38 | output_notebook = papermill.read_notebook(os.environ["OUTPUT_NOTEBOOK"]) 39 | outputs=sum([list(c["outputs"]) \ 40 | if "outputs" in c.keys() else [] \ 41 | for c in output_notebook.node.cells],[]) 42 | 43 | for cell in outputs[::-1]: 44 | if (cell["output_type"] == "error"): 45 | print (cell["traceback"]) 46 | break 47 | 48 | elif (cell["output_type"] == "stream") and \ 49 | (notebook_return_type in ["*/*", ""]): 50 | print (cell["text"]) 51 | break 52 | 53 | elif (cell["output_type"] in ["display_data","execute_result"]): 54 | if (notebook_return_type in cell["data"].keys()): 55 | print (cell["data"][notebook_return_type]) 56 | break 57 | else: 58 | print ("No output with type %s found"%notebook_return_type) -------------------------------------------------------------------------------- /yml/registry.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: kube-registry-v0 5 | namespace: kube-system 6 | labels: 7 | k8s-app: kube-registry 8 | version: v0 9 | spec: 10 | replicas: 1 11 | selector: 12 | k8s-app: kube-registry 13 | version: v0 14 | template: 15 | metadata: 16 | labels: 17 | k8s-app: kube-registry 18 | version: v0 19 | spec: 20 | containers: 21 | - name: registry 22 | image: registry:2.5.1 23 | resources: 24 | # keep request = limit to keep this container in guaranteed class 25 | limits: 26 | cpu: 100m 27 | memory: 100Mi 28 | requests: 29 | cpu: 100m 30 | memory: 100Mi 31 | env: 32 | - name: REGISTRY_HTTP_ADDR 33 | value: :5000 34 | - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY 35 | value: /var/lib/registry 36 | volumeMounts: 37 | - name: image-store 38 | mountPath: /var/lib/registry 39 | ports: 40 | - containerPort: 5000 41 | name: registry 42 | protocol: TCP 43 | volumes: 44 | - name: image-store 45 | hostPath: 46 | path: /data/registry/ 47 | 48 | --- 49 | 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: kube-registry 54 | namespace: kube-system 55 | labels: 56 | k8s-app: kube-registry 57 | spec: 58 | selector: 59 | k8s-app: kube-registry 60 | ports: 61 | - name: registry 62 | port: 5000 63 | protocol: TCP 64 | 65 | --- 66 | 67 | apiVersion: extensions/v1beta1 68 | kind: DaemonSet 69 | metadata: 70 | name: kube-registry-proxy 71 | namespace: kube-system 72 | labels: 73 | k8s-app: kube-registry 74 | kubernetes.io/cluster-service: "true" 75 | version: v0.4 76 | spec: 77 | template: 78 | metadata: 79 | labels: 80 | k8s-app: kube-registry 81 | version: v0.4 82 | spec: 83 | containers: 84 | - name: kube-registry-proxy 85 | image: gcr.io/google_containers/kube-registry-proxy:0.4 86 | resources: 87 | limits: 88 | cpu: 100m 89 | memory: 50Mi 90 | env: 91 | - name: REGISTRY_HOST 92 | value: kube-registry.kube-system.svc.cluster.local 93 | - name: REGISTRY_PORT 94 | value: "5000" 95 | ports: 96 | - name: registry 97 | containerPort: 80 98 | hostPort: 5000 99 | -------------------------------------------------------------------------------- /notebooks/notebook_svd_R.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Singular vector decomposition\n", 8 | "\n", 9 | "explanation to do" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 7, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "library(jsonlite)" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 14, 24 | "metadata": { 25 | "tags": [ 26 | "parameters" 27 | ] 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "INPUT_MATRIX <- '[[1,2,3,4],[2,3,4,5],[3,4,5,6]]'" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 29, 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/html": [ 42 | "\n", 43 | "\n", 44 | "\t\n", 45 | "\n", 46 | "
1234
\n" 47 | ], 48 | "text/latex": [ 49 | "\\begin{tabular}{llll}\n", 50 | "\t 1 & 2 & 3 & 4\\\\\n", 51 | "\\end{tabular}\n" 52 | ], 53 | "text/markdown": [ 54 | "\n", 55 | "| 1 | 2 | 3 | 4 |\n", 56 | "\n" 57 | ], 58 | "text/plain": [ 59 | " [,1] [,2] [,3] [,4]\n", 60 | "[1,] 1 2 3 4 " 61 | ] 62 | }, 63 | "metadata": {}, 64 | "output_type": "display_data" 65 | } 66 | ], 67 | "source": [ 68 | "# Turn default json string matrix\n", 69 | "# and convert to asn R matrix\n", 70 | "arr = fromJSON(INPUT_MATRIX)\n", 71 | "head(arr, n=1)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 30, 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "data": { 81 | "text/plain": [ 82 | "{\n", 83 | " \"d\": [13.0112, 0.8419, 9.9099e-17],\n", 84 | " \"u\": [\n", 85 | " [-0.4177, -0.8117, 0.4082],\n", 86 | " [-0.5647, -0.1201, -0.8165],\n", 87 | " [-0.7118, 0.5716, 0.4082]\n", 88 | " ],\n", 89 | " \"v\": [\n", 90 | " [-0.283, 0.7873, -0.3741],\n", 91 | " [-0.4132, 0.3595, 0.797],\n", 92 | " [-0.5434, -0.0683, -0.4717],\n", 93 | " [-0.6737, -0.4962, 0.0488]\n", 94 | " ]\n", 95 | "} " 96 | ] 97 | }, 98 | "metadata": {}, 99 | "output_type": "display_data" 100 | } 101 | ], 102 | "source": [ 103 | "toJSON(svd(arr), pretty=TRUE)" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "R", 110 | "language": "R", 111 | "name": "ir" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": "r", 115 | "file_extension": ".r", 116 | "mimetype": "text/x-r-source", 117 | "name": "R", 118 | "pygments_lexer": "r", 119 | "version": "3.5.1" 120 | }, 121 | "toc": { 122 | "base_numbering": 1, 123 | "nav_menu": {}, 124 | "number_sections": false, 125 | "sideBar": false, 126 | "skip_h1_title": false, 127 | "title_cell": "Table of Contents", 128 | "title_sidebar": "Contents", 129 | "toc_cell": false, 130 | "toc_position": {}, 131 | "toc_section_display": false, 132 | "toc_window_display": false 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 2 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Serverless Notebooks 2 | 3 | Deploy Jupyter Notebooks as serverless functions with OpenFaas. 4 | 5 | ![pics/splash.png](pics/splash.png) 6 | 7 | This repositoy is an example implementation / workflow on how to turn [Jupyter Notebooks](https://jupyter.org/) into serverless functions within [OpenFaas](https://github.com/openfaas/faas). 8 | 9 | ### Background 10 | 11 | Jupyter Notebooks are files containing code, equations, visualizations, and narrative text that can be run in any of the [available jupyter kernels](https://github.com/jupyter/jupyter/wiki/Jupyter-kernels) in your preferred language. Notebooks have a variety of use cases such as data cleaning and transformation, numerical simulation, statistical modeling, data visualization, or machine learning which mostly are developed with research and discovery primarily in mind. 12 | 13 | After a notebook is developed to produce a meaningful result, it is then a question on how to use the result in some broader context. One way to integrate code originally written in a notebook to a module in a traditional application is to turn it into an [executable script](https://nbconvert.readthedocs.io/en/latest/usage.html#executable-script). However automated or manual conversion forces the removal of [several build in features](https://ipython.readthedocs.io/en/stable/interactive/magics.html) of these notebooks. Another way is to [schedule execution of a notebook](https://medium.com/netflix-techblog/notebook-innovation-591ee3221233) based on user defined paramaters. This method allows users to keep their notebook as is and execute it via a cli or python api. 14 | 15 | The first method is great for general applications but any intermediate viewable outputs will be ignored so you must write logic to export results. The later method has greater implications for notebooks and thinks of them not as whole programs, but runnable functions with paramaters. Using a notebook as a function we can try using placing it in a serverless execution environment to provide an easy workflow from research and development to a usable service without writing any server code! 16 | 17 | ### Getting Started 18 | --- 19 | 20 | #### Requirements: 21 | 22 | - [Minikube](https://github.com/kubernetes/minikube) version 0.30.0 23 | - [Docker](https://docs.docker.com/install/) version 18.09.1 24 | 25 | #### Step zero: Build a cluster 26 | 27 | This repo requires access to a kubernetes cluster. To create a local cluster with `minikube` you can run 28 | 29 | ```shell 30 | ## Create unique minikube profile 31 | minikube profile serverless_notebooks 32 | 33 | ## Start minikube 34 | minikube start \ 35 | --kubernetes-version=v1.11.0 \ 36 | --profile serverless_notebooks 37 | 38 | ## Mount current repo into minikube 39 | ## for pod access 40 | minikube mount $(pwd):/mnt & 41 | ``` 42 | 43 | You can also run `./bin/start_minikube.sh` for an easy scripted one liner 44 | 45 | #### Step one: building the docker images 46 | 47 | Before deploying any of the components we will be building the docker images needed to deploy jupyter or any of the notebook function containers. To build the images, some basic utility scripts are provided 48 | 49 | ``` shell 50 | ## Build to minikube registry 51 | eval $(minikube docker-env) 52 | 53 | ## Build base and dev images 54 | ./bin/build_serverless_datascience_base.sh 55 | ./bin/build_serverless_datascience_dev.sh 56 | ``` 57 | 58 | This step takes a while... 59 | 60 | #### Step two: Deploy cluster components 61 | 62 | To run the notebooks and setup the cluster there are a few steps 63 | 64 | Setup helm within your cluster 65 | 66 | ``` shell 67 | echo "Setting up and init ..." 68 | kubectl create serviceaccount -n kube-system tiller 69 | kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller 70 | helm init --service-account=tiller --tiller-namespace=kube-system --wait 71 | 72 | echo "Securing helm..." 73 | sleep 5 #wait for tiller to deploy 74 | kubectl patch deployment tiller-deploy \ 75 | --namespace=kube-system \ 76 | --type=json \ 77 | --patch='[{"op": "add", "path": "/spec/template/spec/containers/0/command", "value": ["/tiller", "--listen=localhost:44134"]}]' 78 | ``` 79 | 80 | Then deploy `./yml/jupyter.yml` for our jupyter based interactive development environment from within the cluster 81 | 82 | ``` shell 83 | echo "Deploying single user notebook server ..." 84 | kubectl apply -f yml/jupyter.yml 85 | ``` 86 | 87 | Now deploy OpenFaas using `helm` 88 | 89 | ``` shell 90 | echo "Adding openfaas to helm repos ..." 91 | helm repo add openfaas https://openfaas.github.io/faas-netes/ 92 | 93 | echo "Adding openfaas secret ..." 94 | PASSWORD=$(head -c 12 /dev/urandom | shasum| cut -d' ' -f1) 95 | kubectl -n openfaas create secret generic basic-auth \ 96 | --from-literal=basic-auth-user=admin \ 97 | --from-literal=basic-auth-password=\"$PASSWORD\" 98 | 99 | echo "Deploying openfaas ..." 100 | helm repo update && \ 101 | helm upgrade openfaas \ 102 | --install openfaas/openfaas \ 103 | --namespace openfaas \ 104 | --set basic_auth=false \ 105 | --set functionNamespace=openfaas 106 | 107 | echo "Upgrading openfaas image pull policy ..." 108 | helm upgrade openfaas openfaas/openfaas --install --set "faasnetesd.imagePullPolicy=IfNotPresent" 109 | ``` 110 | 111 | You can also deploy all these components using the shell script `./bin/deploy_cluster_components.sh` 112 | 113 | #### Step three: forward ports from jupyter and openfaas 114 | 115 | ``` shell 116 | sh -c '(kubectl port-forward -n default svc/jupyter 9999:8888 &\ 117 | kubectl port-forward -n openfaas svc/gateway-external 8080:8080)' 118 | ``` 119 | 120 | You can now go to `localhost:9999/lab` in your browser and you should see several language kernels at your disposal 121 | 122 | ![pics/language_lab.png](pics/language_lab.png) 123 | 124 | Now navigate to `mnt/notebooks/deploy_notebook_function.ipynb` for further instructions. 125 | -------------------------------------------------------------------------------- /notebooks/notebook_cosine_similarity_scala.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Cosine similarity!\n", 8 | "\n", 9 | "By: Alex Comerford (alexanderjcomerford@gmail.com)\n", 10 | "\n", 11 | "In this notebook we will be explaining what is cosine similarity!\n", 12 | "\n", 13 | "This notebook will be provided as a tutorial and as a service ready function for execution from within OpenFaas. This means that any modifications that are added to this notebook can be deployed and propogated throughout all the consumers of this function, so be careful what you edit before you deploy!" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "### Paramaters\n", 21 | "\n", 22 | "First we will start this notebook with a set list of paramaters defining the context in which we are executing this notebook" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": { 29 | "tags": [ 30 | "parameters" 31 | ] 32 | }, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "null" 38 | ] 39 | }, 40 | "execution_count": 1, 41 | "metadata": {}, 42 | "output_type": "execute_result" 43 | } 44 | ], 45 | "source": [ 46 | "// MATRIX_A/B will be used as paramaters when executing Cosine similarity\n", 47 | "// as a service. By default they are sample values\n", 48 | "val VECTOR_A = List(0,1,2)\n", 49 | "val VECTOR_B = List(4,5,6)\n", 50 | "\n", 51 | "// No output to clutter the notebook\n", 52 | "null" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## What is Cosine similarity??\n", 60 | "\n", 61 | "Cosine similarity can be defined as follows in Latex" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 2, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "text/latex": [ 72 | "$\n", 73 | "\\begin{equation}\n", 74 | "\\cos ({\\bf A},{\\bf B}) = {{\\bf A} {\\bf B} \\over \\|{\\bf A}\\| \\|{\\bf B}\\|} = \\frac{ \\sum_{i=1}^{n}{{\\bf A}_i{\\bf B}_i} }{ \\sqrt{\\sum_{i=1}^{n}{({\\bf A}_i)^2}} \\sqrt{\\sum_{i=1}^{n}{({\\bf B}_i)^2}} }\n", 75 | "\\end{equation}\n", 76 | "$" 77 | ] 78 | }, 79 | "metadata": {}, 80 | "output_type": "display_data" 81 | } 82 | ], 83 | "source": [ 84 | "display(Latex(\"\"\"$\n", 85 | "\\begin{equation}\n", 86 | "\\cos ({\\bf A},{\\bf B}) = {{\\bf A} {\\bf B} \\over \\|{\\bf A}\\| \\|{\\bf B}\\|} = \\frac{ \\sum_{i=1}^{n}{{\\bf A}_i{\\bf B}_i} }{ \\sqrt{\\sum_{i=1}^{n}{({\\bf A}_i)^2}} \\sqrt{\\sum_{i=1}^{n}{({\\bf B}_i)^2}} }\n", 87 | "\\end{equation}\n", 88 | "$\"\"\"))" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "Cosine similarity is a metric for measuring the distance between two vectors.\n", 96 | "\n", 97 | "The outputs we can expect are from `-1` to `1`, dissimilar to similar respectively.\n", 98 | "\n", 99 | "This is a widely used function in several different disciplines especially natural language processing (nlp). One such example in nlp can be word counts across sentences. Sentences with similar word counts will have higher cosine similarity and therefore can be though of as more related!\n", 100 | "\n", 101 | "Cosine similarity can be used in several other domains where some properties of the instances make so that the weights might be larger without meaning anything different. Sensor values that were captured in various lengths (in time) between instances could be such an example.\n", 102 | "\n", 103 | "Below we will implement an `object` in scala to compute the cosine similarity of two \"vectors\" (actually type `List`) as a demonstration ." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 3, 109 | "metadata": {}, 110 | "outputs": [ 111 | { 112 | "data": { 113 | "text/plain": [ 114 | "$line27.$read$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$$iw$CosineSimilarity$@4d4afa2c" 115 | ] 116 | }, 117 | "execution_count": 3, 118 | "metadata": {}, 119 | "output_type": "execute_result" 120 | } 121 | ], 122 | "source": [ 123 | "object CosineSimilarity {\n", 124 | " \n", 125 | " /*\n", 126 | " * This method takes 2 equal length arrays of integers \n", 127 | " * It returns a double representing similarity of the 2 arrays\n", 128 | " * 0.9925 would be 99.25% similar\n", 129 | " * (x dot y)/||X|| ||Y||\n", 130 | " */\n", 131 | " def cosineSimilarity(x: Array[Double], y: Array[Double]): Double = {\n", 132 | " \n", 133 | " // ensure similary \n", 134 | " require(x.size == y.size)\n", 135 | " dotProduct(x, y)/(magnitude(x) * magnitude(y))\n", 136 | " }\n", 137 | " \n", 138 | " /*\n", 139 | " * Return the dot product of the 2 arrays\n", 140 | " * e.g. (a[0]*b[0])+(a[1]*a[2])\n", 141 | " */\n", 142 | " def dotProduct(x: Array[Double], y: Array[Double]): Double = {\n", 143 | " (for((a, b) <- x zip y) yield a * b) sum\n", 144 | " }\n", 145 | " \n", 146 | " /*\n", 147 | " * Return the magnitude of an array\n", 148 | " * We multiply each element, sum it, then square root the result.\n", 149 | " */\n", 150 | " def magnitude(x: Array[Double]): Double = {\n", 151 | " math.sqrt(x map(i => i*i) sum)\n", 152 | " }\n", 153 | " \n", 154 | "}" 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "metadata": {}, 160 | "source": [ 161 | "Now that we have an implementation of cosine similarity we can test it with whatever values we want! In the next cell we will take the paramater input from the top cell and calculate their cosine similarity and print it to `stdout` from within this notebook." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 4, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "data": { 171 | "text/plain": [ 172 | "0.8664002254439633" 173 | ] 174 | }, 175 | "execution_count": 4, 176 | "metadata": {}, 177 | "output_type": "execute_result" 178 | } 179 | ], 180 | "source": [ 181 | "// Convert vectors to Arrays of type Double\n", 182 | "var A = VECTOR_A.toArray.map(_.toDouble)\n", 183 | "var B = VECTOR_B.toArray.map(_.toDouble)\n", 184 | "\n", 185 | "// Compute their cosine similarity\n", 186 | "CosineSimilarity.cosineSimilarity(A,B)" 187 | ] 188 | } 189 | ], 190 | "metadata": { 191 | "kernelspec": { 192 | "display_name": "Scala", 193 | "language": "scala", 194 | "name": "scala" 195 | }, 196 | "language_info": { 197 | "codemirror_mode": "text/x-scala", 198 | "file_extension": ".scala", 199 | "mimetype": "", 200 | "name": "Scala", 201 | "nbconverter_exporter": "", 202 | "version": "2.11.12" 203 | } 204 | }, 205 | "nbformat": 4, 206 | "nbformat_minor": 2 207 | } 208 | -------------------------------------------------------------------------------- /notebooks/notebook_mandelbrot_clojure.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## The mandelbrot set \n", 8 | "\n", 9 | "By: Alex Comerford\n", 10 | "\n", 11 | "This is a notebook explaining the mandelbrotset using clojure!" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## What is the mandelbrot set?\n", 19 | "\n" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "Todo..." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/c+" 38 | ] 39 | }, 40 | "execution_count": 1, 41 | "metadata": {}, 42 | "output_type": "execute_result" 43 | } 44 | ], 45 | "source": [ 46 | "(defn c+ [[re1 im1] [re2 im2]] [(+ re1 re2) (+ im1 im2)])" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 2, 52 | "metadata": {}, 53 | "outputs": [ 54 | { 55 | "data": { 56 | "text/plain": [ 57 | "[20, 11]" 58 | ] 59 | }, 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "output_type": "execute_result" 63 | } 64 | ], 65 | "source": [ 66 | "(c+ [10 10] [10 1])" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "text/plain": [ 77 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/c*" 78 | ] 79 | }, 80 | "execution_count": 3, 81 | "metadata": {}, 82 | "output_type": "execute_result" 83 | } 84 | ], 85 | "source": [ 86 | "(defn c* [[re1 im1] [re2 im2]]\n", 87 | " [(- (* re1 re2) (* im1 im2)) (+ (* re1 im2) (* im1 re2))])" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 4, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "data": { 97 | "text/plain": [ 98 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/c2" 99 | ] 100 | }, 101 | "execution_count": 4, 102 | "metadata": {}, 103 | "output_type": "execute_result" 104 | } 105 | ], 106 | "source": [ 107 | "(defn c2 [c] (c* c c))" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": {}, 114 | "outputs": [ 115 | { 116 | "data": { 117 | "text/plain": [ 118 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/|c|" 119 | ] 120 | }, 121 | "execution_count": 5, 122 | "metadata": {}, 123 | "output_type": "execute_result" 124 | } 125 | ], 126 | "source": [ 127 | "(defn |c| [[re im]] (Math/sqrt (+ (* re re) (* im im))))" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 6, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/get-mandel-set" 139 | ] 140 | }, 141 | "execution_count": 6, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "(defn get-mandel-set [im-range re-range max-iter]\n", 148 | " (for [im im-range\n", 149 | " re re-range\n", 150 | " :let [c [re im]]]\n", 151 | " (loop [z [0 0], cnt -1]\n", 152 | " (let [z (c+ (c2 z) c)\n", 153 | " cnt (inc cnt)]\n", 154 | " (if-not (and (< (|c| z) 4) (< cnt max-iter))\n", 155 | " cnt\n", 156 | " (recur z cnt))))))" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 7, 162 | "metadata": {}, 163 | "outputs": [ 164 | { 165 | "data": { 166 | "text/plain": [ 167 | "#'beaker_clojure_shell_d86b9cf6-dcb1-4fae-b99e-4e6850779e37/print-mandel" 168 | ] 169 | }, 170 | "execution_count": 7, 171 | "metadata": {}, 172 | "output_type": "execute_result" 173 | } 174 | ], 175 | "source": [ 176 | "(def mandel (atom \"\"))\n", 177 | "(defn print-mandel [sz data]\n", 178 | " (let [cs [\".\" \",\" \"\\\"\" \"-\" \":\" \"/\" \"(\" \"*\" \"|\" \"$\" \"#\" \"@\" \"%\" \"~\"]] \n", 179 | " (loop [ds data]\n", 180 | " (when-not (empty? ds)\n", 181 | " (let [cs (map #(nth cs (dec %)) (take sz ds))]\n", 182 | " (doseq [c cs] (reset! mandel (str @mandel c)))\n", 183 | " (reset! mandel (str @mandel \"\\n\"))\n", 184 | " (recur (drop sz ds)))))))" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 8, 190 | "metadata": {}, 191 | "outputs": [ 192 | { 193 | "data": { 194 | "text/plain": [ 195 | ",,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"------::/($~|#~:----\"\"\"\"\"\"\"\"\",,,,,,,,,,\n", 196 | ",,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"-------:::/(*|~~~|(/::-----\"\"\"\"\"\"\"\",,,,,,,,\n", 197 | ",,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"--------::::///($~~~~~~~|(/::::----\"\"\"\"\"\",,,,,,,\n", 198 | ",,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"---------:::/**#***|$#~~~~~~%$||((//(~:---\"\"\"\"\",,,,,,\n", 199 | ",,,,,,\"\"\"\"\"\"\"\"\"\"\"-----------:::://*@~~~~~~~~~~~~~~~~~~~#@~~~~/:--\"\"\"\"\"\",,,,\n", 200 | ",,,\"\"\"\"\"\"\"\"\"\"\"----:::::::::::///(|~@~~~~~~~~~~~~~~~~~~~~~~~@(/:---\"\"\"\"\",,,,\n", 201 | ",\"\"\"\"\"\"\"\"\"-----::/|$(//(*((((((*|%~~~~~~~~~~~~~~~~~~~~~~~~~%$$/:--\"\"\"\"\"\",,,\n", 202 | "\"\"\"\"\"\"\"------:::/(*#~~~@~~%~$||$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/:---\"\"\"\"\"\",,\n", 203 | "\"\"\"\"------::::/((*#@~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$/:---\"\"\"\"\"\",,\n", 204 | "\"---:::::////(*@%~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/::---\"\"\"\"\"\",,\n", 205 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$*//::---\"\"\"\"\"\",,\n", 206 | "\"---:::::////(*@%~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/::---\"\"\"\"\"\",,\n", 207 | "\"\"\"\"------::::/((*#@~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$/:---\"\"\"\"\"\",,\n", 208 | "\"\"\"\"\"\"\"------:::/(*#~~~@~~%~$||$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/:---\"\"\"\"\"\",,\n", 209 | ",\"\"\"\"\"\"\"\"\"-----::/|$(//(*((((((*|%~~~~~~~~~~~~~~~~~~~~~~~~~%$$/:--\"\"\"\"\"\",,,\n", 210 | ",,,\"\"\"\"\"\"\"\"\"\"\"----:::::::::::///(|~@~~~~~~~~~~~~~~~~~~~~~~~@(/:---\"\"\"\"\",,,,\n", 211 | ",,,,,,\"\"\"\"\"\"\"\"\"\"\"-----------:::://*@~~~~~~~~~~~~~~~~~~~#@~~~~/:--\"\"\"\"\"\",,,,\n", 212 | ",,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"---------:::/**#***|$#~~~~~~%$||((//(~:---\"\"\"\"\",,,,,,\n", 213 | ",,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"--------::::///($~~~~~~~|(/::::----\"\"\"\"\"\",,,,,,,\n", 214 | ",,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"-------:::/(*|~~~|(/::-----\"\"\"\"\"\"\"\",,,,,,,,\n", 215 | ",,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"------::/($~|#~:----\"\"\"\"\"\"\"\"\",,,,,,,,,,\n" 216 | ] 217 | }, 218 | "execution_count": 8, 219 | "metadata": {}, 220 | "output_type": "execute_result" 221 | } 222 | ], 223 | "source": [ 224 | "(reset! mandel \"\")\n", 225 | "\n", 226 | "(->>\n", 227 | " (get-mandel-set (range -1.0 1.0 0.1) (range -2 1 0.04) 14)\n", 228 | " (print-mandel 75))\n", 229 | "\n", 230 | "@mandel" 231 | ] 232 | } 233 | ], 234 | "metadata": { 235 | "kernelspec": { 236 | "display_name": "Clojure", 237 | "language": "clojure", 238 | "name": "clojure" 239 | }, 240 | "language_info": { 241 | "codemirror_mode": "Clojure", 242 | "file_extension": ".clj", 243 | "mimetype": "text/x-clojure", 244 | "name": "Clojure", 245 | "nbconverter_exporter": "", 246 | "version": "1.9.0" 247 | }, 248 | "toc": { 249 | "base_numbering": 1, 250 | "nav_menu": {}, 251 | "number_sections": false, 252 | "sideBar": false, 253 | "skip_h1_title": false, 254 | "title_cell": "Table of Contents", 255 | "title_sidebar": "Contents", 256 | "toc_cell": false, 257 | "toc_position": {}, 258 | "toc_section_display": false, 259 | "toc_window_display": false 260 | } 261 | }, 262 | "nbformat": 4, 263 | "nbformat_minor": 2 264 | } 265 | -------------------------------------------------------------------------------- /notebooks/deploy_notebook_function.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Deploy Notebook Function\n", 8 | "\n", 9 | "by: Alex Comerford (alexanderjcomerford@gmail.com)\n", 10 | "\n", 11 | "In this notebook we will guide the user through the steps to deploying a jupyter notebook as a serverless function on openfaas.\n", 12 | "\n", 13 | "The reason that this notebook can be majorly beneficial to several data scientists and users of jupyter notebooks is to show a method for traditional research code first implemented in a jupyter notebook to be deployed as a scalable service that can exist in a production environment" 14 | ] 15 | }, 16 | { 17 | "cell_type": "markdown", 18 | "metadata": {}, 19 | "source": [ 20 | "### Deploying a notebook steps\n", 21 | "\n", 22 | "There are only three steps to deploying a notebook as a function in openfaas\n", 23 | "\n", 24 | "1. Write your jupyter notebook (with optional [parameters](https://papermill.readthedocs.io/en/latest/usage-parameterize.html))\n", 25 | "2. Run the `deploy_notebook` function\n", 26 | "3. Send http requests to \n", 27 | "\n", 28 | "### Looking behind the curtain\n", 29 | "\n", 30 | "In order to get a better understanding of how notebooks are being turned into \n", 31 | "\n", 32 | "The `deploy_notebook` specified below will use your specified notebook and add it to a docker container which bases off of the same container image used to run this development environment. This is to ensure that the notebooks being executed have as close to the same environment in which they are developed. In this container we include a script called `execute_notebook.py` which will be the main script executed by the openfaas `watchdog` to read http request data via stdin and send desired output via stdout.\n", 33 | "\n", 34 | "`watchdog` will execute `execute_notebook.py` which uses `papermill` to execute a notebook in a desired kernel. The executed notebook will then be saved in a temporary path and the output mime-type that matches the `Http_Accept` environment variable (provided by watchdog) will be printed to stdout. This allows for a notebook to have multiple different return types to be requested." 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 130, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "import os\n", 44 | "import json\n", 45 | "import subprocess" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 131, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "def deploy_notebook(notebook_name, \n", 55 | " accept_parameters,\n", 56 | " function_dockerfile=os.path.join(os.environ[\"HOME\"],\"work/mnt/docker/Dockerfile.func\")):\n", 57 | " '''deploy notebook to openfaas via openfaas\n", 58 | "\n", 59 | " Args:\n", 60 | " notebook_name: path to the notebook to deploy.\n", 61 | " accept_parameters: boolean if notebook should accept parameters.\n", 62 | " function_dockerfile: path to dockerfile for functions\n", 63 | "\n", 64 | " Returns:\n", 65 | " tuple: Exit status and message\n", 66 | " '''\n", 67 | " \n", 68 | " ## Parse notebookname to meet OpenFaas standard naming\n", 69 | " funcname=os.path.splitext(\n", 70 | " os.path.basename(notebook_name))[0].lower().replace(\"_\",\"-\")\n", 71 | "\n", 72 | " ## Build image and return exit status\n", 73 | " exit_status = subprocess.check_call([\"docker\", \"build\",\n", 74 | " \"-t\", funcname,\n", 75 | " \"-f\", function_dockerfile,\n", 76 | " \"--build-arg\", \"notebook=%s\"%notebook_name,\n", 77 | " \"--build-arg\", \"accept_parameters=%s\"%str(accept_parameters),\n", 78 | " \"--no-cache\",\n", 79 | " \".\"])\n", 80 | " if exit_status:\n", 81 | " return (False, \"Unable to build docker image...\")\n", 82 | "\n", 83 | " ## Deploy image and return exit status\n", 84 | " exit_status = subprocess.check_call([\"faas-cli\", \"deploy\",\n", 85 | " \"--image\", funcname,\n", 86 | " \"--name\", funcname])\n", 87 | " if exit_status:\n", 88 | " return (False, \"Unable to deploy docker image via faas-cli\")\n", 89 | " \n", 90 | " return (True, os.path.join(os.environ[\"OPENFAAS_URL\"], \"function/%s\"%funcname))" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## Stdout notebooks\n", 98 | "\n", 99 | "In the next two cells we will deploy a simple notebook that prints a string to stdout.\n", 100 | "\n", 101 | "We will then test (with curl) that notebook as a service and print our response" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 132, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/plain": [ 112 | "(True,\n", 113 | " 'gateway-external.openfaas.svc.cluster.local:8080/function/notebook-out-test')" 114 | ] 115 | }, 116 | "execution_count": 132, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "success, out_function_url = deploy_notebook(notebook_name=\"./notebook_out_test.ipynb\", accept_parameters=True)\n", 123 | "success, out_function_url" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 133, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "name": "stdout", 133 | "output_type": "stream", 134 | "text": [ 135 | "signal: terminated\n", 136 | "CPU times: user 20.4 ms, sys: 12.5 ms, total: 32.9 ms\n", 137 | "Wall time: 2.56 s\n" 138 | ] 139 | } 140 | ], 141 | "source": [ 142 | "%%time\n", 143 | "!curl --url {out_function_url}" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "As we can see we have successfully (hopefully) executed a notebook via an http request and returned the output contents of the executed notebook.\n", 151 | "\n", 152 | "It should be noted that even though this particular notebook only has 1 cell that prints to stdout it still took ~5 seconds to execute. With this approach the overhead of the request is not due to code execution but to the overhead of starting the correct jupyter kernel, writing the notebook to disk, and parsing/sending output through stdout. Fortunately this overhead remains (nearly) constant and can be thought of as negligable for longer based computation" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Sending parameters to notebook services\n", 160 | "\n", 161 | "An additional feature of `watchdog` and `execute_notebook.py` is that json passed as the payload to a POST request gets translated to paramaters for the notebook ([language dependent](https://papermill.readthedocs.io/en/latest/reference/papermill-translators.html))\n", 162 | "\n", 163 | "In the next cell we will deploy a new notebook that takes parameters and will produce new outputs based on those paramaters. Feel free to edit the json paramaters in the `curl` command to interact with it yourself!" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 88, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "success, multiple_out_function_url = deploy_notebook(notebook_name=\"./notebook_multiple_outputs.ipynb\", accept_parameters=True)\n", 173 | "success, multiple_out_function_url" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | " Here we are going to define a small utility lambda function in order to convert a dictionary of paramaters into a specially formatted string so inline jupyter shell commands can correctly read single quotes `'` and double quotes `\"`" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 195, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "fmt_json_string = lambda j:json.dumps(j).replace(\"\\\"\",\"\\\\\\\"\").replace(\"\\'\",\"\\'\")" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "Now we will define a dictionary of paramaters to input into the specific notebook deployed above, then use the `fmt_json_string` to format the string appropriately to use with `curl` in the jupyter notebook" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 198, 202 | "metadata": {}, 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "The TEST variable is THIS IS A DEFAULT TEST STRING\n", 209 | "\n", 210 | "The TEST variable is 'Hello!'\n", 211 | "\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "## define parameters\n", 217 | "params = {\n", 218 | " \"TEST\":\"'Hello!'\"\n", 219 | "}\n", 220 | "\n", 221 | "## Without parameters\n", 222 | "!curl -s --url \"{multiple_out_function_url}\"\n", 223 | "\n", 224 | "## With parameters\n", 225 | "!curl -s --data \"{fmt_json_string(params)}\" \\\n", 226 | " --url \"{multiple_out_function_url}\"" 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "## Parameters + return mime-types\n", 234 | "\n", 235 | "Now that we have demonstrated sending parameters to a notebook service and getting a new result based on those parameters, we can now introduce a new feature of `watchdog` and `execute_notebook.py` which is return mime-types. This feature allows the user to specify the `Accept` header in their request which will then be used to parse whatever mime-type was provided in the executed output notebook to be returned to the user.\n", 236 | "\n", 237 | "To demonstrate this we will use the notebook we passed parameters to before except we will specify our `Accept` header in the `curl` request to be of type `image/png` which will return a `base64` representation of a `png` image. " 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 203, 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "data": { 247 | "image/png": "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\n", 248 | "text/plain": [ 249 | "" 250 | ] 251 | }, 252 | "metadata": {}, 253 | "output_type": "display_data" 254 | }, 255 | { 256 | "name": "stdout", 257 | "output_type": "stream", 258 | "text": [ 259 | "CPU times: user 86.1 ms, sys: 29.5 ms, total: 116 ms\n", 260 | "Wall time: 6.77 s\n" 261 | ] 262 | } 263 | ], 264 | "source": [ 265 | "%%time\n", 266 | "\n", 267 | "## define parameters\n", 268 | "params = {\n", 269 | " \"NUM_POINTS\":100\n", 270 | "}\n", 271 | "\n", 272 | "## Get a new plot updating the number of points we want\n", 273 | "!curl -s -o /tmp/notebook-multiple-outputs \\\n", 274 | " -H \"Accept: image/png\" \\\n", 275 | " --data \"{fmt_json_string(params)}\" \\\n", 276 | " --url \"{multiple_out_function_url}\"\n", 277 | "\n", 278 | "## Decode the b64 plot to a png\n", 279 | "!cat /tmp/notebook-multiple-outputs | base64 -d > /tmp/decoded.png\n", 280 | "\n", 281 | "## Display the decoded image\n", 282 | "from IPython.display import display, Image\n", 283 | "display(Image(filename='/tmp/decoded.png'))" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "## Cosine Similarity in scala example\n", 291 | "\n", 292 | "In the next cell we will deploy a notebook written in scala as a function and get the cosine similarity as an output" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 36, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "name": "stdout", 302 | "output_type": "stream", 303 | "text": [ 304 | "CPU times: user 3.84 ms, sys: 6.34 ms, total: 10.2 ms\n", 305 | "Wall time: 17.2 s\n" 306 | ] 307 | }, 308 | { 309 | "data": { 310 | "text/plain": [ 311 | "True" 312 | ] 313 | }, 314 | "execution_count": 36, 315 | "metadata": {}, 316 | "output_type": "execute_result" 317 | } 318 | ], 319 | "source": [ 320 | "deploy_notebook(notebook_name=\"./notebook_cosine_similarity_scala.ipynb\", \n", 321 | " accept_parameters=True)" 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": 18, 327 | "metadata": {}, 328 | "outputs": [ 329 | { 330 | "name": "stdout", 331 | "output_type": "stream", 332 | "text": [ 333 | "0.3588483640679464\n", 334 | "CPU times: user 177 ms, sys: 37 ms, total: 214 ms\n", 335 | "Wall time: 12.2 s\n" 336 | ] 337 | } 338 | ], 339 | "source": [ 340 | "%%time\n", 341 | "\n", 342 | "!curl -s \\\n", 343 | " --data '{\"VECTOR_A\":[1,-2,3,1,-1,1,-1,5,5,5,-5,5, 5], \\\n", 344 | " \"VECTOR_B\":[1,1, 4,1, 1,1, 1,5,5,5, 5,5,-5]}' \\\n", 345 | " -H \"Accept: text/plain\" \\\n", 346 | " $OPENFAAS_URL/function/notebook-cosine-similarity-scala" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "metadata": {}, 352 | "source": [ 353 | "## Mandelbrot Set in Clojure!\n", 354 | "\n", 355 | "In the next few cells we will deploy a notebook written in clojure to generate an ascii version of the mandelbrot set" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 46, 361 | "metadata": {}, 362 | "outputs": [ 363 | { 364 | "name": "stdout", 365 | "output_type": "stream", 366 | "text": [ 367 | "CPU times: user 8.61 ms, sys: 6.96 ms, total: 15.6 ms\n", 368 | "Wall time: 20.5 s\n" 369 | ] 370 | }, 371 | { 372 | "data": { 373 | "text/plain": [ 374 | "True" 375 | ] 376 | }, 377 | "execution_count": 46, 378 | "metadata": {}, 379 | "output_type": "execute_result" 380 | } 381 | ], 382 | "source": [ 383 | "%%time\n", 384 | "deploy_notebook(notebook_name=\"./notebook_mandelbrot_clojure.ipynb\", \n", 385 | " accept_parameters=False)" 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": 48, 391 | "metadata": {}, 392 | "outputs": [ 393 | { 394 | "name": "stdout", 395 | "output_type": "stream", 396 | "text": [ 397 | ",,,,,,,,,,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"-----\"\"\"\"\"\"\"\"\"\",,,,,,,,,,,,,,,,\n", 398 | ",,,,,,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"----:/~@:::---\"\"\"\"\"\"\"\"\",,,,,,,,,,,,,\n", 399 | ",,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"------::/($~|#~:----\"\"\"\"\"\"\"\"\",,,,,,,,,,\n", 400 | ",,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"-------:::/(*|~~~|(/::-----\"\"\"\"\"\"\"\",,,,,,,,\n", 401 | ",,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"--------::::///($~~~~~~~|(/::::----\"\"\"\"\"\",,,,,,,\n", 402 | ",,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"---------:::/**#***|$#~~~~~~%$||((//(~:---\"\"\"\"\",,,,,,\n", 403 | ",,,,,,\"\"\"\"\"\"\"\"\"\"\"-----------:::://*@~~~~~~~~~~~~~~~~~~~#@~~~~/:--\"\"\"\"\"\",,,,\n", 404 | ",,,\"\"\"\"\"\"\"\"\"\"\"----:::::::::::///(|~@~~~~~~~~~~~~~~~~~~~~~~~@(/:---\"\"\"\"\",,,,\n", 405 | ",\"\"\"\"\"\"\"\"\"-----::/|$(//(*((((((*|%~~~~~~~~~~~~~~~~~~~~~~~~~%$$/:--\"\"\"\"\"\",,,\n", 406 | "\"\"\"\"\"\"\"------:::/(*#~~~@~~%~$||$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/:---\"\"\"\"\"\",,\n", 407 | "\"\"\"\"------::::/((*#@~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$/:---\"\"\"\"\"\",,\n", 408 | "\"---:::::////(*@%~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/::---\"\"\"\"\"\",,\n", 409 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$*//::---\"\"\"\"\"\",,\n", 410 | "\"---:::::////(*@%~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/::---\"\"\"\"\"\",,\n", 411 | "\"\"\"\"------::::/((*#@~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~$/:---\"\"\"\"\"\",,\n", 412 | "\"\"\"\"\"\"\"------:::/(*#~~~@~~%~$||$~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/:---\"\"\"\"\"\",,\n", 413 | ",\"\"\"\"\"\"\"\"\"-----::/|$(//(*((((((*|%~~~~~~~~~~~~~~~~~~~~~~~~~%$$/:--\"\"\"\"\"\",,,\n", 414 | ",,,\"\"\"\"\"\"\"\"\"\"\"----:::::::::::///(|~@~~~~~~~~~~~~~~~~~~~~~~~@(/:---\"\"\"\"\",,,,\n", 415 | ",,,,,,\"\"\"\"\"\"\"\"\"\"\"-----------:::://*@~~~~~~~~~~~~~~~~~~~#@~~~~/:--\"\"\"\"\"\",,,,\n", 416 | ",,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"---------:::/**#***|$#~~~~~~%$||((//(~:---\"\"\"\"\",,,,,,\n", 417 | ",,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"--------::::///($~~~~~~~|(/::::----\"\"\"\"\"\",,,,,,,\n", 418 | ",,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"-------:::/(*|~~~|(/::-----\"\"\"\"\"\"\"\",,,,,,,,\n", 419 | ",,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"------::/($~|#~:----\"\"\"\"\"\"\"\"\",,,,,,,,,,\n", 420 | ",,,,,,,,,,,,,,,,,,,,,,,,,\"\"\"\"\"\"\"\"\"\"\"\"\"\"----:/~@:::---\"\"\"\"\"\"\"\"\",,,,,,,,,,,,,\n", 421 | "\n", 422 | "CPU times: user 68.6 ms, sys: 19.2 ms, total: 87.8 ms\n", 423 | "Wall time: 5.35 s\n" 424 | ] 425 | } 426 | ], 427 | "source": [ 428 | "%%time\n", 429 | "\n", 430 | "## Get a new plot updating the number of points we want\n", 431 | "!curl -s \\\n", 432 | " -H \"Accept: text/plain\" \\\n", 433 | " $OPENFAAS_URL/function/notebook-mandelbrot-clojure" 434 | ] 435 | }, 436 | { 437 | "cell_type": "markdown", 438 | "metadata": {}, 439 | "source": [ 440 | "## Singular Vector Decomposition in R" 441 | ] 442 | }, 443 | { 444 | "cell_type": "code", 445 | "execution_count": 8, 446 | "metadata": {}, 447 | "outputs": [ 448 | { 449 | "name": "stdout", 450 | "output_type": "stream", 451 | "text": [ 452 | "CPU times: user 399 µs, sys: 4.24 ms, total: 4.64 ms\n", 453 | "Wall time: 12.8 s\n" 454 | ] 455 | }, 456 | { 457 | "data": { 458 | "text/plain": [ 459 | "True" 460 | ] 461 | }, 462 | "execution_count": 8, 463 | "metadata": {}, 464 | "output_type": "execute_result" 465 | } 466 | ], 467 | "source": [ 468 | "%%time\n", 469 | "deploy_notebook(notebook_name=\"./notebook_svd_R.ipynb\", \n", 470 | " accept_parameters=False)" 471 | ] 472 | }, 473 | { 474 | "cell_type": "code", 475 | "execution_count": 9, 476 | "metadata": {}, 477 | "outputs": [ 478 | { 479 | "name": "stdout", 480 | "output_type": "stream", 481 | "text": [ 482 | "{\n", 483 | " \"d\": [13.0112, 0.8419, 9.9099e-17],\n", 484 | " \"u\": [\n", 485 | " [-0.4177, -0.8117, 0.4082],\n", 486 | " [-0.5647, -0.1201, -0.8165],\n", 487 | " [-0.7118, 0.5716, 0.4082]\n", 488 | " ],\n", 489 | " \"v\": [\n", 490 | " [-0.283, 0.7873, -0.3741],\n", 491 | " [-0.4132, 0.3595, 0.797],\n", 492 | " [-0.5434, -0.0683, -0.4717],\n", 493 | " [-0.6737, -0.4962, 0.0488]\n", 494 | " ]\n", 495 | "} \n", 496 | "CPU times: user 56.9 ms, sys: 30.3 ms, total: 87.3 ms\n", 497 | "Wall time: 4.14 s\n" 498 | ] 499 | } 500 | ], 501 | "source": [ 502 | "%%time\n", 503 | "\n", 504 | "!curl -s \\\n", 505 | " --data '{\\\n", 506 | " \"INPUT_MATRIX\":[[1,-2,3,1], \\\n", 507 | " [-1,1,-1,5,], \\\n", 508 | " [5,5,-5,5]], \\\n", 509 | " }' \\\n", 510 | " -H \"Accept: text/plain\" \\\n", 511 | " $OPENFAAS_URL/function/notebook-svd-r" 512 | ] 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": null, 517 | "metadata": {}, 518 | "outputs": [], 519 | "source": [] 520 | } 521 | ], 522 | "metadata": { 523 | "kernelspec": { 524 | "display_name": "Python 3", 525 | "language": "python", 526 | "name": "python3" 527 | }, 528 | "language_info": { 529 | "codemirror_mode": { 530 | "name": "ipython", 531 | "version": 3 532 | }, 533 | "file_extension": ".py", 534 | "mimetype": "text/x-python", 535 | "name": "python", 536 | "nbconvert_exporter": "python", 537 | "pygments_lexer": "ipython3", 538 | "version": "3.6.8" 539 | }, 540 | "toc": { 541 | "base_numbering": 1, 542 | "nav_menu": {}, 543 | "number_sections": false, 544 | "sideBar": false, 545 | "skip_h1_title": false, 546 | "title_cell": "Table of Contents", 547 | "title_sidebar": "Contents", 548 | "toc_cell": false, 549 | "toc_position": {}, 550 | "toc_section_display": false, 551 | "toc_window_display": false 552 | } 553 | }, 554 | "nbformat": 4, 555 | "nbformat_minor": 2 556 | } 557 | --------------------------------------------------------------------------------