├── 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| 1 | 2 | 3 | 4 |
\n",
45 | "\n",
46 | "
\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 | 
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 | 
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 |
--------------------------------------------------------------------------------