From 9f21307febbbc666096859f29a6b0642de349962 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 13:08:11 +0200 Subject: [PATCH 01/32] Add make targets to entangle and to clean entangled files --- INSTALL.md | 31 ++++++++++++++++++++++++++----- Makefile | 27 ++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index d6dd941..94cf71b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -23,8 +23,8 @@ entangled README.md INSTALL.md Or the [Entangled - Pandoc filters](https://github.com/entangled/filters) Docker image can be used -```shell -docker run --rm -ti --user $(id -u) -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md +```{.awk #pandoc-tangle} +docker run --rm -ti --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md ``` ## Command collection @@ -32,7 +32,20 @@ docker run --rm -ti --user $(id -u) -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 -- All the commands in the README.md can be captured in a Makefile like so: ```{.makefile file=Makefile} -.PHONY: clean test entangle py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm +.PHONY: clean clean-compiled clean-entangled test all entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm + +UID := $(shell id -u) +# Prevent suicide by excluding Makefile +ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.md INSTALL.md | grep -v Makefile | sort -u) +COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm + +entangle: README.md INSTALL.md + <> + +$(ENTANGLED): entangle + +entangled-list: + @echo $(ENTANGLED) py-deps: pip-pybind11 pip-flask pip-celery pip-connexion @@ -68,9 +81,17 @@ test-py: src/py/example.py src/py/newtonraphsonpy.*.so test: test-cli test-cgi test-py test-webservice +all: $(ENTANGLED) $(COMPILED) + +clean: clean-compiled clean-entangled + # Removes the compiled files -clean: - $(RM) bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm +clean-compiled: + $(RM) $(COMPILED) + +# Removed the entangled files +clean-entangled: + $(RM) $(ENTANGLED) start-redis: <> diff --git a/Makefile b/Makefile index 1a1fc32..a4a6cb6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,17 @@ -.PHONY: clean test entangle py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm +.PHONY: clean clean-compiled clean-entangled test all entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm + +UID := $(shell id -u) +# Exclude Makefile +ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.md INSTALL.md | grep -v Makefile | sort -u) +COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm + +entangle: README.md INSTALL.md + docker run --rm -ti --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md + +$(ENTANGLED): entangle + +entangled-list: + @echo $(ENTANGLED) py-deps: pip-pybind11 pip-flask pip-celery pip-connexion @@ -35,9 +48,17 @@ test-py: src/py/example.py src/py/newtonraphsonpy.*.so test: test-cli test-cgi test-py test-webservice +all: $(ENTANGLED) $(COMPILED) + +clean: clean-compiled clean-entangled + # Removes the compiled files -clean: - $(RM) bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm +clean-compiled: + $(RM) $(COMPILED) + +# Removed the entangled files +clean-entangled: + $(RM) $(ENTANGLED) start-redis: docker run --rm -d -p 6379:6379 --name some-redis redis From 6c228d3f24dd1ac375c79ca00d6188ee4260f980 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 14:47:59 +0200 Subject: [PATCH 02/32] Use git hook to always entangle before commit --- INSTALL.md | 41 +++++++++++++++++++++++++++++++++++++++++ Makefile | 8 ++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 94cf71b..d496604 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -27,6 +27,43 @@ Or the [Entangled - Pandoc filters](https://github.com/entangled/filters) Docker docker run --rm -ti --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md ``` +Or automatic generation during a commit. + +We will use a pre-commit git hook to generate code. + +The hook script runs entangle using Docker and adds newly written files to the current git commit. + +```{.awk file=.githooks/pre-commit} +#!/bin/sh + +UID=$(id -u) + +echo Entangling + +FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +[ -z "$FILES" ] && exit 0 +echo $FILES + +echo 'Adding written files to commit' +echo $FILES | xargs git add + +exit 0 +``` + +The hook must be made executable with + +```{.awk #hook-permission} +chmod +x .githooks/pre-commit +``` + +The git hook can be enabled with + +```{.awk #init-git-hook} +git config --local core.hooksPath .githooks +``` + +(`core.hooksPath` config is available in git version >= 2.9) + ## Command collection All the commands in the README.md can be captured in a Makefile like so: @@ -125,6 +162,9 @@ host-files: build-wasm test-wasm: <> +init-git-hook: + <> + <> ``` For example the Python dependencies can be installed with @@ -189,3 +229,4 @@ npx cypress run --config-file false The `npx` command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). The tests will also be run in the [GH Action continous integration build](.github/workflows/main.yml). + diff --git a/Makefile b/Makefile index a4a6cb6..2c15cc6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: clean clean-compiled clean-entangled test all entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) -# Exclude Makefile +# Prevent suicide by excluding Makefile ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.md INSTALL.md | grep -v Makefile | sort -u) COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm @@ -90,4 +90,8 @@ host-files: build-wasm python3 -m http.server 8000 test-wasm: - npx cypress run --config-file false \ No newline at end of file + npx cypress run --config-file false + +init-git-hook: + chmod +x .githooks/pre-commit + git config --local core.hooksPath .githooks \ No newline at end of file From 8c01662cabe87ffa528158243aac2d8c0b410a27 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 14:48:55 +0200 Subject: [PATCH 03/32] Add placeholder for .githooks dir --- .githooks/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .githooks/.gitkeep diff --git a/.githooks/.gitkeep b/.githooks/.gitkeep new file mode 100644 index 0000000..e69de29 From 6dee4f471e57bd72efb68da102012a7b559ba206 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 14:51:51 +0200 Subject: [PATCH 04/32] Add git hook script --- .githooks/pre-commit | 11 +++++++++++ INSTALL.md | 5 +---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100755 .githooks/pre-commit diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..c508591 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,11 @@ +#!/bin/sh + +UID=$(id -u) + +echo 'Check entangled files are up to date' + +FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +echo $FILES + +echo 'Adding written files to commit' +echo $FILES | xargs git add \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index d496604..42d9ca3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -38,16 +38,13 @@ The hook script runs entangle using Docker and adds newly written files to the c UID=$(id -u) -echo Entangling +echo 'Check entangled files are up to date' FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') -[ -z "$FILES" ] && exit 0 echo $FILES echo 'Adding written files to commit' echo $FILES | xargs git add - -exit 0 ``` The hook must be made executable with From d2670edde538af317c5da88b760b5a7cb8cdc6a5 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 14:53:47 +0200 Subject: [PATCH 05/32] Added entangle output files --- INSTALL.md | 1 - apache2/apache2.conf | 11 +++ cypress/integration/example-app_spec.js | 8 ++ .../integration/example-web-worker_spec.js | 7 ++ cypress/integration/example_spec.js | 7 ++ src/cgi-newtonraphson.cpp | 63 ++++++++++++++++ src/cli-newtonraphson.cpp | 52 +++++++++++++ src/js/app.js | 75 +++++++++++++++++++ src/js/example-app.html | 12 +++ src/js/example-web-worker.html | 23 ++++++ src/js/example.html | 19 +++++ src/js/worker.js | 23 ++++++ src/newtonraphson.hpp | 17 +++++ src/py-newtonraphson.cpp | 54 +++++++++++++ src/py/api.py | 8 ++ src/py/example.py | 6 ++ src/py/hello-templated.py | 10 +++ src/py/hello.py | 9 +++ src/py/openapi.yaml | 50 +++++++++++++ src/py/tasks.py | 19 +++++ src/py/templates/form.html | 9 +++ src/py/templates/hello.html | 8 ++ src/py/templates/result.html | 3 + src/py/webapp-celery.py | 33 ++++++++ src/py/webapp.py | 22 ++++++ src/py/webservice.py | 6 ++ src/wasm-newtonraphson.cpp | 49 ++++++++++++ 27 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 apache2/apache2.conf create mode 100644 cypress/integration/example-app_spec.js create mode 100644 cypress/integration/example-web-worker_spec.js create mode 100644 cypress/integration/example_spec.js create mode 100644 src/cgi-newtonraphson.cpp create mode 100644 src/cli-newtonraphson.cpp create mode 100644 src/js/app.js create mode 100644 src/js/example-app.html create mode 100644 src/js/example-web-worker.html create mode 100644 src/js/example.html create mode 100644 src/js/worker.js create mode 100644 src/newtonraphson.hpp create mode 100644 src/py-newtonraphson.cpp create mode 100644 src/py/api.py create mode 100644 src/py/example.py create mode 100644 src/py/hello-templated.py create mode 100644 src/py/hello.py create mode 100644 src/py/openapi.yaml create mode 100644 src/py/tasks.py create mode 100644 src/py/templates/form.html create mode 100644 src/py/templates/hello.html create mode 100644 src/py/templates/result.html create mode 100644 src/py/webapp-celery.py create mode 100644 src/py/webapp.py create mode 100644 src/py/webservice.py create mode 100644 src/wasm-newtonraphson.cpp diff --git a/INSTALL.md b/INSTALL.md index 42d9ca3..ff3fdce 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -226,4 +226,3 @@ npx cypress run --config-file false The `npx` command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). The tests will also be run in the [GH Action continous integration build](.github/workflows/main.yml). - diff --git a/apache2/apache2.conf b/apache2/apache2.conf new file mode 100644 index 0000000..6961d67 --- /dev/null +++ b/apache2/apache2.conf @@ -0,0 +1,11 @@ +# this Apache2 configuration snippet is stored as apache2/apache2.conf +ServerName 127.0.0.1 +Listen 8080 +LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so +LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so +LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so +LoadModule cgi_module /usr/lib/apache2/modules/mod_cgi.so +ErrorLog httpd_error_log +PidFile httpd.pid + +ScriptAlias "/cgi-bin/" "cgi-bin/" \ No newline at end of file diff --git a/cypress/integration/example-app_spec.js b/cypress/integration/example-app_spec.js new file mode 100644 index 0000000..f831bac --- /dev/null +++ b/cypress/integration/example-app_spec.js @@ -0,0 +1,8 @@ +describe('src/js/example-app.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-app.html'); + cy.get('input[name=guess]').type('-30'); + cy.contains('Submit').click(); + cy.get('#answer').contains('-1.00'); + }); +}); \ No newline at end of file diff --git a/cypress/integration/example-web-worker_spec.js b/cypress/integration/example-web-worker_spec.js new file mode 100644 index 0000000..fcc8b24 --- /dev/null +++ b/cypress/integration/example-web-worker_spec.js @@ -0,0 +1,7 @@ +// this JavaScript snippet is run by cypress and is stored as cypress/integration/example-web-worker_spec.js +describe('src/js/example-web-worker.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-web-worker.html'); + cy.get('#answer').contains('-1.00'); + }); +}); \ No newline at end of file diff --git a/cypress/integration/example_spec.js b/cypress/integration/example_spec.js new file mode 100644 index 0000000..023a8fd --- /dev/null +++ b/cypress/integration/example_spec.js @@ -0,0 +1,7 @@ +// this JavaScript snippet is run by cypress and is stored as cypress/integration/example_spec.js +describe('src/js/example.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example.html'); + cy.get('#answer').contains('-1.00'); + }); +}); \ No newline at end of file diff --git a/src/cgi-newtonraphson.cpp b/src/cgi-newtonraphson.cpp new file mode 100644 index 0000000..fa2ebd8 --- /dev/null +++ b/src/cgi-newtonraphson.cpp @@ -0,0 +1,63 @@ +// this C++ snippet is stored as src/cgi-newtonraphson.hpp +#include +#include +#include + +// this C++ code snippet is later referred to as <> +#include "newtonraphson.hpp" + +namespace rootfinding +{ + +// An example function is x^3 - x^2 + 2 +double func(double x) +{ + return x * x * x - x * x + 2; +} + +// Derivative of the above function which is 3*x^x - 2*x +double derivFunc(double x) +{ + return 3 * x * x - 2 * x; +} + +NewtonRaphson::NewtonRaphson(double tolerancein) : tolerance(tolerancein) {} + +// Function to find the root +double NewtonRaphson::find(double xin) +{ + double x = xin; + double delta_x = func(x) / derivFunc(x); + while (abs(delta_x) >= tolerance) + { + delta_x = func(x) / derivFunc(x); + + // x_new = x_old - f(x) / f'(x) + x = x - delta_x; + } + return x; +}; + + +} // namespace rootfinding + +int main(int argc, char *argv[]) +{ + std::cout << "Content-type: application/json" << std::endl << std::endl; + + // Retrieve epsilon and guess from request body + nlohmann::json request(nlohmann::json::parse(std::cin)); + double epsilon = request["epsilon"]; + double guess = request["guess"]; + + // Find root + rootfinding::NewtonRaphson finder(epsilon); + double root = finder.find(guess); + + // Assemble response + nlohmann::json response; + response["guess"] = guess; + response["root"] = root; + std::cout << response.dump(2) << std::endl; + return 0; +} \ No newline at end of file diff --git a/src/cli-newtonraphson.cpp b/src/cli-newtonraphson.cpp new file mode 100644 index 0000000..b72e168 --- /dev/null +++ b/src/cli-newtonraphson.cpp @@ -0,0 +1,52 @@ +// this C++ snippet is stored as src/newtonraphson.cpp +#include + +// this C++ code snippet is later referred to as <> +#include "newtonraphson.hpp" + +namespace rootfinding +{ + +// An example function is x^3 - x^2 + 2 +double func(double x) +{ + return x * x * x - x * x + 2; +} + +// Derivative of the above function which is 3*x^x - 2*x +double derivFunc(double x) +{ + return 3 * x * x - 2 * x; +} + +NewtonRaphson::NewtonRaphson(double tolerancein) : tolerance(tolerancein) {} + +// Function to find the root +double NewtonRaphson::find(double xin) +{ + double x = xin; + double delta_x = func(x) / derivFunc(x); + while (abs(delta_x) >= tolerance) + { + delta_x = func(x) / derivFunc(x); + + // x_new = x_old - f(x) / f'(x) + x = x - delta_x; + } + return x; +}; + + +} // namespace rootfinding + +// Driver program to test above +int main() +{ + double x0 = -20; // Initial values assumed + double epsilon = 0.001; + rootfinding::NewtonRaphson finder(epsilon); + double x1 = finder.find(x0); + + std::cout << "The value of the root is : " << x1 << std::endl; + return 0; +} \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..a320aff --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,75 @@ +// this JavaScript snippet is stored as src/js/app.js +function Heading() { + const title = 'Root finding web application'; + return

{title}

+} +// this JavaScript snippet stored as src/js/app.js +function Result(props) { + const root = props.root; + let message = 'Not submitted'; + if (root !== undefined) { + message = 'Root = ' + root; + } + return
{message}
; +} +// this JavaScript snippet appenended to src/js/app.js +function App() { + // this JavaScript snippet is later referred to as <> + const [epsilon, setEpsilon] = React.useState(0.001); + // this JavaScript snippet is appended to <> + function onEpsilonChange(event) { + setEpsilon(event.target.value); + } + // this JavaScript snippet is appended to <> + const [guess, setGuess] = React.useState(-20); + + function onGuessChange(event) { + setGuess(event.target.value); + } + // this JavaScript snippet is appended to <> + const [root, setRoot] = React.useState(undefined); + + function handleSubmit(event) { + // this JavaScript snippet is later referred to as <> + event.preventDefault(); + // this JavaScript snippet is appended to <> + const worker = new Worker('worker.js'); + // this JavaScript snippet is appended to <> + worker.postMessage({ + type: 'CALCULATE', + payload: { epsilon: epsilon, guess: guess } + }); + // this JavaScript snippet is appended to <> + worker.onmessage = function(message) { + if (message.data.type === 'RESULT') { + const result = message.data.payload.root; + setRoot(result); + worker.terminate(); + } + }; + } + + return ( +
+ + { /* this JavaScript snippet is later referred to as <> */ } +
+ + + +
+ +
+ ); +} +// this JavaScript snippet appenended to src/js/app.js +ReactDOM.render( + , + document.getElementById('container') +); \ No newline at end of file diff --git a/src/js/example-app.html b/src/js/example-app.html new file mode 100644 index 0000000..e24de67 --- /dev/null +++ b/src/js/example-app.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/js/example-web-worker.html b/src/js/example-web-worker.html new file mode 100644 index 0000000..170077a --- /dev/null +++ b/src/js/example-web-worker.html @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/src/js/example.html b/src/js/example.html new file mode 100644 index 0000000..3ec769c --- /dev/null +++ b/src/js/example.html @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/src/js/worker.js b/src/js/worker.js new file mode 100644 index 0000000..53f7855 --- /dev/null +++ b/src/js/worker.js @@ -0,0 +1,23 @@ +// this JavaScript snippet is stored as src/js/worker.js +importScripts('newtonraphsonwasm.js'); + +// this JavaScript snippet is later referred to as <> +onmessage = function(message) { + // this JavaScript snippet is before referred to as <> + if (message.data.type === 'CALCULATE') { + createModule().then((module) => { + // this JavaScript snippet is before referred to as <> + const epsilon = message.data.payload.epsilon; + const finder = new module.NewtonRaphson(epsilon); + const guess = message.data.payload.guess; + const root = finder.find(guess); + // this JavaScript snippet is before referred to as <> + postMessage({ + type: 'RESULT', + payload: { + root: root + } + }); + }); + } +}; \ No newline at end of file diff --git a/src/newtonraphson.hpp b/src/newtonraphson.hpp new file mode 100644 index 0000000..7cc6647 --- /dev/null +++ b/src/newtonraphson.hpp @@ -0,0 +1,17 @@ +// this C++ snippet is stored as src/newtonraphson.hpp +#ifndef H_NEWTONRAPHSON_H +#define H_NEWTONRAPHSON_H + +#include + +namespace rootfinding { + class NewtonRaphson { + public: + NewtonRaphson(double tolerancein); + double find(double xin); + private: + double tolerance; + }; +} + +#endif \ No newline at end of file diff --git a/src/py-newtonraphson.cpp b/src/py-newtonraphson.cpp new file mode 100644 index 0000000..2555b13 --- /dev/null +++ b/src/py-newtonraphson.cpp @@ -0,0 +1,54 @@ +// this C++ snippet is stored as src/py-newtonraphson.cpp +#include +#include + +// this C++ code snippet is later referred to as <> +#include "newtonraphson.hpp" + +namespace rootfinding +{ + +// An example function is x^3 - x^2 + 2 +double func(double x) +{ + return x * x * x - x * x + 2; +} + +// Derivative of the above function which is 3*x^x - 2*x +double derivFunc(double x) +{ + return 3 * x * x - 2 * x; +} + +NewtonRaphson::NewtonRaphson(double tolerancein) : tolerance(tolerancein) {} + +// Function to find the root +double NewtonRaphson::find(double xin) +{ + double x = xin; + double delta_x = func(x) / derivFunc(x); + while (abs(delta_x) >= tolerance) + { + delta_x = func(x) / derivFunc(x); + + // x_new = x_old - f(x) / f'(x) + x = x - delta_x; + } + return x; +}; + + +} // namespace rootfinding + +namespace py = pybind11; + +PYBIND11_MODULE(newtonraphsonpy, m) { + py::class_(m, "NewtonRaphson") + .def(py::init(), py::arg("epsilon")) + .def("find", + &rootfinding::NewtonRaphson::find, + py::arg("guess"), + "Find root starting from initial guess" + ) + ; +} \ No newline at end of file diff --git a/src/py/api.py b/src/py/api.py new file mode 100644 index 0000000..1760858 --- /dev/null +++ b/src/py/api.py @@ -0,0 +1,8 @@ +# this Python snippet is stored as src/py/api.py +def calculate(body): + epsilon = body['epsilon'] + guess = body['guess'] + from newtonraphsonpy import NewtonRaphson + finder = NewtonRaphson(epsilon) + root = finder.find(guess) + return {'root': root} \ No newline at end of file diff --git a/src/py/example.py b/src/py/example.py new file mode 100644 index 0000000..3894153 --- /dev/null +++ b/src/py/example.py @@ -0,0 +1,6 @@ +# this Python snippet is stored as src/py/example.py +from newtonraphsonpy import NewtonRaphson + +finder = NewtonRaphson(epsilon=0.001) +root = finder.find(guess=-20) +print(root) \ No newline at end of file diff --git a/src/py/hello-templated.py b/src/py/hello-templated.py new file mode 100644 index 0000000..2c3144d --- /dev/null +++ b/src/py/hello-templated.py @@ -0,0 +1,10 @@ +# this Python snippet is stored as src/py/hello-templated.py +from flask import Flask, render_template + +app = Flask(__name__) + +@app.route('/hello/') +def hello_name(name=None): + return render_template('hello.html', name=name) + +app.run() \ No newline at end of file diff --git a/src/py/hello.py b/src/py/hello.py new file mode 100644 index 0000000..ebb2637 --- /dev/null +++ b/src/py/hello.py @@ -0,0 +1,9 @@ +# this Python snippet is stored as src/py/hello.py +from flask import Flask +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello World!" + +app.run() \ No newline at end of file diff --git a/src/py/openapi.yaml b/src/py/openapi.yaml new file mode 100644 index 0000000..3ff1f97 --- /dev/null +++ b/src/py/openapi.yaml @@ -0,0 +1,50 @@ +# this yaml snippet is stored as src/py/openapi.yaml +openapi: 3.0.0 +info: + title: Root finder + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: 0.1.0 +paths: + /api/newtonraphson: + post: + description: Perform root finding with the Newton Raphson algorithm + operationId: api.calculate + requestBody: + content: + 'application/json': + schema: + $ref: '#/components/schemas/NRRequest' + example: + epsilon: 0.001 + guess: -20 + responses: + '200': + description: The found root + content: + application/json: + schema: + $ref: '#/components/schemas/NRResponse' +components: + schemas: + NRRequest: + type: object + properties: + epsilon: + type: number + minimum: 0 + guess: + type: number + required: + - epsilon + - guess + additionalProperties: false + NRResponse: + type: object + properties: + root: + type: number + required: + - root + additionalProperties: false \ No newline at end of file diff --git a/src/py/tasks.py b/src/py/tasks.py new file mode 100644 index 0000000..9554c0a --- /dev/null +++ b/src/py/tasks.py @@ -0,0 +1,19 @@ +# this Python snippet is stored as src/py/tasks.py +import time + +# this Python code snippet is later referred to as <> +from celery import Celery +capp = Celery('tasks', broker='redis://localhost:6379', backend='redis://localhost:6379') + +@capp.task(bind=True) +def calculate(self, epsilon, guess): + if not self.request.called_directly: + self.update_state(state='INITIALIZING') + time.sleep(5) + from newtonraphsonpy import NewtonRaphson + finder = NewtonRaphson(epsilon) + if not self.request.called_directly: + self.update_state(state='FINDING') + time.sleep(5) + root = finder.find(guess) + return {'root': root, 'guess': guess, 'epsilon':epsilon} \ No newline at end of file diff --git a/src/py/templates/form.html b/src/py/templates/form.html new file mode 100644 index 0000000..96059fd --- /dev/null +++ b/src/py/templates/form.html @@ -0,0 +1,9 @@ +{# this Jinja2 template snippet is stored as src/py/templates/form.html #} + +
+ + + + + +
\ No newline at end of file diff --git a/src/py/templates/hello.html b/src/py/templates/hello.html new file mode 100644 index 0000000..261046f --- /dev/null +++ b/src/py/templates/hello.html @@ -0,0 +1,8 @@ +{# this Jinja2 template snippet is stored as src/py/templates/hello.html #} + +Hello from Flask +{% if name %} +

Hello {{ name }}!

+{% else %} +

Hello, World!

+{% endif %} \ No newline at end of file diff --git a/src/py/templates/result.html b/src/py/templates/result.html new file mode 100644 index 0000000..edf3ebc --- /dev/null +++ b/src/py/templates/result.html @@ -0,0 +1,3 @@ +{# this Jinja2 template snippet is stored as src/py/templates/result.html #} + +

With epsilon of {{ epsilon }} and a guess of {{ guess }} the found root is {{ root }}.

\ No newline at end of file diff --git a/src/py/webapp-celery.py b/src/py/webapp-celery.py new file mode 100644 index 0000000..922895a --- /dev/null +++ b/src/py/webapp-celery.py @@ -0,0 +1,33 @@ +# this Python snippet is stored as src/py/webapp-celery.py +from flask import Flask, render_template, request, redirect, url_for + +app = Flask(__name__) + +# this Python code snippet is later referred to as <> +@app.route('/', methods=['GET']) +def form(): + return render_template('form.html') + +# this Python code snippet is later referred to as <> +@app.route('/', methods=['POST']) +def submit(): + epsilon = float(request.form['epsilon']) + guess = float(request.form['guess']) + from tasks import calculate + job = calculate.delay(epsilon, guess) + return redirect(url_for('result', jobid=job.id)) + +# this Python code snippet is later referred to as <> +@app.route('/result/') +def result(jobid): + from tasks import capp + job = capp.AsyncResult(jobid) + job.maybe_throw() + if job.successful(): + result = job.get() + return render_template('result.html', epsilon=result['epsilon'], guess=result['guess'], root=result['root']) + else: + return job.status + +if __name__ == '__main__': + app.run(port=5000) \ No newline at end of file diff --git a/src/py/webapp.py b/src/py/webapp.py new file mode 100644 index 0000000..f518077 --- /dev/null +++ b/src/py/webapp.py @@ -0,0 +1,22 @@ +# this Python snippet is stored as src/py/webapp.py +from flask import Flask, render_template, request +app = Flask(__name__) + +# this Python code snippet is later referred to as <> +@app.route('/', methods=['GET']) +def form(): + return render_template('form.html') + +# this Python code snippet is later referred to as <> +@app.route('/', methods=['POST']) +def calculate(): + epsilon = float(request.form['epsilon']) + guess = float(request.form['guess']) + + from newtonraphsonpy import NewtonRaphson + finder = NewtonRaphson(epsilon) + root = finder.find(guess) + + return render_template('result.html', epsilon=epsilon, guess=guess, root=root) + +app.run(port=5001) \ No newline at end of file diff --git a/src/py/webservice.py b/src/py/webservice.py new file mode 100644 index 0000000..bfaabcf --- /dev/null +++ b/src/py/webservice.py @@ -0,0 +1,6 @@ +# this Python snippet is stored as src/py/webservice.py +import connexion + +app = connexion.App(__name__) +app.add_api('openapi.yaml', validate_responses=True) +app.run(port=8080) \ No newline at end of file diff --git a/src/wasm-newtonraphson.cpp b/src/wasm-newtonraphson.cpp new file mode 100644 index 0000000..a632904 --- /dev/null +++ b/src/wasm-newtonraphson.cpp @@ -0,0 +1,49 @@ +// this C++ snippet is stored as src/wasm-newtonraphson.cpp +#include + +// this C++ code snippet is later referred to as <> +#include "newtonraphson.hpp" + +namespace rootfinding +{ + +// An example function is x^3 - x^2 + 2 +double func(double x) +{ + return x * x * x - x * x + 2; +} + +// Derivative of the above function which is 3*x^x - 2*x +double derivFunc(double x) +{ + return 3 * x * x - 2 * x; +} + +NewtonRaphson::NewtonRaphson(double tolerancein) : tolerance(tolerancein) {} + +// Function to find the root +double NewtonRaphson::find(double xin) +{ + double x = xin; + double delta_x = func(x) / derivFunc(x); + while (abs(delta_x) >= tolerance) + { + delta_x = func(x) / derivFunc(x); + + // x_new = x_old - f(x) / f'(x) + x = x - delta_x; + } + return x; +}; + + +} // namespace rootfinding + +using namespace emscripten; + +EMSCRIPTEN_BINDINGS(newtonraphsonwasm) { + class_("NewtonRaphson") + .constructor() + .function("find", &rootfinding::NewtonRaphson::find) + ; +} \ No newline at end of file From 1a843bd4bf77acf8e51597970129d8728e4726c7 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 15:04:08 +0200 Subject: [PATCH 06/32] Added Perl as dep --- INSTALL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/INSTALL.md b/INSTALL.md index ff3fdce..2f71be0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -9,6 +9,7 @@ To run the commands in the README.md the following items are required 1. Python development, install with `sudo apt install -y python3-dev` 1. [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) 1. [Docker Engine](https://docs.docker.com/install/), setup so `docker` command can be run [without sudo](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user). +1. [Perl](https://www.perl.org/), already installed on Linux ## Generating code from Markdown From 9fb745ba9e48353968dbd561228bdb1d729a53ef Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 15:07:22 +0200 Subject: [PATCH 07/32] Only git add when files are written --- .githooks/pre-commit | 1 + INSTALL.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index c508591..d6c356a 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,6 +5,7 @@ UID=$(id -u) echo 'Check entangled files are up to date' FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +[ -z "$FILES" ] && exit 0 echo $FILES echo 'Adding written files to commit' diff --git a/INSTALL.md b/INSTALL.md index 2f71be0..fadfb0d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -42,6 +42,7 @@ UID=$(id -u) echo 'Check entangled files are up to date' FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +[ -z "$FILES" ] && exit 0 echo $FILES echo 'Adding written files to commit' From feeef08d63b666a43cba833864590d7a30d192c4 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 15:26:50 +0200 Subject: [PATCH 08/32] In GH action dont entangle but check entangle is up to date --- .github/workflows/main.yml | 118 +++++++++++++++++-------------------- INSTALL.md | 5 +- Makefile | 7 ++- 3 files changed, 62 insertions(+), 68 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4d2cbb..86f1b7c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,25 +8,25 @@ on: [push, pull_request] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: + entangle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check all entangled files are in sync with Markdown + run: make check cpp: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - # Should not be needed anymore when https://github.com/NLESC-JCER/cpp2wasm/issues/1 is fixed - - name: Generate source code - uses: docker://nlesc/pandoc-tangle:0.5.0 - with: - args: --preserve-tabs README.md INSTALL.md + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - - name: Run C++ examples - run: make test-cli test-cgi + - name: Run C++ examples + run: make test-cli test-cgi python: - # The type of runner that the job will run on + # The type of runner that the job will run on runs-on: ubuntu-latest # Redis is needed for Celery services: @@ -35,65 +35,53 @@ jobs: ports: - 6379:6379 steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - # Should not be needed anymore when https://github.com/NLESC-JCER/cpp2wasm/issues/1 is fixed - - name: Generate source code - uses: docker://nlesc/pandoc-tangle - with: - args: --preserve-tabs README.md INSTALL.md - - - uses: actions/setup-python@v1 - with: - python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax - architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified - - - name: Install Python dependencies - run: make py-deps && pip install httpie - - - name: Run Python example - run: make test-py - - - name: Start web application in background - run: make run-webapp & - - - name: Test web application - run: http --ignore-stdin -f localhost:5001 epsilon=0.001 guess=-20 - - - name: Start web service in background - run: make run-webservice & - - - name: Test web service - run: make test-webservice - - - name: Start Celery web app in background - run: make run-celery-webapp & - - - name: Start Celery worker in background - run: | - cd src/py - PYTHONPATH=$PWD/../.. celery -A tasks worker & - cd ../.. - - - name: Test Celery web app - run: | - http --ignore-stdin -hf localhost:5000 epsilon=0.001 guess=-20 | tee response.txt - # Parse result url from response - RESULT_URL=$(cat response.txt |grep Location |awk '{print $2}') - sleep 2 - http --ignore-stdin $RESULT_URL + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v1 + with: + python-version: "3.x" # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: "x64" # optional x64 or x86. Defaults to x64 if not specified + + - name: Install Python dependencies + run: make py-deps && pip install httpie + + - name: Run Python example + run: make test-py + + - name: Start web application in background + run: make run-webapp & + + - name: Test web application + run: http --ignore-stdin -f localhost:5001 epsilon=0.001 guess=-20 + + - name: Start web service in background + run: make run-webservice & + + - name: Test web service + run: make test-webservice + + - name: Start Celery web app in background + run: make run-celery-webapp & + + - name: Start Celery worker in background + run: | + cd src/py + PYTHONPATH=$PWD/../.. celery -A tasks worker & + cd ../.. + + - name: Test Celery web app + run: | + http --ignore-stdin -hf localhost:5000 epsilon=0.001 guess=-20 | tee response.txt + # Parse result url from response + RESULT_URL=$(cat response.txt |grep Location |awk '{print $2}') + sleep 2 + http --ignore-stdin $RESULT_URL wasm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - # Should not be needed anymore when https://github.com/NLESC-JCER/cpp2wasm/issues/1 is fixed - - name: Generate source code - uses: docker://nlesc/pandoc-tangle - with: - args: --preserve-tabs README.md INSTALL.md - - name: Install emscripten uses: mymindstorm/setup-emsdk@v4 diff --git a/INSTALL.md b/INSTALL.md index fadfb0d..712e0a4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -68,7 +68,7 @@ git config --local core.hooksPath .githooks All the commands in the README.md can be captured in a Makefile like so: ```{.makefile file=Makefile} -.PHONY: clean clean-compiled clean-entangled test all entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm +.PHONY: clean clean-compiled clean-entangled test all check entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) # Prevent suicide by excluding Makefile @@ -164,6 +164,9 @@ test-wasm: init-git-hook: <> <> + +check: entangle + git diff-index --quiet HEAD -- ``` For example the Python dependencies can be installed with diff --git a/Makefile b/Makefile index 2c15cc6..a033ffd 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean clean-compiled clean-entangled test all entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm +.PHONY: clean clean-compiled clean-entangled test all check entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) # Prevent suicide by excluding Makefile @@ -94,4 +94,7 @@ test-wasm: init-git-hook: chmod +x .githooks/pre-commit - git config --local core.hooksPath .githooks \ No newline at end of file + git config --local core.hooksPath .githooks + +check: entangle + git diff-index --quiet HEAD -- \ No newline at end of file From 2217bb085a26c78365d6ca33a80b2f1c19ade362 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 15:37:36 +0200 Subject: [PATCH 09/32] Dont use `-ti` in Docker command as it is not needed --- INSTALL.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 712e0a4..74f2843 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -25,7 +25,7 @@ entangled README.md INSTALL.md Or the [Entangled - Pandoc filters](https://github.com/entangled/filters) Docker image can be used ```{.awk #pandoc-tangle} -docker run --rm -ti --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md +docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md ``` Or automatic generation during a commit. diff --git a/README.md b/README.md index 3c3da4a..43cbcda 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Document describing a way that a researcher with a C++ algorithm can make it ava - as a Python application via pybind11, Flask and Celery - in the web browser using web assembly and JavaScript -We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide and method to convert the code snippets to files are described in the [INSTALL.md](INSTALL.md) document. +We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide and method to convert the code snippets to files are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md) The [Newton-Raphson root finding algorithm](https://en.wikipedia.org/wiki/Newton%27s_method) will be the use case. The algorithm is explained in [this video series](https://www.youtube.com/watch?v=cOmAk82cr9M). From 527d398debca6bb5b016d4340f19529465af58fb Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 15:39:28 +0200 Subject: [PATCH 10/32] Updated Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a033ffd..38d2474 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.m COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm entangle: README.md INSTALL.md - docker run --rm -ti --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md + docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md $(ENTANGLED): entangle From 4a4687482cda50ecf9aca3a622873f71708f9241 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 16:19:12 +0200 Subject: [PATCH 11/32] Added CONTRIBUTING.md + Move testing to TESTING.md + Moved code generation to CONTRIBUTING.md --- .githooks/pre-commit | 2 +- CONTRIBUTING.md | 176 +++++++++++++++++++++++++++++++++++++++++++ INSTALL.md | 115 +--------------------------- Makefile | 6 +- README.md | 2 +- TESTING.md | 55 ++++++++++++++ 6 files changed, 240 insertions(+), 116 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 TESTING.md diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d6c356a..70a1135 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -4,7 +4,7 @@ UID=$(id -u) echo 'Check entangled files are up to date' -FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') [ -z "$FILES" ] && exit 0 echo $FILES diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..35118eb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,176 @@ +# Contributing + +- [Contributing](#contributing) + - [Types of Contributions](#types-of-contributions) + - [Report Bugs](#report-bugs) + - [Fix Bugs](#fix-bugs) + - [Implement Features](#implement-features) + - [Submit Feedback](#submit-feedback) + - [Get Started!](#get-started) + - [Pull Request Guidelines](#pull-request-guidelines) + - [Tips](#tips) + - [Generating code from Markdown](#generating-code-from-markdown) + - [Generate code from Markdown and vice versa](generate-code-from-markdown-and-vice-versa) + - [Generate code from Markdown on commit](#generate-code-from-markdown-on-commit) + - [New release](#new-release) + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +## Types of Contributions + +### Report Bugs + +Report bugs at [https://github.com/NLESC-JCER/cpp2wasm/issues](https://github.com/NLESC-JCER/cpp2wasm/issues). + +If you are reporting a bug, please include: + +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. + +### Fix Bugs + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +### Implement Features + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +### Submit Feedback + +The best way to send feedback is to file an issue at [https://github.com/NLESC-JCER/cpp2wasm/issues](https://github.com/NLESC-JCER/cpp2wasm/issues). + +If you are proposing a feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +## Get Started! + +Ready to contribute? Here's how to set up `cpp2wasm` for local development. + +1. Fork the `cpp2wasm` repo on GitHub. +2. Clone your fork locally:: + + ```shell + git clone git@github.com:your_name_here/cpp2wasm.git + ``` + +3. Install the dependencies as listed in [INSTALL.md#dependencies](INSTALL.md#dependencies). + +4. Create a branch for local development:: + + ```shell + git checkout -b name-of-your-bugfix-or-feature + ``` + + Now you can make your changes locally. + +5. Write tests where possible. Writing tests should be done in a literate way in [TESTING.md](TESTING.md) + +6. When you're done making changes, make sure the Markdown and source code files are entangled with. + + ```shell + make entangle + ``` + +7. Commit your changes and push your branch to GitHub:: + + ```shell + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + ``` + +8. Submit a pull request through the GitHub website. + +## Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check + https://travis-ci.com/{{ cookiecutter.github_username }}/cpp2wasm/pull_requests + and make sure that the tests pass for all supported Python versions. + +## Tips + +## Generating code from Markdown + +The [Entangled - Pandoc filters](https://github.com/entangled/filters) Docker image can be used to generate source code files from the Markdown files. + +```{.awk #pandoc-tangle} +docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md +``` + +## Generate code from Markdown and vice versa + +Use Entangled deamon to convert code blocks in Markdown to and from source code files. +Each time a Markdown code block is changed the source code files will be updated. +Each time a source code file is changed the code blocks in the Markdown files will be updated. + +1. Install [entangled](https://github.com/entangled/entangled) +2. Run entangled daemon with + +```shell +entangled *.md +``` + +### Generate code from Markdown on commit + +To automatically generate code from Markdown on each commit, initialize the git hook with. + +```shell +make init-git-hook +``` + +The rest of this section describes how the git hook works. + +The pre-commit hook script runs entangle using Docker and adds newly written files to the current git commit. + +```{.awk file=.githooks/pre-commit} +#!/bin/sh + +UID=$(id -u) + +echo 'Check entangled files are up to date' + +FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +[ -z "$FILES" ] && exit 0 +echo $FILES + +echo 'Adding written files to commit' +echo $FILES | xargs git add +``` + +The hook must be made executable with + +```{.awk #hook-permission} +chmod +x .githooks/pre-commit +``` + +The git hook can be enabled with + +```{.awk #init-git-hook} +git config --local core.hooksPath .githooks +``` + +(`core.hooksPath` config is available in git version >= 2.9) + +## New release + +A reminder for the maintainers on how to create a new release. + +1. Make sure all your changes are committed. +1. Create a GitHub release +1. Check and fix author list on Zenodo diff --git a/INSTALL.md b/INSTALL.md index 74f2843..b8ab30d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -11,71 +11,19 @@ To run the commands in the README.md the following items are required 1. [Docker Engine](https://docs.docker.com/install/), setup so `docker` command can be run [without sudo](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user). 1. [Perl](https://www.perl.org/), already installed on Linux -## Generating code from Markdown - -Entangled is used to convert code blocks in Markdown to source code files. - -1. Install [entangled](https://github.com/entangled/entangled) -2. Run entangled daemon with - -```shell -entangled README.md INSTALL.md -``` - -Or the [Entangled - Pandoc filters](https://github.com/entangled/filters) Docker image can be used - -```{.awk #pandoc-tangle} -docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md -``` - -Or automatic generation during a commit. - -We will use a pre-commit git hook to generate code. - -The hook script runs entangle using Docker and adds newly written files to the current git commit. - -```{.awk file=.githooks/pre-commit} -#!/bin/sh - -UID=$(id -u) - -echo 'Check entangled files are up to date' - -FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') -[ -z "$FILES" ] && exit 0 -echo $FILES - -echo 'Adding written files to commit' -echo $FILES | xargs git add -``` - -The hook must be made executable with - -```{.awk #hook-permission} -chmod +x .githooks/pre-commit -``` - -The git hook can be enabled with - -```{.awk #init-git-hook} -git config --local core.hooksPath .githooks -``` - -(`core.hooksPath` config is available in git version >= 2.9) - ## Command collection -All the commands in the README.md can be captured in a Makefile like so: +All the commands in the [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md) can be captured in a [Makefile](https://en.wikipedia.org/wiki/Makefile) like so: ```{.makefile file=Makefile} .PHONY: clean clean-compiled clean-entangled test all check entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) # Prevent suicide by excluding Makefile -ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.md INSTALL.md | grep -v Makefile | sort -u) +ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' *.md | grep -v Makefile | sort -u) COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm -entangle: README.md INSTALL.md +entangle: *.md <> $(ENTANGLED): entangle @@ -175,59 +123,4 @@ For example the Python dependencies can be installed with make py-deps ``` -See [GitHub Actions workflow](.github/workflows/main.yml) for other usages of the Makefile. - -## Tests - -To make sure WebAssembly module code snippets work we want have a tests for it. -To test the WebAssembly module we will use the [cypress](https://www.cypress.io/) test framework. -Cypress can simulate what a user would do and expect in a web browser. - -We want to test if visiting the example web page renders the answer `-1.00`. - -First a test for the direct WebAssembly example. - -```{.js file=cypress/integration/example_spec.js} -// this JavaScript snippet is run by cypress and is stored as cypress/integration/example_spec.js -describe('src/js/example.html', () => { - it('should render -1.00', () => { - cy.visit('http://localhost:8000/src/js/example.html'); - cy.get('#answer').contains('-1.00'); - }); -}); -``` - -Second a test for the WebAssembly called through a web worker. - -```{.js file=cypress/integration/example-web-worker_spec.js} -// this JavaScript snippet is run by cypress and is stored as cypress/integration/example-web-worker_spec.js -describe('src/js/example-web-worker.html', () => { - it('should render -1.00', () => { - cy.visit('http://localhost:8000/src/js/example-web-worker.html'); - cy.get('#answer').contains('-1.00'); - }); -}); -``` - -And lastly a test for the full React/form/Web worker/WebAssembly combination. - -```{.js file=cypress/integration/example-app_spec.js} -describe('src/js/example-app.html', () => { - it('should render -1.00', () => { - cy.visit('http://localhost:8000/src/js/example-app.html'); - cy.get('input[name=guess]').type('-30'); - cy.contains('Submit').click(); - cy.get('#answer').contains('-1.00'); - }); -}); -``` - -The test can be run with - -```{.awk #test-wasm} -npx cypress run --config-file false -``` - -The `npx` command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). - -The tests will also be run in the [GH Action continous integration build](.github/workflows/main.yml). +See [GitHub Actions workflow](.github/workflows/main.yml) and [CONTRIBUTING.md](CONTRIBUTING.md) for other usages of the Makefile. diff --git a/Makefile b/Makefile index 38d2474..13a527f 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,11 @@ UID := $(shell id -u) # Prevent suicide by excluding Makefile -ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' README.md INSTALL.md | grep -v Makefile | sort -u) +ENTANGLED := $(shell perl -ne 'print $$1,"\n" if /^```\{.*file=(.*)\}/' *.md | grep -v Makefile | sort -u) COMPILED := bin/newtonraphson.exe src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm -entangle: README.md INSTALL.md - docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs README.md INSTALL.md +entangle: *.md + docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md $(ENTANGLED): entangle diff --git a/README.md b/README.md index 43cbcda..6d0ed55 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Document describing a way that a researcher with a C++ algorithm can make it ava - as a Python application via pybind11, Flask and Celery - in the web browser using web assembly and JavaScript -We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide and method to convert the code snippets to files are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md) +We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide and method to convert the code snippets to files are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md). The [Newton-Raphson root finding algorithm](https://en.wikipedia.org/wiki/Newton%27s_method) will be the use case. The algorithm is explained in [this video series](https://www.youtube.com/watch?v=cOmAk82cr9M). diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..f72b947 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,55 @@ +# Tests + +To make sure [JavaScript and WebAssembly code snippets](README.md#JavaScript) and [Single page application](README.md#single-page-application) work we want have a tests for them. + +To test we will use the [cypress](https://www.cypress.io/) test framework. +Cypress can simulate what a user would do and expect in a web browser. + +We want to test if visiting the example web page renders the answer `-1.00`. + +First a test for the direct WebAssembly example. + +```{.js file=cypress/integration/example_spec.js} +// this JavaScript snippet is run by cypress and is stored as cypress/integration/example_spec.js +describe('src/js/example.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example.html'); + cy.get('#answer').contains('-1.00'); + }); +}); +``` + +Second a test for the WebAssembly called through a web worker. + +```{.js file=cypress/integration/example-web-worker_spec.js} +// this JavaScript snippet is run by cypress and is stored as cypress/integration/example-web-worker_spec.js +describe('src/js/example-web-worker.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-web-worker.html'); + cy.get('#answer').contains('-1.00'); + }); +}); +``` + +And lastly a test for the full React/form/Web worker/WebAssembly combination. + +```{.js file=cypress/integration/example-app_spec.js} +describe('src/js/example-app.html', () => { + it('should render -1.00', () => { + cy.visit('http://localhost:8000/src/js/example-app.html'); + cy.get('input[name=guess]').type('-30'); + cy.contains('Submit').click(); + cy.get('#answer').contains('-1.00'); + }); +}); +``` + +The test can be run with + +```{.awk #test-wasm} +npx cypress run --config-file false +``` + +The `npx` command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). + +The tests will also be run in the [GH Action continous integration build](.github/workflows/main.yml). From 5c2f7b57b89cb388a826ee9225748b96ee854b3d Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 16:20:48 +0200 Subject: [PATCH 12/32] Added .zenodo.json --- .zenodo.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .zenodo.json diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..89c5b5b --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,6 @@ +{ + "license": { + "id": "Apache-2.0" + }, + "title": "Guide to make C++ available as a web application" +} From 4376eb53adc2053e610fcb89f0d06e33fa8873df Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 22 May 2020 16:32:13 +0200 Subject: [PATCH 13/32] Add anchor comment to new code blocks --- .githooks/pre-commit | 1 + CONTRIBUTING.md | 1 + INSTALL.md | 1 + Makefile | 1 + 4 files changed, 4 insertions(+) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 70a1135..f7f27ea 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,4 +1,5 @@ #!/bin/sh +# this shell script is stored as .githooks/pre-commit UID=$(id -u) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35118eb..29cc302 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -140,6 +140,7 @@ The pre-commit hook script runs entangle using Docker and adds newly written fil ```{.awk file=.githooks/pre-commit} #!/bin/sh +# this shell script is stored as .githooks/pre-commit UID=$(id -u) diff --git a/INSTALL.md b/INSTALL.md index b8ab30d..8daf4a4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -16,6 +16,7 @@ To run the commands in the README.md the following items are required All the commands in the [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md) can be captured in a [Makefile](https://en.wikipedia.org/wiki/Makefile) like so: ```{.makefile file=Makefile} +# this Makefile snippet is stored as Makefile .PHONY: clean clean-compiled clean-entangled test all check entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) diff --git a/Makefile b/Makefile index 13a527f..ca6641e 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +# this Makefile snippet is stored as Makefile .PHONY: clean clean-compiled clean-entangled test all check entangle entangle-list py-deps start-redis stop-redis run-webservice run-celery-webapp run-webapp build-wasm host-files test-wasm UID := $(shell id -u) From 05880016e6b8c477cb7b8abb3b7766933d5a7bf7 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 25 May 2020 11:51:49 +0200 Subject: [PATCH 14/32] Ignore compiled files --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 5824f8b..98bd884 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ __pycache__/ cypress/plugins cypress/support cypress/videos + +# Ignore compiled files +bin/newtonraphson.exe +src/py/newtonraphsonpy.*.so +apache2/cgi-bin/newtonraphson +src/js/newtonraphsonwasm.js +src/js/newtonraphsonwasm.wasm From ac14ae781c1c5135c30b0933d378974f9131e654 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Tue, 26 May 2020 11:51:32 +0200 Subject: [PATCH 15/32] Entangling is in CONTRIBITING.md and files are in repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d0ed55..ab5b8c2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Document describing a way that a researcher with a C++ algorithm can make it ava - as a Python application via pybind11, Flask and Celery - in the web browser using web assembly and JavaScript -We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide and method to convert the code snippets to files are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md). +We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md). The [repo](https://github.com/NLESC-JCER/cpp2wasm) contains the files that can be made from the code snippets in this guide. The code snippets can be [entangled](https://entangled.github.io/) to files using any of [these](CONTRIBUTING.md#tips) methods. The [Newton-Raphson root finding algorithm](https://en.wikipedia.org/wiki/Newton%27s_method) will be the use case. The algorithm is explained in [this video series](https://www.youtube.com/watch?v=cOmAk82cr9M). From a09c05aeea5720292c8d581c337f3b0f3b3ebfff Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 15:51:55 +0200 Subject: [PATCH 16/32] Split & comment entangle check command --- .githooks/pre-commit | 5 ++++- CONTRIBUTING.md | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index f7f27ea..25d6e07 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -5,7 +5,10 @@ UID=$(id -u) echo 'Check entangled files are up to date' -FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +# Entangle Markdown to source code and store the output +LOG=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null) +# Parse filenames from output +FILES=$(echo $LOG | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') [ -z "$FILES" ] && exit 0 echo $FILES diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29cc302..ce92f70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,7 +146,10 @@ UID=$(id -u) echo 'Check entangled files are up to date' -FILES=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') +# Entangle Markdown to source code and store the output +LOG=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null) +# Parse which filenames have been written from output +FILES=$(echo $LOG | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') [ -z "$FILES" ] && exit 0 echo $FILES From f6bea28ad22b8decddcef8e044a631606ea218e8 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 15:54:54 +0200 Subject: [PATCH 17/32] In CI pin Python to 3.8 --- .githooks/pre-commit | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 25d6e07..00d6070 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,7 +7,7 @@ echo 'Check entangled files are up to date' # Entangle Markdown to source code and store the output LOG=$(docker run --rm --user ${UID} -v ${PWD}:/data nlesc/pandoc-tangle:0.5.0 --preserve-tabs *.md 2>&1 > /dev/null) -# Parse filenames from output +# Parse which filenames have been written from output FILES=$(echo $LOG | perl -ne 'print $1,"\n" if /^Writing \`(.*)\`./') [ -z "$FILES" ] && exit 0 echo $FILES diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86f1b7c..3564ba7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: - uses: actions/setup-python@v1 with: - python-version: "3.x" # Version range or exact version of a Python version to use, using SemVer's version range syntax + python-version: "3.8" # Version range or exact version of a Python version to use, using SemVer's version range syntax architecture: "x64" # optional x64 or x86. Defaults to x64 if not specified - name: Install Python dependencies From d6c760d8164fe12583f8df458aaf0717212c03d3 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 15:59:20 +0200 Subject: [PATCH 18/32] Capture service logs and upload as artifacts --- .github/workflows/main.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3564ba7..1c13062 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,13 +56,13 @@ jobs: run: http --ignore-stdin -f localhost:5001 epsilon=0.001 guess=-20 - name: Start web service in background - run: make run-webservice & + run: make run-webservice > ./run-webapp.log 2>&1 & - name: Test web service run: make test-webservice - name: Start Celery web app in background - run: make run-celery-webapp & + run: make run-celery-webapp > ./run-celery-webapp.log 2>&1 & - name: Start Celery worker in background run: | @@ -77,6 +77,12 @@ jobs: RESULT_URL=$(cat response.txt |grep Location |awk '{print $2}') sleep 2 http --ignore-stdin $RESULT_URL + + - name: Upload log of services + uses: actions/upload-artifact@v2 + with: + name: service-logs + path: ./run-*.log wasm: runs-on: ubuntu-latest steps: @@ -89,7 +95,13 @@ jobs: run: make build-wasm - name: Start web server for hosting files in background - run: make host-files & + run: make host-files > ./web-server.log & - name: Run tests run: make test-wasm + + - name: Upload log of web server + uses: actions/upload-artifact@v2 + with: + name: web-server-log + path: ./web-server.log \ No newline at end of file From 9fab09291c819a4813566336d32a0e82394a4a94 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 16:48:00 +0200 Subject: [PATCH 19/32] Fix log filenames + always upload logs --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c13062..d87291d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,13 +50,13 @@ jobs: run: make test-py - name: Start web application in background - run: make run-webapp & + run: make run-webapp ./run-webapp.log 2>&1 & - name: Test web application run: http --ignore-stdin -f localhost:5001 epsilon=0.001 guess=-20 - name: Start web service in background - run: make run-webservice > ./run-webapp.log 2>&1 & + run: make run-webservice > ./run-webservice.log 2>&1 & - name: Test web service run: make test-webservice @@ -79,6 +79,7 @@ jobs: http --ignore-stdin $RESULT_URL - name: Upload log of services + if: ${{ always() }} uses: actions/upload-artifact@v2 with: name: service-logs @@ -101,6 +102,7 @@ jobs: run: make test-wasm - name: Upload log of web server + if: ${{ always() }} uses: actions/upload-artifact@v2 with: name: web-server-log From 413dc21a2fc2486a9c9e642fb6998e480f453db5 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 17:06:52 +0200 Subject: [PATCH 20/32] Apply suggestions from code review Co-authored-by: Faruk D. --- README.md | 2 +- TESTING.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ab5b8c2..411fe90 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Document describing a way that a researcher with a C++ algorithm can make it ava - as a Python application via pybind11, Flask and Celery - in the web browser using web assembly and JavaScript -We assume the operating system is Linux (We used Linux while writing this guide). The required dependencies to run this guide are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md). The [repo](https://github.com/NLESC-JCER/cpp2wasm) contains the files that can be made from the code snippets in this guide. The code snippets can be [entangled](https://entangled.github.io/) to files using any of [these](CONTRIBUTING.md#tips) methods. +This guide was written and tested on Linux operating system. The required dependencies to run this guide are described in the [INSTALL.md](INSTALL.md) document. If you want to contribute to the guide see [CONTRIBUTING.md](CONTRIBUTING.md). The [repo](https://github.com/NLESC-JCER/cpp2wasm) contains the files that can be made from the code snippets in this guide. The code snippets can be [entangled](https://entangled.github.io/) to files using any of [these](CONTRIBUTING.md#tips) methods. The [Newton-Raphson root finding algorithm](https://en.wikipedia.org/wiki/Newton%27s_method) will be the use case. The algorithm is explained in [this video series](https://www.youtube.com/watch?v=cOmAk82cr9M). diff --git a/TESTING.md b/TESTING.md index f72b947..a8fae55 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2,12 +2,12 @@ To make sure [JavaScript and WebAssembly code snippets](README.md#JavaScript) and [Single page application](README.md#single-page-application) work we want have a tests for them. -To test we will use the [cypress](https://www.cypress.io/) test framework. -Cypress can simulate what a user would do and expect in a web browser. +To test, we will use the [cypress](https://www.cypress.io/) JavaScript end to end testing framework. +Cypress can simulate user behavior such as clicking buttons etc. and checks expected result in a web browser. -We want to test if visiting the example web page renders the answer `-1.00`. +In the following example, we test if the example web page renders the answer `-1.00` when it is visited. -First a test for the direct WebAssembly example. +Let's, first write a test for the direct WebAssembly example. ```{.js file=cypress/integration/example_spec.js} // this JavaScript snippet is run by cypress and is stored as cypress/integration/example_spec.js @@ -19,7 +19,7 @@ describe('src/js/example.html', () => { }); ``` -Second a test for the WebAssembly called through a web worker. +Second, a test for the WebAssembly called through a web worker. ```{.js file=cypress/integration/example-web-worker_spec.js} // this JavaScript snippet is run by cypress and is stored as cypress/integration/example-web-worker_spec.js @@ -31,7 +31,7 @@ describe('src/js/example-web-worker.html', () => { }); ``` -And lastly a test for the full React/form/Web worker/WebAssembly combination. +And lastly, a test for the React/form/Web worker/WebAssembly combination. ```{.js file=cypress/integration/example-app_spec.js} describe('src/js/example-app.html', () => { @@ -44,12 +44,12 @@ describe('src/js/example-app.html', () => { }); ``` -The test can be run with +The test can be run with the following command: ```{.awk #test-wasm} npx cypress run --config-file false ``` -The `npx` command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). +The [`npx`](https://www.npmjs.com/package/npx) command ships with NodeJS which is included in the Emscripten SDK and can be used to run commands available on [npm repository](https://npmjs.com/). The tests will also be run in the [GH Action continous integration build](.github/workflows/main.yml). From d65244ac921cd27a13ff6db16abba3b1e9fb6125 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 17:37:18 +0200 Subject: [PATCH 21/32] tee background jobs --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d87291d..8f3cad7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,24 +50,24 @@ jobs: run: make test-py - name: Start web application in background - run: make run-webapp ./run-webapp.log 2>&1 & + run: make run-webapp 2>&1 | tee ./run-webapp.log & - name: Test web application run: http --ignore-stdin -f localhost:5001 epsilon=0.001 guess=-20 - name: Start web service in background - run: make run-webservice > ./run-webservice.log 2>&1 & + run: make run-webservice 2>&1 | tee ./run-webservice.log & - name: Test web service run: make test-webservice - name: Start Celery web app in background - run: make run-celery-webapp > ./run-celery-webapp.log 2>&1 & + run: make run-celery-webapp 2>&1 | tee ./run-celery-webapp.log & - name: Start Celery worker in background run: | cd src/py - PYTHONPATH=$PWD/../.. celery -A tasks worker & + PYTHONPATH=$PWD/../.. celery -A tasks worker 2>&1 | tee ./run-celery-worker.log & cd ../.. - name: Test Celery web app @@ -96,7 +96,7 @@ jobs: run: make build-wasm - name: Start web server for hosting files in background - run: make host-files > ./web-server.log & + run: make host-files 2>&1 | tee ./web-server.log & - name: Run tests run: make test-wasm @@ -106,4 +106,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: web-server-log - path: ./web-server.log \ No newline at end of file + path: ./web-server.log From 5ba427089f964ad6b6297c5950fc2a08ea870583 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 17:52:40 +0200 Subject: [PATCH 22/32] Test against Python 3.6, 3.7 and 3.8 --- .github/workflows/main.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8f3cad7..370927a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,12 @@ jobs: run: make test-cli test-cgi python: # The type of runner that the job will run on + name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8] + fail-fast: true # Redis is needed for Celery services: redis: @@ -38,10 +43,14 @@ jobs: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 with: - python-version: "3.8" # Version range or exact version of a Python version to use, using SemVer's version range syntax - architecture: "x64" # optional x64 or x86. Defaults to x64 if not specified + python-version: ${{ matrix.python-version }} + architecture: "x64" + + - name: Which Python + run: which python - name: Install Python dependencies run: make py-deps && pip install httpie From 0453ecc5708dd62c1c24fa41f86501331854da00 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 18:20:04 +0200 Subject: [PATCH 23/32] Use f-string instead of jinja templates --- README.md | 107 +++++++---------------------------- src/py/templates/.gitkeep | 0 src/py/templates/form.html | 9 --- src/py/templates/hello.html | 8 --- src/py/templates/result.html | 3 - src/py/webapp-celery.py | 18 +++++- src/py/webapp.py | 13 ++++- 7 files changed, 48 insertions(+), 110 deletions(-) delete mode 100644 src/py/templates/.gitkeep delete mode 100644 src/py/templates/form.html delete mode 100644 src/py/templates/hello.html delete mode 100644 src/py/templates/result.html diff --git a/README.md b/README.md index 411fe90..116ccf2 100644 --- a/README.md +++ b/README.md @@ -375,93 +375,20 @@ The web application has 3 kinds of pages: Each page is available on a different url. In flask the way urls are mapped to Python function is done by adding a route decorator to the function for example: -```{.python file=src/py/hello.py} -# this Python snippet is stored as src/py/hello.py -from flask import Flask -app = Flask(__name__) - -@app.route("/") -def hello(): - return "Hello World!" - -app.run() -``` - -Run with - -```{.awk #py-hello} -python src/py/hello.py -``` - -The above route will just return the string "Hello World!" in the web browser when visiting [http://localhost:5000/](http://localhost:5000/). It is possible to return a html page as well, but to make it dynamic it soon becomes a mess of string concatenations. Template engines help to avoid the concatination mess. Flask is configured with the [Jinja2](https://jinja.palletsprojects.com/) template engine. A template for the above route could look like: - -```{.html file=src/py/templates/hello.html} -{# this Jinja2 template snippet is stored as src/py/templates/hello.html #} - -Hello from Flask -{% if name %} -

Hello {{ name }}!

-{% else %} -

Hello, World!

-{% endif %} -``` - -and to render the template the function would look like: - -```{.python file=src/py/hello-templated.py} -# this Python snippet is stored as src/py/hello-templated.py -from flask import Flask, render_template - -app = Flask(__name__) - -@app.route('/hello/') -def hello_name(name=None): - return render_template('hello.html', name=name) - -app.run() -``` - -Where `name` is a variable which gets combined with template to render into a html page. - -The web application can be started with - -```{.awk #py-hello-templated} -python src/py/hello-templated.py -``` - -In a web browser you can visit [http://localhost:5000/hello/yourname](http://localhost:5000/hello/yourname) to the web application. - -Let's make the web application for our Newton raphson algorithm. - -The first thing we want is the web page with the form, the template that renders the form looks like - -```{.html file=src/py/templates/form.html} -{# this Jinja2 template snippet is stored as src/py/templates/form.html #} - -
- - - - - -
-``` - -The home page will render the form like so +The starting page will render the form like so ```{.python #py-form} # this Python code snippet is later referred to as <> @app.route('/', methods=['GET']) def form(): - return render_template('form.html') -``` - -The result will be displayed on a html page with the following template - -```{.html file=src/py/templates/result.html} -{# this Jinja2 template snippet is stored as src/py/templates/result.html #} - -

With epsilon of {{ epsilon }} and a guess of {{ guess }} the found root is {{ root }}.

+ return ''' +
+ + + + + +
''' ``` The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result html page. @@ -477,7 +404,12 @@ def calculate(): finder = NewtonRaphson(epsilon) root = finder.find(guess) - return render_template('result.html', epsilon=epsilon, guess=guess, root=root) + return f''' +

With epsilon of {epsilon} and a guess of {guess} the found root is {root}.

''' +``` + +```{.python #py-calculate} + # this Python code snippet is appended to <> ``` Putting it all together in @@ -576,9 +508,14 @@ def result(jobid): job.maybe_throw() if job.successful(): result = job.get() - return render_template('result.html', epsilon=result['epsilon'], guess=result['guess'], root=result['root']) + epsilon = result['epsilon'] + guess = result['guess'] + root = result['root'] + return f''' +

With epsilon of {epsilon} and a guess of {guess} the found root is {root}.

''' else: - return job.status + return f''' +

{job.status}

''' ``` Putting it all together diff --git a/src/py/templates/.gitkeep b/src/py/templates/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/py/templates/form.html b/src/py/templates/form.html deleted file mode 100644 index 96059fd..0000000 --- a/src/py/templates/form.html +++ /dev/null @@ -1,9 +0,0 @@ -{# this Jinja2 template snippet is stored as src/py/templates/form.html #} - -

- - - - - -
\ No newline at end of file diff --git a/src/py/templates/hello.html b/src/py/templates/hello.html deleted file mode 100644 index 261046f..0000000 --- a/src/py/templates/hello.html +++ /dev/null @@ -1,8 +0,0 @@ -{# this Jinja2 template snippet is stored as src/py/templates/hello.html #} - -Hello from Flask -{% if name %} -

Hello {{ name }}!

-{% else %} -

Hello, World!

-{% endif %} \ No newline at end of file diff --git a/src/py/templates/result.html b/src/py/templates/result.html deleted file mode 100644 index edf3ebc..0000000 --- a/src/py/templates/result.html +++ /dev/null @@ -1,3 +0,0 @@ -{# this Jinja2 template snippet is stored as src/py/templates/result.html #} - -

With epsilon of {{ epsilon }} and a guess of {{ guess }} the found root is {{ root }}.

\ No newline at end of file diff --git a/src/py/webapp-celery.py b/src/py/webapp-celery.py index 922895a..874002d 100644 --- a/src/py/webapp-celery.py +++ b/src/py/webapp-celery.py @@ -6,7 +6,14 @@ # this Python code snippet is later referred to as <> @app.route('/', methods=['GET']) def form(): - return render_template('form.html') + return ''' +
+ + + + + +
''' # this Python code snippet is later referred to as <> @app.route('/', methods=['POST']) @@ -25,9 +32,14 @@ def result(jobid): job.maybe_throw() if job.successful(): result = job.get() - return render_template('result.html', epsilon=result['epsilon'], guess=result['guess'], root=result['root']) + epsilon = result['epsilon'] + guess = result['guess'] + root = result['root'] + return f''' +

With epsilon of {epsilon} and a guess of {guess} the found root is {root}.

''' else: - return job.status + return f''' +

{job.status}

''' if __name__ == '__main__': app.run(port=5000) \ No newline at end of file diff --git a/src/py/webapp.py b/src/py/webapp.py index f518077..5508b55 100644 --- a/src/py/webapp.py +++ b/src/py/webapp.py @@ -5,7 +5,14 @@ # this Python code snippet is later referred to as <> @app.route('/', methods=['GET']) def form(): - return render_template('form.html') + return ''' +

+ + + + + +
''' # this Python code snippet is later referred to as <> @app.route('/', methods=['POST']) @@ -17,6 +24,8 @@ def calculate(): finder = NewtonRaphson(epsilon) root = finder.find(guess) - return render_template('result.html', epsilon=epsilon, guess=guess, root=root) + return f''' +

With epsilon of {epsilon} and a guess of {guess} the found root is {root}.

''' + # this Python code snippet is appended to <> app.run(port=5001) \ No newline at end of file From b09b0b62a313583363d396ef184e0fb9c003ee63 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 18:32:55 +0200 Subject: [PATCH 24/32] Describe f-string --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 116ccf2..f309056 100644 --- a/README.md +++ b/README.md @@ -497,7 +497,7 @@ def submit(): return redirect(url_for('result', jobid=job.id)) ``` -The last method is to ask the Celery task queue what the status is of the job and return the result when it is succesful. +The last method is to ask the Celery task queue what the status is of the job and return the result when it is succesful. To construct the returned html we use [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) to replace the variable names with the variable values. ```{.python #py-result} # this Python code snippet is later referred to as <> From 05a7f703c1f539d71ab174e89b6a2c1a71ade658 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 3 Jun 2020 18:34:43 +0200 Subject: [PATCH 25/32] Pin Celery to v4.4.3 As latest v4.4.4 Celery is broken. --- Makefile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ca6641e..a69c1d5 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ pip-flask: pip install flask pip-celery: - pip install celery[redis] + pip install celery[redis]==4.4.3 pip-connexion: pip install connexion[swagger-ui] diff --git a/README.md b/README.md index f309056..ce06b21 100644 --- a/README.md +++ b/README.md @@ -448,7 +448,7 @@ docker run --rm -d -p 6379:6379 --name some-redis redis To use Celery we must install the redis flavored version with ```{.awk #pip-celery} -pip install celery[redis] +pip install celery[redis]==4.4.3 ``` Let's set up a method that can be submitted to the Celery task queue. From 4a2336b17e0c581f6ac8fb1e48c0d786dc2b299a Mon Sep 17 00:00:00 2001 From: Johannes Hidding Date: Tue, 2 Jun 2020 13:15:38 +0200 Subject: [PATCH 26/32] Add Entangled badge Up for discussion, but I thought this is a nice touch. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ce06b21..c916e76 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Visualization](#visualization) [![CI](https://github.com/NLESC-JCER/cpp2wasm/workflows/CI/badge.svg)](https://github.com/NLESC-JCER/cpp2wasm/actions?query=workflow%3ACI) +[![Entangled](https://img.shields.io/badge/entangled-Use%20the%20source!-%2300aeff)](https://entangled.github.io/) Document describing a way that a researcher with a C++ algorithm can make it available as a web application. We will host the C++ algorithm as an web application in several different ways: From 88e1c9fe5f0cee5eee0445fa40f92f6a2e0271a1 Mon Sep 17 00:00:00 2001 From: Johannes Hidding Date: Tue, 2 Jun 2020 11:37:40 +0200 Subject: [PATCH 27/32] Create entangled.dhall --- entangled.dhall | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 entangled.dhall diff --git a/entangled.dhall b/entangled.dhall new file mode 100644 index 0000000..464f0bb --- /dev/null +++ b/entangled.dhall @@ -0,0 +1,7 @@ +let entangled = https://raw.githubusercontent.com/entangled/entangled/v1.0.1/data/config-schema.dhall + sha256:9fd18824499379eee53b974ca7570b3bc064fda546348d9b31841afab3b053a7 + +in { entangled = entangled.Config :: { database = Some ".entangled/db.sqlite" + , watchList = ["README.md", "INSTALL.md"] : List Text + } + } From e9659f8b1105786f28698501f11c70d2fbc9a7f1 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Tue, 2 Jun 2020 13:29:25 +0200 Subject: [PATCH 28/32] Make entangled aware of all Markdown files --- entangled.dhall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entangled.dhall b/entangled.dhall index 464f0bb..ea5bcba 100644 --- a/entangled.dhall +++ b/entangled.dhall @@ -2,6 +2,6 @@ let entangled = https://raw.githubusercontent.com/entangled/entangled/v1.0.1/dat sha256:9fd18824499379eee53b974ca7570b3bc064fda546348d9b31841afab3b053a7 in { entangled = entangled.Config :: { database = Some ".entangled/db.sqlite" - , watchList = ["README.md", "INSTALL.md"] : List Text + , watchList = ["README.md", "INSTALL.md", "CONTRIBUTING.md", "TESTING.md"] : List Text } } From 0033d878172abcecf7a1115578eaea2044a5bdfa Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Tue, 2 Jun 2020 13:29:05 +0200 Subject: [PATCH 29/32] Use entangled v1 subcommand to run daemon --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce92f70..4aa11fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,7 +123,7 @@ Each time a source code file is changed the code blocks in the Markdown files wi 2. Run entangled daemon with ```shell -entangled *.md +entangled daemon ``` ### Generate code from Markdown on commit From 8aab1bf195e609b0121147f5f04b42b7beec85e6 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 4 Jun 2020 07:58:15 +0200 Subject: [PATCH 30/32] Ignore entangled db --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 98bd884..304aa76 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ src/py/newtonraphsonpy.*.so apache2/cgi-bin/newtonraphson src/js/newtonraphsonwasm.js src/js/newtonraphsonwasm.wasm + +# Ignore entangled db +.entangled/db.sqlite From b29546b43779f0ebd8a13eb0b38d98496c9e559b Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 4 Jun 2020 08:07:10 +0200 Subject: [PATCH 31/32] Describe Flask request --- README.md | 11 +++++------ src/py/webapp.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c916e76..9175dc2 100644 --- a/README.md +++ b/README.md @@ -374,9 +374,9 @@ The web application has 3 kinds of pages: 2. a page to show the progress of the calculation 3. and a page which shows the result of the calculation. Each calculation will have it's own submit and result page. -Each page is available on a different url. In flask the way urls are mapped to Python function is done by adding a route decorator to the function for example: +Each page is available on a different url. In flask the way urls are mapped to Python function is done by adding a route decorator (`@app.route`) to the function. -The starting page will render the form like so +The first page with the form and submit button is defined as a function returning a HTML form. ```{.python #py-form} # this Python code snippet is later referred to as <> @@ -392,7 +392,7 @@ def form(): ''' ``` -The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result html page. +The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result html page. To get the submitted values we use the [Flask global `request` object](https://flask.palletsprojects.com/en/1.1.x/api/#flask.request). To construct the returned html we use [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) to replace the variable names with the variable values. ```{.python #py-calculate} # this Python code snippet is later referred to as <> @@ -417,7 +417,7 @@ Putting it all together in ```{.python file=src/py/webapp.py} # this Python snippet is stored as src/py/webapp.py -from flask import Flask, render_template, request +from flask import Flask, request app = Flask(__name__) <> @@ -498,7 +498,7 @@ def submit(): return redirect(url_for('result', jobid=job.id)) ``` -The last method is to ask the Celery task queue what the status is of the job and return the result when it is succesful. To construct the returned html we use [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) to replace the variable names with the variable values. +The last method is to ask the Celery task queue what the status is of the job and return the result when it is succesful. ```{.python #py-result} # this Python code snippet is later referred to as <> @@ -890,7 +890,6 @@ To make writing a SPA easier, a number of frameworks have been developed. The mo They have their strengths and weaknesses which are summarized in the [here](https://en.wikipedia.org/wiki/Comparison_of_JavaScript_frameworks#Features). - For Newton-Raphson web application I picked React as it is light and functional, because I like the small API footprint and the functional programming paradigm. The C++ algorithm is compiled into a wasm file using bindings. When a calculation form is submitted in the React application a web worker loads the wasm file, starts the calculation, renders the result. With this architecture the application only needs cheap static file hosting to host the html, js and wasm files. **The calculation will be done in the web browser on the end users machine instead of a server**. diff --git a/src/py/webapp.py b/src/py/webapp.py index 5508b55..a6eff3b 100644 --- a/src/py/webapp.py +++ b/src/py/webapp.py @@ -1,5 +1,5 @@ # this Python snippet is stored as src/py/webapp.py -from flask import Flask, render_template, request +from flask import Flask, request app = Flask(__name__) # this Python code snippet is later referred to as <> From 0fcbfedc74f0fe5af7e2224d563db0e54e9f48c3 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 4 Jun 2020 08:46:19 +0200 Subject: [PATCH 32/32] Update Flask links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9175dc2..c271398 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ The web application has 3 kinds of pages: 2. a page to show the progress of the calculation 3. and a page which shows the result of the calculation. Each calculation will have it's own submit and result page. -Each page is available on a different url. In flask the way urls are mapped to Python function is done by adding a route decorator (`@app.route`) to the function. +Each page is available on a different url. In Flask the way urls are mapped to Python function is done by adding a [route decorator](https://flask.palletsprojects.com/en/1.1.x/quickstart/#routing) (`@app.route`) to the function. The first page with the form and submit button is defined as a function returning a HTML form. @@ -392,7 +392,7 @@ def form(): ''' ``` -The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result html page. To get the submitted values we use the [Flask global `request` object](https://flask.palletsprojects.com/en/1.1.x/api/#flask.request). To construct the returned html we use [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) to replace the variable names with the variable values. +The form will be submitted to the '/' path with the POST method. In the handler of this route we want to perform the calculation and return the result html page. To get the submitted values we use the Flask global [request](https://flask.palletsprojects.com/en/1.1.x/quickstart/#accessing-request-data) object. To construct the returned html we use [f-strings](https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals) to replace the variable names with the variable values. ```{.python #py-calculate} # this Python code snippet is later referred to as <>