From 0cd9711b1ac7cbbbb2145e89e19886e85ea77c85 Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Fri, 8 Sep 2023 12:22:22 -0700 Subject: [PATCH 01/11] Add new executions and run endpoints with echo res --- lab/routes/execapi.js | 46 +++++++++++++++++++++++++++++++++++++++++++ machine/machine.js | 6 +++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 47a4a9955..6fc2e5ab8 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -11,7 +11,53 @@ const { uploadExecFiles } = require('../execapiutils'); +// The only responsibility this function will have is to find an available machine +// pass the req object and return the res object. +// All other processing needs to be handled by the machine. +// Especially the code execution folder creation and deletions. +// The previous endpoint worked when run locally and the docker volumes were mounted +// from local directorie, but it doesn't work when run as independent docker containers. router.post('/executions', async (req, res, next) => { + // find an available machine and send the request to the /code/run endpoint on the machine + let machines; + try { + machines = await Machine.find({}, { address: 1 }); + if (machines.length == 0) { + return res.status(400).json({ message: 'No machines available' }); + } + // call the machine api + let result = await fetch(machines[0].address + '/code/run', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(req.body) + }); + result = await result.json(); + + res.send(result); + } + catch (err) { + console.error(err); + return res.status(500).json({ message: err.message }); + } +}); + +/** + * Route for submitting a new code execution. + * + * @param {Object} req - HTTP request object containing request data. + * @property {string} req.body.src_code - The source code to be executed. + * @property {string} [req.body.dataset_file_id] - The dataset file ID (optional). + * @property {string} [req.body.dataset_id] - The dataset ID (optional). + * The dataset_file_id OR the dataset_id are used to load the dataset into a + * Pandas DataFrame as variable 'df' + * @property {string} [req.body.experiment_id] - The experiment ID (optional). + * The experiment_id is used to load the experiment model as variable 'model' + * @param {Object} res - HTTP response object for sending the response back to the client. + * @param {Function} next - Function to proceed to the next middleware step. + */ +router.post('/executions_old', async (req, res, next) => { if (req.body.src_code == null) { return res.status(400).json({ message: 'No src_code provided' }); } diff --git a/machine/machine.js b/machine/machine.js index abaf5f57d..e944c813d 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -512,7 +512,11 @@ app.post("/code/run/install", jsonParser, (req, res) => { }); // run code execution -app.post("/code/run", jsonParser, (req, res) => { +app.post('/code/run', jsonParser, (req, res) => { + res.send({ 'received': req.body }) +}); + +app.post("/code/run_old", jsonParser, (req, res) => { let args = [ "machine/pyutils/run_code.py", "--code", From 133734c227bbbee7fc8052b72ce6d37ab0d0c9ef Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Fri, 8 Sep 2023 13:02:06 -0700 Subject: [PATCH 02/11] Add src_code, dataset_id, dataset_file_id params --- lab/routes/execapi.js | 35 +++++++++++++++++++++++++++++------ machine/machine.js | 4 +++- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 6fc2e5ab8..f32e87491 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -11,13 +11,36 @@ const { uploadExecFiles } = require('../execapiutils'); -// The only responsibility this function will have is to find an available machine -// pass the req object and return the res object. -// All other processing needs to be handled by the machine. -// Especially the code execution folder creation and deletions. -// The previous endpoint worked when run locally and the docker volumes were mounted -// from local directorie, but it doesn't work when run as independent docker containers. +/** + * The only responsibility this function will have is to: + * 1. Check required and optional parameters + * 2. Find an available machine + * 3. Call the /code/run endpoint with the req.body + * 4. Return the res object from /code/run as JSON + * All other processing needs to be handled by the machine. + * Especially the code execution folder creation and deletions. + * The previous endpoint worked when run locally and the docker volumes were + * mounted from local directories, but it doesn't work when run as independent + * docker containers. + */ router.post('/executions', async (req, res, next) => { + + // src_code is required + if (req.body.src_code == null) { + return res.status(400).json({ message: 'No src_code provided' }); + } + + // dataset_file_id is optional, if a dataset_id is passed instead, then + // retrieve the dataset_file_id from the DB + if (req.body.dataset_file_id == null) { + if (req.body.dataset_id != null) { + let dataset = await getDatasetById(req.body.dataset_id) + if (dataset != null) { + req.body.dataset_file_id = dataset.files[0]._id; + } + } + } + // find an available machine and send the request to the /code/run endpoint on the machine let machines; try { diff --git a/machine/machine.js b/machine/machine.js index e944c813d..0fa2b87ae 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -513,7 +513,9 @@ app.post("/code/run/install", jsonParser, (req, res) => { // run code execution app.post('/code/run', jsonParser, (req, res) => { - res.send({ 'received': req.body }) + // Note, the body params passed need to be updated in this new function. + // dataset_file_id instead of _data_file_id (i.e. drop the first _) + res.send({ 'echo': req.body }) }); app.post("/code/run_old", jsonParser, (req, res) => { From 8fcd3cefcbb0c05c7b411db086e5b70130de41db Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Fri, 8 Sep 2023 15:44:24 -0700 Subject: [PATCH 03/11] Add the execution model to the machine folder --- machine/machine.js | 3 +++ machine/models/execution.js | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 machine/models/execution.js diff --git a/machine/machine.js b/machine/machine.js index 0fa2b87ae..88df6a61c 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -50,6 +50,7 @@ var chokidar = require("chokidar"); var rimraf = require("rimraf"); var WebSocketServer = require("ws").Server; const machine_utils = require("./machine_utils.js"); +const Execution = require('./models/execution') /* App instantiation */ var app = express(); @@ -519,6 +520,8 @@ app.post('/code/run', jsonParser, (req, res) => { }); app.post("/code/run_old", jsonParser, (req, res) => { + + let args = [ "machine/pyutils/run_code.py", "--code", diff --git a/machine/models/execution.js b/machine/models/execution.js new file mode 100644 index 000000000..46783217a --- /dev/null +++ b/machine/models/execution.js @@ -0,0 +1,40 @@ +const mongoose = require('mongoose'); + +const executionSchema = new mongoose.Schema({ + src_code: { + type: String, + required: true + }, + _experiment_id: { + type: mongoose.Schema.Types.ObjectId, + }, + _dataset_id: { + type: mongoose.Schema.Types.ObjectId, + }, + _dataset_file_id: { + type: mongoose.Schema.Types.ObjectId, + }, + status: { + type: String, + }, + result: { + type: String, + }, + files: [{ + _id: { + type: mongoose.Schema.Types.ObjectId, + }, + filename: { + type: String, + }, + mimetype: { + type: String, + }, + timestamp: { + type: Date, + default: Date.now + }, + }] +}); + +module.exports = mongoose.model('Execution', executionSchema); From 57d662fe2894c3a760df4ba5ad08469c172de379 Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Mon, 11 Sep 2023 13:08:29 -0700 Subject: [PATCH 04/11] Add executions to /executions endpoing in lab --- lab/routes/execapi.js | 38 ++++++++++++++++++++++++++-- machine/machine.js | 49 +++++++++++++++++++++++++++++++------ machine/models/execution.js | 40 ------------------------------ 3 files changed, 77 insertions(+), 50 deletions(-) delete mode 100644 machine/models/execution.js diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index f32e87491..8ef9ad909 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -41,6 +41,29 @@ router.post('/executions', async (req, res, next) => { } } + // create a new execution + let execution = new Execution({ + src_code: req.body.src_code, + status: 'submitted', + result: null, + files: [] + }); + + if (req.body.dataset_file_id != null) { + execution._dataset_file_id = req.body.dataset_file_id; + } + + if (req.body.experiment_id != null) { + execution._experiment_id = req.body.experiment_id; + } + + try { + const newExecution = await execution.save(); + execution._id = newExecution._id; + } catch (err) { + return res.status(500).json({ message: err.message }); + } + // find an available machine and send the request to the /code/run endpoint on the machine let machines; try { @@ -54,11 +77,22 @@ router.post('/executions', async (req, res, next) => { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(req.body) + // body: JSON.stringify(req.body) + body: JSON.stringify(execution) }); result = await result.json(); - res.send(result); + // test echo first + execution = result + // update the execution status + // execution.status = result.exec_results.status; + // execution.result = result.exec_results.result; + // execution.files = result.exec_results.files; + + // test echo first + // const updatedExecution = await execution.save(); + + res.send(execution); } catch (err) { console.error(err); diff --git a/machine/machine.js b/machine/machine.js index 88df6a61c..c98f0e767 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -50,7 +50,6 @@ var chokidar = require("chokidar"); var rimraf = require("rimraf"); var WebSocketServer = require("ws").Server; const machine_utils = require("./machine_utils.js"); -const Execution = require('./models/execution') /* App instantiation */ var app = express(); @@ -218,6 +217,47 @@ app.get("/projects", (req, res) => { res.send(projects); }); +app.post('/code/run', jsonParser, (req, res) => { + // Note, the body params passed need to be updated in this new function. + // dataset_file_id instead of _data_file_id (i.e. drop the first _) + + // These parameter will be included in req.body: + // src_code - required + // dataset_file_id - optional + // _id - execution_id + // experiment_id - optional + + res.send({ 'echo': req.body }); + console.log('Executing code...'); + + // Create a new Execution document for mongodb + // This will return an execution id + // Can this still be done in lab? + + // Create the temp path to store the code execuction generated files + // This uses CODE_RUN_PATH + + // Set up the args for the run_code.py (same as run_old) + + // Spawn the process (same as run_old) + + // Capture stdout (same as run_old) + + // Capture stderr (same as run_old) + + // Close close (same as run_old) + + // Upload files from tmp folder + // re-use sendResults ? + // Add a files array to the response with the file info + // like the one returned by uploadExecFiles. + + // delete tmp folder + + // Send result back + +}); + // Starts experiment app.post("/projects/:id", jsonParser, (req, res) => { var experimentId = req.body._id; @@ -513,15 +553,8 @@ app.post("/code/run/install", jsonParser, (req, res) => { }); // run code execution -app.post('/code/run', jsonParser, (req, res) => { - // Note, the body params passed need to be updated in this new function. - // dataset_file_id instead of _data_file_id (i.e. drop the first _) - res.send({ 'echo': req.body }) -}); - app.post("/code/run_old", jsonParser, (req, res) => { - let args = [ "machine/pyutils/run_code.py", "--code", diff --git a/machine/models/execution.js b/machine/models/execution.js deleted file mode 100644 index 46783217a..000000000 --- a/machine/models/execution.js +++ /dev/null @@ -1,40 +0,0 @@ -const mongoose = require('mongoose'); - -const executionSchema = new mongoose.Schema({ - src_code: { - type: String, - required: true - }, - _experiment_id: { - type: mongoose.Schema.Types.ObjectId, - }, - _dataset_id: { - type: mongoose.Schema.Types.ObjectId, - }, - _dataset_file_id: { - type: mongoose.Schema.Types.ObjectId, - }, - status: { - type: String, - }, - result: { - type: String, - }, - files: [{ - _id: { - type: mongoose.Schema.Types.ObjectId, - }, - filename: { - type: String, - }, - mimetype: { - type: String, - }, - timestamp: { - type: Date, - default: Date.now - }, - }] -}); - -module.exports = mongoose.model('Execution', executionSchema); From 6cadfb8fa8af48eed8d6c4869de0fd14978b6f7f Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Mon, 11 Sep 2023 14:36:50 -0700 Subject: [PATCH 05/11] Make code exec result the same. Needs files upload --- lab/routes/execapi.js | 48 +++++++++++++++++-------------- machine/machine.js | 62 ++++++++++++++++++++++++++++++++++++++-- machine/machine_utils.js | 45 +++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 24 deletions(-) diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 8ef9ad909..09cdd55e6 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -14,9 +14,10 @@ const { /** * The only responsibility this function will have is to: * 1. Check required and optional parameters - * 2. Find an available machine - * 3. Call the /code/run endpoint with the req.body - * 4. Return the res object from /code/run as JSON + * 2. Create and save Executions + * 3. Find an available machine + * 4. Call the /code/run endpoint with the req.body + * 5. Return the res object from /code/run as JSON * All other processing needs to be handled by the machine. * Especially the code execution folder creation and deletions. * The previous endpoint worked when run locally and the docker volumes were @@ -30,17 +31,6 @@ router.post('/executions', async (req, res, next) => { return res.status(400).json({ message: 'No src_code provided' }); } - // dataset_file_id is optional, if a dataset_id is passed instead, then - // retrieve the dataset_file_id from the DB - if (req.body.dataset_file_id == null) { - if (req.body.dataset_id != null) { - let dataset = await getDatasetById(req.body.dataset_id) - if (dataset != null) { - req.body.dataset_file_id = dataset.files[0]._id; - } - } - } - // create a new execution let execution = new Execution({ src_code: req.body.src_code, @@ -49,6 +39,10 @@ router.post('/executions', async (req, res, next) => { files: [] }); + if (req.body.dataset_id != null) { + execution._dataset_id = req.body.dataset_id; + } + if (req.body.dataset_file_id != null) { execution._dataset_file_id = req.body.dataset_file_id; } @@ -57,6 +51,19 @@ router.post('/executions', async (req, res, next) => { execution._experiment_id = req.body.experiment_id; } + // dataset_file_id is optional, if a dataset_id is passed instead, then + // retrieve the dataset_file_id from the DB + if (req.body.dataset_file_id == null) { + if (req.body.dataset_id != null) { + let dataset = await getDatasetById(req.body.dataset_id) + if (dataset != null) { + req.body.dataset_file_id = dataset.files[0]._id; + // update the execution _dataset_file_id + execution._dataset_file_id = dataset.files[0]._id; + } + } + } + try { const newExecution = await execution.save(); execution._id = newExecution._id; @@ -82,15 +89,14 @@ router.post('/executions', async (req, res, next) => { }); result = await result.json(); - // test echo first - execution = result + console.log("*****result:" + result); + // update the execution status - // execution.status = result.exec_results.status; - // execution.result = result.exec_results.result; - // execution.files = result.exec_results.files; + execution.status = result.exec_results.status; + execution.result = result.exec_results.result; + execution.files = result.exec_results.files; - // test echo first - // const updatedExecution = await execution.save(); + const updatedExecution = await execution.save(); res.send(execution); } diff --git a/machine/machine.js b/machine/machine.js index c98f0e767..071350d64 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -217,7 +217,7 @@ app.get("/projects", (req, res) => { res.send(projects); }); -app.post('/code/run', jsonParser, (req, res) => { +app.post('/code/run', jsonParser, async (req, res) => { // Note, the body params passed need to be updated in this new function. // dataset_file_id instead of _data_file_id (i.e. drop the first _) @@ -227,7 +227,6 @@ app.post('/code/run', jsonParser, (req, res) => { // _id - execution_id // experiment_id - optional - res.send({ 'echo': req.body }); console.log('Executing code...'); // Create a new Execution document for mongodb @@ -236,25 +235,82 @@ app.post('/code/run', jsonParser, (req, res) => { // Create the temp path to store the code execuction generated files // This uses CODE_RUN_PATH + let tmppath = path.join(process.env.CODE_RUN_PATH, req.body._id.toString()); + if (!fs.existsSync(tmppath)) fs.mkdirSync(tmppath, { recursive: true }); // Set up the args for the run_code.py (same as run_old) + let executionId = req.body._id; + let args = [ + "machine/pyutils/run_code.py", + "--code", + req.body.src_code, + "--execution_id", + executionId, + ]; + + if (req.body._dataset_file_id !== undefined) { + args.push("--dataset_file_id"); + args.push(req.body._dataset_file_id); + } + + if (req.body._experiment_id !== undefined) { + args.push("--experiment_id"); + args.push(req.body._experiment_id); + } // Spawn the process (same as run_old) + let pyProc = spawn("python", args, { cwd: process.env.PROJECT_ROOT }); + + let result = req.body; + result.exec_results = {}; // Capture stdout (same as run_old) + pyProc.stdout.on("data", (data) => { + result.exec_results = JSON.parse(data.toString()); + }); // Capture stderr (same as run_old) + pyProc.stderr.on("data", (data) => { + // console.log(`stderr: ${data}`); + try { + result.exec_results = JSON.parse(data.toString()); + } catch (e) { + console.log(e); + } + }); // Close close (same as run_old) + pyProc.on("close", (code) => { + // result.code = code; + console.log(`child process exited with code ${code}`); + result.exec_results.files = []; + res.send(result); + }); // Upload files from tmp folder // re-use sendResults ? // Add a files array to the response with the file info // like the one returned by uploadExecFiles. - + // Lab should talk to Mongo. Not the Machine. + // Looks like I'll have to use sendResults! + // Suggestion: do what sendResults does inside machine_utils.uploadExecFiles + // but, have it return the files array! + // const files = await machine_utils.uploadExecFiles(executionId, tmppath); + // result.exec_results.files = files; + // result.exec_results.files = []; + + // Don't delete tmp folder until uploadExecFiles is working correctly. // delete tmp folder + // fs.rmdir(tmppath, { recursive: true }, (err) => { + // if (err) { + // console.error(err); + // } else { + // console.log(tmppath + ' folder deleted'); + // } + // }); // Send result back + // res.send(result); }); diff --git a/machine/machine_utils.js b/machine/machine_utils.js index aca8aebb8..77272916a 100644 --- a/machine/machine_utils.js +++ b/machine/machine_utils.js @@ -28,6 +28,9 @@ along with this program. If not, see . (Autogenerated header, do not modify) */ +const path = require("path"); +const fs = require('fs'); + //Generate a list of projects based on machine_config.json var getProjects = function(algorithms) { var project_list = []; @@ -81,9 +84,51 @@ var checkCapacity = function(projId, maxCapacity, projects) { }; +var uploadExecFiles = async function(executionId, filepath) { +// async function uploadExecFiles(executionId, filepath) { + const files = []; + + const gfs = new GridFSBucket(db.db, { + bucketName: 'fs' + }); + + const filenames = await fs.promises.readdir(filepath); + + for (const file of filenames) { + const filename = path.join(filepath, file); + const stats = await fs.promises.stat(filename); + if (!stats.isDirectory()) { + const fileId = new mongoose.Types.ObjectId(); + const writeStream = gfs.openUploadStreamWithId(fileId, + file, + { + metadata: { + execution_id: executionId, + contentType: mime.lookup(file) + }, + contentType: 'binary/octet-stream' + } + ); + const readStream = fs.createReadStream(filename); + readStream.pipe(writeStream); + + await new Promise((resolve, reject) => { + writeStream.on('error', reject); + writeStream.on('finish', () => { + console.log('file uploaded to GridFS:' + filename); + files.push({ _id: fileId, filename: file, mimetype: mime.lookup(file) }); + resolve(); + }); + }); + } + } + + return files; +} exports.getProjects = getProjects; exports.getCapacity = getCapacity; exports.checkCapacity = checkCapacity; +exports.uploadExecFiles = uploadExecFiles; From 2302db3e14f7d871af535db2bce4d2a7e4db493b Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Tue, 12 Sep 2023 15:55:35 -0700 Subject: [PATCH 06/11] Add /executions/:id/files endpoint to upload files --- lab/execapiutils.js | 54 +++++++++++++++---------------- lab/routes/execapi.js | 20 ++++++++++++ machine/machine.js | 9 ++++-- machine/machine_utils.js | 68 ++++++++++++++++++++-------------------- 4 files changed, 87 insertions(+), 64 deletions(-) diff --git a/lab/execapiutils.js b/lab/execapiutils.js index b3c68cb6b..96364dfdd 100644 --- a/lab/execapiutils.js +++ b/lab/execapiutils.js @@ -6,6 +6,7 @@ const db = require('./dbgoose'); const path = require("path"); const fs = require('fs'); const { default: mongoose } = require('mongoose'); +const Stream = require('stream'); // const { isConstructorDeclaration } = require('typescript'); const mime = require('mime-types'); @@ -49,47 +50,46 @@ async function getExecutionById(req, res, next) { next(); } -async function uploadExecFiles(executionId, filepath) { +async function uploadExecFiles(executionId, req) { const files = []; const gfs = new GridFSBucket(db.db, { bucketName: 'fs' }); - const filenames = await fs.promises.readdir(filepath); + const uploadedFiles = req.files; - for (const file of filenames) { - const filename = path.join(filepath, file); - const stats = await fs.promises.stat(filename); - if (!stats.isDirectory()) { - const fileId = new mongoose.Types.ObjectId(); - const writeStream = gfs.openUploadStreamWithId(fileId, - file, - { - metadata: { - execution_id: executionId, - contentType: mime.lookup(file) - }, - contentType: 'binary/octet-stream' - } - ); - const readStream = fs.createReadStream(filename); - readStream.pipe(writeStream); + for (const uploadedFile of uploadedFiles) { + const fileId = new mongoose.Types.ObjectId(); + const writeStream = gfs.openUploadStreamWithId(fileId, + uploadedFile.originalname, + { + metadata: { + execution_id: executionId, + contentType: uploadedFile.mimetype + }, + contentType: 'binary/octet-stream' + } + ); + const bufferStream = new Stream.PassThrough(); + bufferStream.end(uploadedFile.buffer); - await new Promise((resolve, reject) => { - writeStream.on('error', reject); - writeStream.on('finish', () => { - console.log('file uploaded to GridFS:' + filename); - files.push({ _id: fileId, filename: file, mimetype: mime.lookup(file) }); - resolve(); - }); + bufferStream.pipe(writeStream); + + await new Promise((resolve, reject) => { + writeStream.on('error', reject); + writeStream.on('finish', () => { + console.log('file uploaded to GridFS:' + uploadedFile.originalname); + files.push({ _id: fileId, filename: uploadedFile.originalname, mimetype: uploadedFile.mimetype }); + resolve(); }); - } + }); } return files; } + module.exports = { getDatasetById, getExecutionById, diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 09cdd55e6..8c06682d3 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -5,6 +5,7 @@ const fs = require('fs'); const Machine = require('../models/machine'); const Execution = require('../models/execution'); const fetch = require('isomorphic-fetch'); +const upload = require('multer')(); const { getDatasetById, getExecutionById, @@ -273,4 +274,23 @@ router.patch('/executions/:id', getExecutionById, async (req, res, next) => { } }); +router.put('/executions/:id/files', upload.array('files'), getExecutionById, async (req, res, next) => { + try { + console.log('req.files:', req.files); + const executionId = req.params.id; + const files = await uploadExecFiles(executionId, req); + res.execution.files = files; + const updatedExecution = await res.execution.save(); + res.send({ + message: "Files uploaded", + files: files, + updatedExecution: updatedExecution + }); + + } catch (err) { + console.error(err); + next(err); + } +}); + module.exports = router; diff --git a/machine/machine.js b/machine/machine.js index 071350d64..b1c1095f2 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -217,7 +217,7 @@ app.get("/projects", (req, res) => { res.send(projects); }); -app.post('/code/run', jsonParser, async (req, res) => { +app.post('/code/run', jsonParser, (req, res) => { // Note, the body params passed need to be updated in this new function. // dataset_file_id instead of _data_file_id (i.e. drop the first _) @@ -280,10 +280,13 @@ app.post('/code/run', jsonParser, async (req, res) => { }); // Close close (same as run_old) - pyProc.on("close", (code) => { + pyProc.on("close", async (code) => { // result.code = code; console.log(`child process exited with code ${code}`); - result.exec_results.files = []; + // TODO: this part is still untested, need to work on this next. + const files = await machine_utils.sendExecFiles(executionId, tmppath); + result.exec_results.files = files; + res.send(result); }); diff --git a/machine/machine_utils.js b/machine/machine_utils.js index 77272916a..78e115157 100644 --- a/machine/machine_utils.js +++ b/machine/machine_utils.js @@ -31,6 +31,8 @@ along with this program. If not, see . const path = require("path"); const fs = require('fs'); +// const { isConstructorDeclaration } = require('typescript'); +const mime = require('mime-types'); //Generate a list of projects based on machine_config.json var getProjects = function(algorithms) { var project_list = []; @@ -84,46 +86,44 @@ var checkCapacity = function(projId, maxCapacity, projects) { }; -var uploadExecFiles = async function(executionId, filepath) { -// async function uploadExecFiles(executionId, filepath) { - const files = []; +async function sendExecFiles(executionId, filepath) { + try { + const uri = `http://${process.env.LAB_HOST}:${process.env.LAB_PORT}/execapi/v1/executions/${executionId}/files`; - const gfs = new GridFSBucket(db.db, { - bucketName: 'fs' - }); + const files = []; - const filenames = await fs.promises.readdir(filepath); + const filenames = await fs.promises.readdir(filepath); - for (const file of filenames) { - const filename = path.join(filepath, file); - const stats = await fs.promises.stat(filename); - if (!stats.isDirectory()) { - const fileId = new mongoose.Types.ObjectId(); - const writeStream = gfs.openUploadStreamWithId(fileId, - file, - { - metadata: { - execution_id: executionId, - contentType: mime.lookup(file) + for (const file of filenames) { + const filename = path.join(filepath, file); + const stats = await fs.promises.stat(filename); + if (!stats.isDirectory()) { + const fileContent = await fs.readFile(filename); + + const response = await fetch(uri, { + method: 'POST', + body: fileContent, + headers: { + 'Content-Type': 'application/octet-stream', }, - contentType: 'binary/octet-stream' - } - ); - const readStream = fs.createReadStream(filename); - readStream.pipe(writeStream); - - await new Promise((resolve, reject) => { - writeStream.on('error', reject); - writeStream.on('finish', () => { - console.log('file uploaded to GridFS:' + filename); - files.push({ _id: fileId, filename: file, mimetype: mime.lookup(file) }); - resolve(); }); - }); + + if (!response.ok) { + throw new Error(`Error sending file ${file}: ${response.status} ${response.statusText}`); + } + + const responseBody = await response.text(); + console.log(responseBody); + + files.push({ filename, success: true }); + } } - } - return files; + return files; + } catch (error) { + console.error(error); + return []; + } } @@ -131,4 +131,4 @@ var uploadExecFiles = async function(executionId, filepath) { exports.getProjects = getProjects; exports.getCapacity = getCapacity; exports.checkCapacity = checkCapacity; -exports.uploadExecFiles = uploadExecFiles; +exports.sendExecFiles = sendExecFiles; From bdd400bd92a8a21e48b9f710318e428d8df5b673 Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Wed, 13 Sep 2023 16:07:10 -0700 Subject: [PATCH 07/11] Finish files upload endpoint, clean up code. --- .env | 2 +- lab/routes/execapi.js | 14 ++++------ machine/machine.js | 60 ++++++++-------------------------------- machine/machine_utils.js | 49 ++++++++++++++++---------------- machine/package.json | 1 + 5 files changed, 43 insertions(+), 83 deletions(-) diff --git a/.env b/.env index e423a93fb..a7cb8b073 100644 --- a/.env +++ b/.env @@ -7,5 +7,5 @@ # Leave this set to 0 on the GitHub repo so the unit and # integration tests do not need to have wheels (until we # find a convenient way to use wheels on GitHub) -TAG=1.1.a0 +TAG=0.20.a0 USE_WHEELS=1 diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 8c06682d3..4dab82919 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -90,8 +90,6 @@ router.post('/executions', async (req, res, next) => { }); result = await result.json(); - console.log("*****result:" + result); - // update the execution status execution.status = result.exec_results.status; execution.result = result.exec_results.result; @@ -99,7 +97,8 @@ router.post('/executions', async (req, res, next) => { const updatedExecution = await execution.save(); - res.send(execution); + // res.send(execution); + res.send(updatedExecution); } catch (err) { console.error(err); @@ -274,17 +273,14 @@ router.patch('/executions/:id', getExecutionById, async (req, res, next) => { } }); -router.put('/executions/:id/files', upload.array('files'), getExecutionById, async (req, res, next) => { +// This internal API endpoint will be called by the machine during a code execution. +router.put('/executions/:id/files', upload.array('files'), async (req, res, next) => { try { - console.log('req.files:', req.files); const executionId = req.params.id; const files = await uploadExecFiles(executionId, req); - res.execution.files = files; - const updatedExecution = await res.execution.save(); res.send({ message: "Files uploaded", - files: files, - updatedExecution: updatedExecution + files: files }); } catch (err) { diff --git a/machine/machine.js b/machine/machine.js index b1c1095f2..4f65a0028 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -218,27 +218,11 @@ app.get("/projects", (req, res) => { }); app.post('/code/run', jsonParser, (req, res) => { - // Note, the body params passed need to be updated in this new function. - // dataset_file_id instead of _data_file_id (i.e. drop the first _) - - // These parameter will be included in req.body: - // src_code - required - // dataset_file_id - optional - // _id - execution_id - // experiment_id - optional - - console.log('Executing code...'); - - // Create a new Execution document for mongodb - // This will return an execution id - // Can this still be done in lab? - + // Create the temp path to store the code execuction generated files - // This uses CODE_RUN_PATH let tmppath = path.join(process.env.CODE_RUN_PATH, req.body._id.toString()); if (!fs.existsSync(tmppath)) fs.mkdirSync(tmppath, { recursive: true }); - // Set up the args for the run_code.py (same as run_old) let executionId = req.body._id; let args = [ "machine/pyutils/run_code.py", @@ -258,18 +242,15 @@ app.post('/code/run', jsonParser, (req, res) => { args.push(req.body._experiment_id); } - // Spawn the process (same as run_old) let pyProc = spawn("python", args, { cwd: process.env.PROJECT_ROOT }); let result = req.body; result.exec_results = {}; - // Capture stdout (same as run_old) pyProc.stdout.on("data", (data) => { result.exec_results = JSON.parse(data.toString()); }); - // Capture stderr (same as run_old) pyProc.stderr.on("data", (data) => { // console.log(`stderr: ${data}`); try { @@ -279,42 +260,23 @@ app.post('/code/run', jsonParser, (req, res) => { } }); - // Close close (same as run_old) pyProc.on("close", async (code) => { - // result.code = code; console.log(`child process exited with code ${code}`); - // TODO: this part is still untested, need to work on this next. const files = await machine_utils.sendExecFiles(executionId, tmppath); result.exec_results.files = files; + // delete tmp folder + fs.rmdir(tmppath, { recursive: true }, (err) => { + if (err) { + console.error(err); + } else { + console.log(tmppath + ' folder deleted'); + } + }); + res.send(result); + }); - - // Upload files from tmp folder - // re-use sendResults ? - // Add a files array to the response with the file info - // like the one returned by uploadExecFiles. - // Lab should talk to Mongo. Not the Machine. - // Looks like I'll have to use sendResults! - // Suggestion: do what sendResults does inside machine_utils.uploadExecFiles - // but, have it return the files array! - // const files = await machine_utils.uploadExecFiles(executionId, tmppath); - // result.exec_results.files = files; - // result.exec_results.files = []; - - // Don't delete tmp folder until uploadExecFiles is working correctly. - // delete tmp folder - // fs.rmdir(tmppath, { recursive: true }, (err) => { - // if (err) { - // console.error(err); - // } else { - // console.log(tmppath + ' folder deleted'); - // } - // }); - - // Send result back - // res.send(result); - }); // Starts experiment diff --git a/machine/machine_utils.js b/machine/machine_utils.js index 78e115157..26777fbfc 100644 --- a/machine/machine_utils.js +++ b/machine/machine_utils.js @@ -1,6 +1,6 @@ /* ~This file is part of the Aliro library~ -Copyright (C) 2023 Epistasis Lab, +Copyright (C) 2023 Epistasis Lab, Center for Artificial Intelligence Research and Education (CAIRE), Department of Computational Biomedicine (CBM), Cedars-Sinai Medical Center. @@ -29,7 +29,9 @@ along with this program. If not, see . */ const path = require("path"); +const fetch = require('isomorphic-fetch'); const fs = require('fs'); +const FormData = require('form-data'); // const { isConstructorDeclaration } = require('typescript'); const mime = require('mime-types'); @@ -86,40 +88,40 @@ var checkCapacity = function(projId, maxCapacity, projects) { }; +// send files generated by a code execution to be added in MongoDB. async function sendExecFiles(executionId, filepath) { try { + var files = []; const uri = `http://${process.env.LAB_HOST}:${process.env.LAB_PORT}/execapi/v1/executions/${executionId}/files`; - - const files = []; - + const formData = new FormData(); const filenames = await fs.promises.readdir(filepath); - for (const file of filenames) { - const filename = path.join(filepath, file); - const stats = await fs.promises.stat(filename); + for (const filename of filenames) { + const file = path.join(filepath, filename); + const stats = await fs.promises.stat(file); if (!stats.isDirectory()) { - const fileContent = await fs.readFile(filename); + formData.append('files', fs.createReadStream(file)); + } + } - const response = await fetch(uri, { - method: 'POST', - body: fileContent, - headers: { - 'Content-Type': 'application/octet-stream', - }, - }); + const response = await fetch(uri, { + method: 'PUT', + body: formData, + headers: { + 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`, + }, + }); - if (!response.ok) { - throw new Error(`Error sending file ${file}: ${response.status} ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Error sending files: ${response.status} ${response.statusText}`); + } - const responseBody = await response.text(); - console.log(responseBody); + const responseBody = await response.json(); - files.push({ filename, success: true }); - } - } + files = responseBody.files; return files; + } catch (error) { console.error(error); return []; @@ -127,7 +129,6 @@ async function sendExecFiles(executionId, filepath) { } - exports.getProjects = getProjects; exports.getCapacity = getCapacity; exports.checkCapacity = checkCapacity; diff --git a/machine/package.json b/machine/package.json index 12e812b55..a7db68bb9 100644 --- a/machine/package.json +++ b/machine/package.json @@ -29,6 +29,7 @@ "continuation-local-storage": "^3.1.7", "cors": "^2.7.1", "express": "^4.18.2", + "isomorphic-fetch": "^2.2.1", "lodash": "^4.17.21", "md5-file": "^3.1.1", "mime": "^1.3.6", From bef0c621a2654ac35875db5411e88f55e20c4f3f Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Wed, 13 Sep 2023 16:15:47 -0700 Subject: [PATCH 08/11] Clean up old executions and code run endpoints --- lab/routes/execapi.js | 28 ++++++------- machine/machine.js | 87 ++++++++++------------------------------ machine/machine_utils.js | 3 +- 3 files changed, 36 insertions(+), 82 deletions(-) diff --git a/lab/routes/execapi.js b/lab/routes/execapi.js index 4dab82919..0ec0f6725 100644 --- a/lab/routes/execapi.js +++ b/lab/routes/execapi.js @@ -13,6 +13,19 @@ const { } = require('../execapiutils'); /** + * Route for submitting a new code execution. + * + * @param {Object} req - HTTP request object containing request data. + * @property {string} req.body.src_code - The source code to be executed. + * @property {string} [req.body.dataset_file_id] - The dataset file ID (optional). + * @property {string} [req.body.dataset_id] - The dataset ID (optional). + * The dataset_file_id OR the dataset_id are used to load the dataset into a + * Pandas DataFrame as variable 'df' + * @property {string} [req.body.experiment_id] - The experiment ID (optional). + * The experiment_id is used to load the experiment model as variable 'model' + * @param {Object} res - HTTP response object for sending the response back to the client. + * @param {Function} next - Function to proceed to the next middleware step. + * * The only responsibility this function will have is to: * 1. Check required and optional parameters * 2. Create and save Executions @@ -106,20 +119,7 @@ router.post('/executions', async (req, res, next) => { } }); -/** - * Route for submitting a new code execution. - * - * @param {Object} req - HTTP request object containing request data. - * @property {string} req.body.src_code - The source code to be executed. - * @property {string} [req.body.dataset_file_id] - The dataset file ID (optional). - * @property {string} [req.body.dataset_id] - The dataset ID (optional). - * The dataset_file_id OR the dataset_id are used to load the dataset into a - * Pandas DataFrame as variable 'df' - * @property {string} [req.body.experiment_id] - The experiment ID (optional). - * The experiment_id is used to load the experiment model as variable 'model' - * @param {Object} res - HTTP response object for sending the response back to the client. - * @param {Function} next - Function to proceed to the next middleware step. - */ + router.post('/executions_old', async (req, res, next) => { if (req.body.src_code == null) { return res.status(400).json({ message: 'No src_code provided' }); diff --git a/machine/machine.js b/machine/machine.js index 4f65a0028..d2cf85d84 100644 --- a/machine/machine.js +++ b/machine/machine.js @@ -217,68 +217,6 @@ app.get("/projects", (req, res) => { res.send(projects); }); -app.post('/code/run', jsonParser, (req, res) => { - - // Create the temp path to store the code execuction generated files - let tmppath = path.join(process.env.CODE_RUN_PATH, req.body._id.toString()); - if (!fs.existsSync(tmppath)) fs.mkdirSync(tmppath, { recursive: true }); - - let executionId = req.body._id; - let args = [ - "machine/pyutils/run_code.py", - "--code", - req.body.src_code, - "--execution_id", - executionId, - ]; - - if (req.body._dataset_file_id !== undefined) { - args.push("--dataset_file_id"); - args.push(req.body._dataset_file_id); - } - - if (req.body._experiment_id !== undefined) { - args.push("--experiment_id"); - args.push(req.body._experiment_id); - } - - let pyProc = spawn("python", args, { cwd: process.env.PROJECT_ROOT }); - - let result = req.body; - result.exec_results = {}; - - pyProc.stdout.on("data", (data) => { - result.exec_results = JSON.parse(data.toString()); - }); - - pyProc.stderr.on("data", (data) => { - // console.log(`stderr: ${data}`); - try { - result.exec_results = JSON.parse(data.toString()); - } catch (e) { - console.log(e); - } - }); - - pyProc.on("close", async (code) => { - console.log(`child process exited with code ${code}`); - const files = await machine_utils.sendExecFiles(executionId, tmppath); - result.exec_results.files = files; - - // delete tmp folder - fs.rmdir(tmppath, { recursive: true }, (err) => { - if (err) { - console.error(err); - } else { - console.log(tmppath + ' folder deleted'); - } - }); - - res.send(result); - - }); -}); - // Starts experiment app.post("/projects/:id", jsonParser, (req, res) => { var experimentId = req.body._id; @@ -574,14 +512,19 @@ app.post("/code/run/install", jsonParser, (req, res) => { }); // run code execution -app.post("/code/run_old", jsonParser, (req, res) => { +app.post('/code/run', jsonParser, (req, res) => { + + // Create the temp path to store the code execuction generated files + let tmppath = path.join(process.env.CODE_RUN_PATH, req.body._id.toString()); + if (!fs.existsSync(tmppath)) fs.mkdirSync(tmppath, { recursive: true }); + let executionId = req.body._id; let args = [ "machine/pyutils/run_code.py", "--code", req.body.src_code, "--execution_id", - req.body._id, + executionId, ]; if (req.body._dataset_file_id !== undefined) { @@ -612,10 +555,22 @@ app.post("/code/run_old", jsonParser, (req, res) => { } }); - pyProc.on("close", (code) => { - // result.code = code; + pyProc.on("close", async (code) => { console.log(`child process exited with code ${code}`); + const files = await machine_utils.sendExecFiles(executionId, tmppath); + result.exec_results.files = files; + + // delete tmp folder + fs.rmdir(tmppath, { recursive: true }, (err) => { + if (err) { + console.error(err); + } else { + console.log(tmppath + ' folder deleted'); + } + }); + res.send(result); + }); }); diff --git a/machine/machine_utils.js b/machine/machine_utils.js index 26777fbfc..fb8920dd9 100644 --- a/machine/machine_utils.js +++ b/machine/machine_utils.js @@ -32,9 +32,8 @@ const path = require("path"); const fetch = require('isomorphic-fetch'); const fs = require('fs'); const FormData = require('form-data'); - -// const { isConstructorDeclaration } = require('typescript'); const mime = require('mime-types'); + //Generate a list of projects based on machine_config.json var getProjects = function(algorithms) { var project_list = []; From 9e4243a3ea12186ce139e1abe8c2cc8526c44dd3 Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Wed, 13 Sep 2023 19:37:23 -0700 Subject: [PATCH 09/11] Add int. tests for 'df' and 'model' on code runs --- .../datasets/test/integration/bananamodel.csv | 1001 +++++++++++++++++ .../test/integration/exec_df_iris.csv | 16 + tests/integration/jest/machineApi.test.ts | 187 ++- 3 files changed, 1195 insertions(+), 9 deletions(-) create mode 100644 data/datasets/test/integration/bananamodel.csv create mode 100644 data/datasets/test/integration/exec_df_iris.csv diff --git a/data/datasets/test/integration/bananamodel.csv b/data/datasets/test/integration/bananamodel.csv new file mode 100644 index 000000000..0ed23cc2d --- /dev/null +++ b/data/datasets/test/integration/bananamodel.csv @@ -0,0 +1,1001 @@ +At1 At2 class +1.14 -0.114 -1.0 +-1.52 -1.15 1.0 +-1.05 0.72 -1.0 +-0.9159999999999999 0.397 1.0 +-1.09 0.43700000000000006 1.0 +-0.584 0.0937 1.0 +1.83 0.452 -1.0 +-1.25 -0.28600000000000003 1.0 +1.7 1.21 1.0 +-0.48200000000000004 -0.485 1.0 +1.79 -0.45899999999999996 -1.0 +-0.122 -0.8079999999999999 -1.0 +0.0809 1.93 1.0 +-0.541 -0.332 1.0 +-1.02 0.619 -1.0 +-0.768 -1.04 -1.0 +-1.69 -0.0461 1.0 +1.26 1.21 1.0 +0.7240000000000001 0.9890000000000001 -1.0 +0.444 1.99 -1.0 +-1.01 -1.36 -1.0 +-0.863 0.496 1.0 +1.16 -0.45799999999999996 1.0 +-0.595 -0.6509999999999999 1.0 +-0.77 0.364 1.0 +-0.871 -0.825 1.0 +0.996 -1.7 1.0 +1.28 0.691 -1.0 +0.925 0.895 -1.0 +-0.687 -1.29 -1.0 +1.74 0.9640000000000001 1.0 +1.18 -0.335 1.0 +2.52 1.43 1.0 +1.71 -0.044000000000000004 -1.0 +0.271 -0.591 1.0 +1.12 0.626 -1.0 +1.3 0.196 -1.0 +-1.59 -0.68 1.0 +0.408 0.0673 1.0 +1.13 1.48 -1.0 +0.763 0.9209999999999999 -1.0 +-1.41 1.11 -1.0 +-0.75 -0.8809999999999999 -1.0 +1.16 0.978 -1.0 +1.13 0.405 -1.0 +-0.522 -1.34 -1.0 +-1.41 0.894 -1.0 +0.00902 -0.434 1.0 +-2.14 -1.43 1.0 +-1.31 1.25 -1.0 +0.040999999999999995 -1.13 -1.0 +0.0483 0.866 -1.0 +-2.11 0.193 -1.0 +0.522 1.46 -1.0 +0.0284 1.62 -1.0 +0.396 -0.606 1.0 +0.536 0.9209999999999999 -1.0 +0.315 -0.182 1.0 +-0.12300000000000001 -1.07 -1.0 +0.526 1.48 -1.0 +0.00665 0.0118 1.0 +-0.35200000000000004 -0.49 1.0 +-0.0701 -1.23 -1.0 +-0.149 -1.2 -1.0 +0.785 0.0481 1.0 +-1.62 0.593 -1.0 +-0.0314 -1.01 -1.0 +-0.285 -1.1 -1.0 +1.33 1.51 1.0 +1.09 -1.37 1.0 +-0.223 -1.28 -1.0 +-0.0341 -1.07 -1.0 +1.22 1.13 -1.0 +-1.67 -1.26 1.0 +1.97 -0.772 -1.0 +-0.508 -0.715 1.0 +0.603 -0.10800000000000001 1.0 +-0.32299999999999995 -0.213 1.0 +-0.124 -1.12 -1.0 +-0.439 -0.961 -1.0 +0.201 0.0024 1.0 +0.812 0.708 -1.0 +0.888 0.8170000000000001 -1.0 +0.0238 0.0836 1.0 +0.415 -1.0 -1.0 +-0.308 2.19 1.0 +0.767 -0.248 1.0 +1.23 -1.2 1.0 +1.33 1.63 1.0 +0.27 0.0491 1.0 +-1.69 -1.87 1.0 +0.495 -0.281 1.0 +-0.519 -0.799 -1.0 +-1.99 0.5489999999999999 -1.0 +1.36 -0.732 1.0 +-1.03 0.654 -1.0 +0.431 -1.33 -1.0 +-0.0583 -1.15 -1.0 +-0.209 0.345 1.0 +1.26 -1.37 1.0 +-1.78 -0.37799999999999995 1.0 +0.98 -0.0439 1.0 +-0.0536 1.6 -1.0 +-1.39 -0.451 1.0 +1.22 -0.361 1.0 +1.22 0.561 -1.0 +-0.838 0.35600000000000004 1.0 +-0.446 -0.861 -1.0 +1.17 -1.39 1.0 +-0.0868 -1.33 -1.0 +-1.12 0.5760000000000001 -1.0 +-0.28 -1.3 -1.0 +-0.0269 0.958 1.0 +-0.6970000000000001 1.35 -1.0 +1.38 -1.74 1.0 +0.408 1.16 -1.0 +1.2 1.54 1.0 +2.07 1.02 1.0 +-0.462 -0.187 1.0 +1.27 0.589 -1.0 +-0.10099999999999999 -0.765 -1.0 +-0.8190000000000001 1.26 -1.0 +0.615 -0.0228 1.0 +-1.79 -0.98 1.0 +-1.79 -0.9540000000000001 1.0 +0.826 1.5 1.0 +0.845 0.7659999999999999 -1.0 +-0.47600000000000003 -1.49 -1.0 +1.01 0.48 -1.0 +1.39 -0.376 -1.0 +0.35700000000000004 -1.07 -1.0 +0.77 1.4 -1.0 +-1.08 0.114 1.0 +-0.795 -1.43 -1.0 +0.706 1.38 1.0 +-1.26 0.23 1.0 +-0.833 -0.569 -1.0 +-0.0303 2.11 -1.0 +-0.223 -0.419 1.0 +-0.562 -0.873 -1.0 +0.9259999999999999 0.972 -1.0 +-1.86 -1.57 1.0 +-0.8059999999999999 0.941 -1.0 +-0.214 0.38 1.0 +0.7440000000000001 1.42 1.0 +1.94 -0.65 -1.0 +1.77 1.35 1.0 +-0.903 0.10099999999999999 1.0 +-0.0418 -1.02 -1.0 +0.309 -0.175 1.0 +-0.634 -0.9690000000000001 -1.0 +-1.62 0.102 -1.0 +-1.21 1.27 -1.0 +0.529 0.133 -1.0 +0.386 1.55 -1.0 +-0.0962 0.19 1.0 +-0.536 1.13 -1.0 +1.02 -0.261 1.0 +-0.8759999999999999 -1.0 -1.0 +2.35 1.21 1.0 +-0.982 -1.08 -1.0 +-1.16 0.469 1.0 +-1.74 0.693 -1.0 +-1.1 1.16 -1.0 +0.0179 -0.7 1.0 +-0.62 -0.9840000000000001 -1.0 +-1.52 0.66 -1.0 +1.07 -0.985 1.0 +0.917 0.525 -1.0 +-0.0482 1.52 -1.0 +1.49 1.55 1.0 +-0.584 1.01 -1.0 +-0.0625 -0.12 1.0 +-1.68 1.08 -1.0 +-1.77 -0.9229999999999999 1.0 +-0.20199999999999999 0.7120000000000001 1.0 +1.06 -0.42700000000000005 1.0 +1.71 1.1 1.0 +-1.7 0.872 -1.0 +1.86 1.14 1.0 +-0.529 -0.35700000000000004 1.0 +-0.41100000000000003 -1.3 -1.0 +0.6659999999999999 1.36 1.0 +0.162 -0.7909999999999999 -1.0 +-0.29600000000000004 0.255 1.0 +-0.0347 1.3 -1.0 +0.294 -1.5 -1.0 +-0.52 -1.17 -1.0 +0.741 -1.2 1.0 +-1.33 1.08 -1.0 +-0.0987 0.966 1.0 +0.0131 -0.9690000000000001 -1.0 +0.8959999999999999 1.58 1.0 +-0.41200000000000003 0.49200000000000005 1.0 +0.6829999999999999 1.07 -1.0 +-0.753 -0.0615 1.0 +-0.0674 1.85 -1.0 +1.08 0.6829999999999999 -1.0 +1.15 0.243 -1.0 +0.49700000000000005 -0.6709999999999999 1.0 +0.174 1.92 -1.0 +1.64 0.0477 -1.0 +-0.478 -0.7959999999999999 -1.0 +-0.447 -1.0 -1.0 +-1.04 -0.2 1.0 +2.06 -0.48200000000000004 -1.0 +-0.89 -1.17 -1.0 +0.312 -1.23 -1.0 +0.976 -1.39 1.0 +0.29100000000000004 -0.795 -1.0 +0.303 -0.14300000000000002 1.0 +1.1 -0.695 1.0 +0.579 -1.02 1.0 +-1.54 0.685 -1.0 +0.9159999999999999 0.14400000000000002 1.0 +1.18 0.614 -1.0 +0.254 -0.862 -1.0 +-0.877 -0.401 1.0 +1.97 0.99 1.0 +0.47100000000000003 1.39 -1.0 +1.63 0.475 -1.0 +0.945 0.309 -1.0 +0.121 -1.3 -1.0 +1.06 -0.69 1.0 +1.42 -0.32 1.0 +0.535 0.504 1.0 +0.912 -0.114 -1.0 +-0.75 1.14 -1.0 +1.14 -0.209 1.0 +-1.03 -1.09 -1.0 +0.268 -1.08 1.0 +-1.77 -0.828 1.0 +0.37799999999999995 -0.0609 1.0 +1.19 0.113 1.0 +-0.168 1.32 -1.0 +2.07 1.21 1.0 +-1.07 0.7070000000000001 -1.0 +-1.19 0.309 -1.0 +1.19 0.6970000000000001 -1.0 +0.121 1.72 -1.0 +1.23 -0.0582 1.0 +0.44 0.39899999999999997 1.0 +-0.723 0.633 -1.0 +-0.63 0.9490000000000001 -1.0 +0.14300000000000002 0.263 1.0 +1.23 -0.615 1.0 +1.9 -0.0565 -1.0 +1.22 -0.0172 -1.0 +0.0107 0.0258 1.0 +0.36 -0.715 1.0 +0.139 -1.01 -1.0 +0.308 -1.06 -1.0 +-0.645 -0.23800000000000002 1.0 +1.15 0.715 -1.0 +-0.531 -1.27 -1.0 +1.05 0.701 -1.0 +0.0681 -0.76 -1.0 +-0.595 1.46 -1.0 +0.72 0.9890000000000001 -1.0 +-1.86 -0.963 1.0 +0.0692 -0.828 -1.0 +0.863 0.765 -1.0 +-0.574 1.32 -1.0 +-0.467 -1.51 -1.0 +1.43 0.484 -1.0 +-0.5920000000000001 -1.17 -1.0 +0.122 -0.040999999999999995 1.0 +0.909 0.11800000000000001 -1.0 +-0.705 -0.93 -1.0 +1.05 0.45299999999999996 -1.0 +-0.7859999999999999 -1.33 -1.0 +0.07 1.7 -1.0 +-0.36 -1.28 -1.0 +-1.25 0.985 -1.0 +1.33 1.42 1.0 +0.778 -0.6940000000000001 1.0 +-1.87 -1.26 1.0 +0.17800000000000002 1.49 -1.0 +-1.03 1.18 -1.0 +-0.956 -0.818 1.0 +0.0345 -0.691 -1.0 +-1.25 0.512 1.0 +-0.187 -1.38 -1.0 +-1.24 1.0 -1.0 +0.32899999999999996 -0.37 1.0 +-1.37 -0.8029999999999999 1.0 +0.529 1.62 -1.0 +-0.23 1.08 -1.0 +-0.619 -0.748 -1.0 +-0.914 1.07 -1.0 +0.163 -1.31 -1.0 +1.19 -1.34 1.0 +0.983 -1.31 1.0 +-1.63 -1.41 1.0 +-1.02 -0.29600000000000004 1.0 +-0.387 0.26 1.0 +-1.59 -1.0 1.0 +1.69 -0.7240000000000001 -1.0 +-0.8690000000000001 -0.0393 1.0 +-1.9 0.106 1.0 +-1.75 0.722 -1.0 +-1.06 -1.07 -1.0 +-0.509 -1.41 -1.0 +-0.11199999999999999 -0.841 1.0 +1.08 0.79 -1.0 +0.498 0.5670000000000001 -1.0 +-0.7959999999999999 -1.63 -1.0 +-0.3 -1.04 -1.0 +-2.34 0.24600000000000002 -1.0 +0.8420000000000001 -1.41 1.0 +-0.0263 -1.35 -1.0 +0.586 1.15 -1.0 +1.87 -0.239 -1.0 +0.40399999999999997 -0.868 -1.0 +0.564 -1.13 1.0 +-0.0584 0.54 1.0 +-1.58 -1.26 1.0 +-0.813 -0.57 1.0 +-0.579 -0.748 1.0 +-1.35 1.32 -1.0 +-0.639 -0.938 -1.0 +1.49 -0.007790000000000001 -1.0 +0.7190000000000001 0.0781 1.0 +0.0346 1.7 -1.0 +-0.27399999999999997 0.0468 1.0 +0.207 0.0304 1.0 +1.63 -0.251 -1.0 +-0.207 -0.745 -1.0 +-0.7170000000000001 1.0 -1.0 +1.17 -0.5670000000000001 1.0 +0.6609999999999999 1.69 -1.0 +-1.13 0.6 -1.0 +-0.109 -1.25 -1.0 +0.68 0.7020000000000001 -1.0 +0.867 0.81 -1.0 +-0.562 -1.01 -1.0 +1.79 -0.102 -1.0 +-1.87 -1.1 1.0 +-0.946 -0.19699999999999998 1.0 +0.812 -0.195 1.0 +0.287 -0.517 1.0 +0.262 -0.6729999999999999 -1.0 +-0.358 -1.02 -1.0 +-0.392 -1.27 -1.0 +-1.2 -1.02 -1.0 +-0.337 -1.45 -1.0 +-1.54 -0.643 1.0 +0.311 1.76 -1.0 +-0.473 -0.7490000000000001 -1.0 +1.71 0.0217 -1.0 +1.88 -0.368 -1.0 +-1.18 -0.0978 1.0 +0.0431 -0.0199 1.0 +1.23 0.354 -1.0 +0.0788 -0.0998 1.0 +0.0544 -1.59 -1.0 +-0.24 1.72 1.0 +-0.496 -1.09 -1.0 +-0.636 1.35 -1.0 +1.15 0.634 -1.0 +1.29 -1.43 1.0 +-0.983 1.06 -1.0 +-0.20600000000000002 -0.474 1.0 +0.298 -0.8290000000000001 1.0 +-0.972 -1.06 -1.0 +1.21 0.354 -1.0 +0.32799999999999996 1.71 1.0 +0.313 -1.07 -1.0 +-1.24 -0.35200000000000004 1.0 +-1.39 -0.68 1.0 +1.2 -1.56 1.0 +-0.20800000000000002 0.332 1.0 +0.5529999999999999 1.54 -1.0 +0.354 -1.19 -1.0 +-0.74 -1.53 -1.0 +-0.769 1.35 -1.0 +-0.0381 -0.7909999999999999 -1.0 +-0.102 -0.13 1.0 +-1.06 0.496 1.0 +-0.48100000000000004 1.09 -1.0 +0.866 -0.917 1.0 +1.06 0.266 -1.0 +1.73 1.68 1.0 +-0.516 -0.935 -1.0 +-0.893 -1.27 -1.0 +1.2 -0.948 1.0 +0.763 1.15 -1.0 +0.794 0.762 -1.0 +-0.314 0.0194 1.0 +-0.456 1.55 -1.0 +1.04 0.563 -1.0 +-0.0978 -0.424 1.0 +1.37 -0.643 -1.0 +-0.201 0.0726 1.0 +-1.97 0.611 -1.0 +0.99 -0.0762 1.0 +1.28 0.902 -1.0 +0.569 1.62 1.0 +-1.2 -0.451 1.0 +-0.508 1.48 -1.0 +-1.69 -0.175 -1.0 +-0.298 -1.26 -1.0 +0.301 -1.17 -1.0 +0.975 1.48 -1.0 +-0.853 -0.254 1.0 +0.8370000000000001 1.75 1.0 +0.379 1.36 -1.0 +-0.364 -0.0828 1.0 +0.9390000000000001 -0.35100000000000003 1.0 +1.24 0.5539999999999999 -1.0 +1.37 -0.331 1.0 +-0.48 -0.9129999999999999 -1.0 +1.34 -0.261 -1.0 +0.28600000000000003 1.23 -1.0 +0.672 2.03 -1.0 +-0.52 -1.52 -1.0 +-0.513 1.29 -1.0 +-0.648 -0.631 -1.0 +-0.113 -0.19699999999999998 1.0 +0.10300000000000001 0.32899999999999996 1.0 +-0.941 0.976 -1.0 +-0.5329999999999999 -0.402 1.0 +1.09 -1.59 1.0 +-0.395 -0.7559999999999999 -1.0 +1.46 -0.41700000000000004 1.0 +-0.7240000000000001 -1.26 -1.0 +-1.59 -0.98 1.0 +0.159 1.51 -1.0 +-0.843 1.4 -1.0 +-0.581 -1.27 -1.0 +-0.268 -1.26 -1.0 +-1.58 -0.8420000000000001 1.0 +-0.52 1.2 -1.0 +-0.0203 1.92 1.0 +-1.52 0.934 -1.0 +1.05 0.444 -1.0 +0.35100000000000003 -0.0402 1.0 +-0.322 -1.12 -1.0 +-0.654 -0.9990000000000001 -1.0 +0.165 -1.58 -1.0 +2.6 0.951 1.0 +1.52 -0.0653 -1.0 +-1.29 -0.529 1.0 +1.18 0.392 -1.0 +-0.00264 -1.02 -1.0 +1.24 0.273 -1.0 +1.45 0.39799999999999996 -1.0 +0.54 -1.26 -1.0 +-0.324 -1.03 -1.0 +-1.69 -1.2 1.0 +-0.455 0.868 -1.0 +2.16 1.16 1.0 +-0.23199999999999998 -1.4 -1.0 +0.685 1.69 -1.0 +-0.0537 -1.52 -1.0 +1.03 0.5479999999999999 -1.0 +-0.758 -1.21 -1.0 +-0.259 0.496 1.0 +-0.47200000000000003 0.41100000000000003 1.0 +-0.127 -1.33 -1.0 +-0.784 -1.05 -1.0 +-1.73 -1.32 1.0 +-1.24 -0.919 1.0 +0.275 -0.987 -1.0 +1.66 0.672 -1.0 +-1.07 0.963 -1.0 +-0.262 1.28 -1.0 +-1.4 -0.37 1.0 +-0.28800000000000003 0.305 1.0 +0.778 1.44 1.0 +1.23 -0.493 -1.0 +-1.27 -0.273 1.0 +-0.9940000000000001 1.14 -1.0 +0.74 1.2 1.0 +0.489 0.904 -1.0 +0.805 -0.237 1.0 +-1.86 -1.47 1.0 +-0.617 -1.02 -1.0 +-0.66 0.965 -1.0 +0.48700000000000004 -0.13 1.0 +-0.0508 -1.19 -1.0 +0.96 -0.522 1.0 +-1.21 0.778 -1.0 +0.149 1.98 -1.0 +-1.76 -0.903 1.0 +0.0486 -0.389 1.0 +-0.8909999999999999 1.0 -1.0 +0.263 -1.48 -1.0 +1.21 1.28 -1.0 +-1.46 0.0829 1.0 +-1.04 1.19 -1.0 +-1.69 -0.542 1.0 +0.7070000000000001 0.0524 1.0 +1.31 -0.254 -1.0 +-0.488 -0.5660000000000001 1.0 +1.8 -0.8109999999999999 -1.0 +1.84 1.18 1.0 +1.49 -0.156 -1.0 +-0.8809999999999999 -1.59 -1.0 +0.529 0.7879999999999999 1.0 +0.5710000000000001 0.19699999999999998 1.0 +-2.07 0.0318 -1.0 +-1.23 -0.308 1.0 +0.682 1.5 -1.0 +-1.33 0.0398 1.0 +-1.35 0.424 1.0 +0.307 -0.6779999999999999 -1.0 +0.8590000000000001 -0.9520000000000001 1.0 +-0.672 0.6829999999999999 1.0 +1.36 0.258 -1.0 +-0.37 1.48 -1.0 +-1.9 -1.1 1.0 +-1.38 -0.32 1.0 +0.23399999999999999 0.958 -1.0 +-1.01 -0.7709999999999999 1.0 +-0.7390000000000001 0.578 1.0 +-0.107 -1.5 -1.0 +0.5920000000000001 0.973 -1.0 +0.52 1.21 -1.0 +0.41600000000000004 1.86 1.0 +-0.18100000000000002 0.344 1.0 +1.27 -0.441 -1.0 +0.42200000000000004 -0.934 1.0 +-0.7040000000000001 -0.802 -1.0 +0.08800000000000001 -1.25 -1.0 +0.9520000000000001 0.42700000000000005 -1.0 +1.39 -0.218 -1.0 +-1.08 -1.3 -1.0 +1.25 -0.35 -1.0 +0.183 -1.21 -1.0 +-0.0765 -1.12 -1.0 +-0.40700000000000003 0.602 1.0 +0.975 0.809 -1.0 +-0.0552 0.882 1.0 +-0.946 1.02 -1.0 +1.11 -0.106 -1.0 +-0.794 1.07 -1.0 +-0.652 -1.1 -1.0 +-2.08 -1.78 1.0 +-0.631 -0.867 -1.0 +-0.856 -1.31 -1.0 +-0.215 -1.01 -1.0 +-1.12 0.154 1.0 +-1.1 0.135 1.0 +-0.134 -1.1 -1.0 +0.8440000000000001 0.243 -1.0 +0.47200000000000003 0.289 1.0 +1.64 0.174 -1.0 +-0.413 -0.0275 1.0 +-0.875 0.591 1.0 +-0.40399999999999997 0.17800000000000002 1.0 +-0.297 -0.467 1.0 +-0.698 -1.26 -1.0 +1.2 0.298 -1.0 +-0.34600000000000003 -1.5 -1.0 +-1.14 -0.494 1.0 +-0.0242 -0.659 1.0 +-0.495 -1.2 -1.0 +-0.8029999999999999 -0.782 -1.0 +-0.6409999999999999 0.516 1.0 +-0.0874 -0.948 -1.0 +-1.04 0.679 -1.0 +0.701 0.0237 1.0 +-0.677 -1.41 -1.0 +-0.516 0.831 1.0 +-0.8640000000000001 -1.56 -1.0 +0.863 -1.02 1.0 +-1.12 0.5589999999999999 1.0 +-0.779 -1.14 -1.0 +-0.9 1.49 -1.0 +2.43 1.39 1.0 +-1.67 -1.73 1.0 +0.743 -1.18 1.0 +-1.52 -0.6579999999999999 1.0 +1.28 -0.17600000000000002 1.0 +1.58 -1.29 1.0 +0.61 0.488 -1.0 +-2.15 -0.858 1.0 +-0.0307 0.6779999999999999 1.0 +-0.22699999999999998 -1.19 -1.0 +0.642 1.65 1.0 +-1.58 -1.25 1.0 +0.373 1.48 -1.0 +0.33299999999999996 -0.845 1.0 +-1.18 -0.758 1.0 +1.15 -1.52 1.0 +-0.601 -0.539 1.0 +-1.57 -1.74 1.0 +0.652 -0.6459999999999999 1.0 +-1.43 1.26 -1.0 +0.71 0.0529 1.0 +1.18 0.585 -1.0 +-1.01 -1.27 1.0 +0.13 1.38 -1.0 +0.469 -0.321 1.0 +-1.08 0.72 -1.0 +0.987 0.858 -1.0 +1.57 -0.389 -1.0 +-0.41100000000000003 0.727 1.0 +-0.72 0.18899999999999997 1.0 +-0.00329 -0.0777 1.0 +-0.937 -0.086 1.0 +2.21 1.02 1.0 +-0.606 -0.997 -1.0 +1.4 0.377 -1.0 +0.37 0.312 1.0 +-1.41 -0.214 1.0 +-0.192 1.66 -1.0 +-0.0246 -0.141 1.0 +1.07 -0.0433 -1.0 +-0.47700000000000004 -1.25 -1.0 +-1.35 -0.485 1.0 +0.996 0.225 -1.0 +-1.25 -1.28 -1.0 +-0.198 1.67 1.0 +-0.8 -0.282 1.0 +0.168 0.345 1.0 +0.851 1.46 -1.0 +1.45 -1.24 1.0 +-1.02 -1.18 -1.0 +-1.31 0.9159999999999999 -1.0 +-0.457 -0.322 1.0 +0.0744 1.04 -1.0 +0.902 -1.08 1.0 +-0.904 0.953 -1.0 +-0.11 -0.9109999999999999 -1.0 +1.01 -1.05 1.0 +0.441 -1.13 -1.0 +-0.287 -0.726 1.0 +0.205 -0.47100000000000003 1.0 +-0.625 1.14 -1.0 +-0.623 -0.737 1.0 +-0.254 -0.932 -1.0 +-0.8170000000000001 -1.19 -1.0 +-1.73 0.87 -1.0 +0.17600000000000002 1.94 -1.0 +0.899 -0.235 1.0 +0.8340000000000001 -0.584 1.0 +0.15 -0.256 1.0 +-1.23 -0.336 1.0 +0.866 -0.807 1.0 +1.07 0.0877 -1.0 +-0.256 1.52 -1.0 +2.34 -1.25 -1.0 +-0.374 -1.08 -1.0 +-1.16 0.381 -1.0 +-1.3 -1.17 1.0 +0.527 -0.251 1.0 +-0.748 -0.57 1.0 +1.02 0.997 -1.0 +0.105 0.10300000000000001 1.0 +0.563 0.13699999999999998 1.0 +0.317 -0.0233 1.0 +0.15 1.3 -1.0 +-1.31 0.0072299999999999994 1.0 +1.42 -0.146 -1.0 +0.5760000000000001 1.7 1.0 +-0.106 -0.316 1.0 +-1.2 0.932 -1.0 +-1.14 0.488 -1.0 +-0.151 1.66 -1.0 +-1.46 -0.909 1.0 +1.36 -0.425 -1.0 +1.41 0.83 -1.0 +0.17600000000000002 -1.11 -1.0 +-0.526 1.26 -1.0 +0.552 -1.07 1.0 +-1.14 0.304 1.0 +-1.64 0.14300000000000002 1.0 +-0.18899999999999997 -1.24 -1.0 +0.775 0.0171 1.0 +-0.598 1.22 -1.0 +-1.71 -1.29 1.0 +1.52 0.409 -1.0 +0.5479999999999999 -0.9590000000000001 -1.0 +-0.636 -0.988 1.0 +-0.0723 -1.08 -1.0 +0.0949 -0.8540000000000001 1.0 +0.726 0.289 -1.0 +1.43 0.147 -1.0 +-0.0238 0.000407 1.0 +0.9390000000000001 1.69 1.0 +-1.29 0.511 -1.0 +0.324 1.68 1.0 +-0.506 -1.09 -1.0 +0.341 -1.32 -1.0 +-0.978 0.9309999999999999 -1.0 +-1.31 -0.22899999999999998 1.0 +1.29 0.677 -1.0 +1.01 -0.94 1.0 +0.0219 -0.38799999999999996 1.0 +-0.647 -1.22 -1.0 +-0.31 -1.38 -1.0 +-0.146 0.132 1.0 +1.38 0.877 -1.0 +0.24 -1.14 -1.0 +-1.01 -1.53 -1.0 +1.21 -1.02 1.0 +-0.623 1.24 -1.0 +-1.42 0.83 -1.0 +-0.0908 -0.846 -1.0 +1.39 -0.872 1.0 +0.502 -0.536 1.0 +-0.046 1.51 -1.0 +-1.35 -1.19 1.0 +1.77 0.109 -1.0 +-0.166 -0.986 -1.0 +0.152 1.53 -1.0 +0.308 0.9329999999999999 -1.0 +-1.11 0.439 1.0 +-1.87 -0.451 1.0 +-1.66 -0.735 1.0 +1.18 0.424 -1.0 +-0.109 -0.884 -1.0 +0.243 0.304 1.0 +1.04 0.816 -1.0 +0.514 1.27 -1.0 +-0.145 1.67 -1.0 +0.16899999999999998 1.86 1.0 +-0.9840000000000001 -0.807 -1.0 +0.379 -0.755 -1.0 +-1.11 0.655 1.0 +-0.9179999999999999 1.13 -1.0 +-0.43 -0.722 -1.0 +-1.62 -1.85 1.0 +0.377 0.198 1.0 +-0.32899999999999996 -0.858 -1.0 +0.125 2.16 -1.0 +-0.10800000000000001 -1.3 -1.0 +-0.11800000000000001 1.7 1.0 +0.826 0.7440000000000001 -1.0 +-1.1 0.8109999999999999 -1.0 +1.2 -0.598 1.0 +-1.89 -0.727 1.0 +1.45 0.22899999999999998 -1.0 +0.833 -0.254 1.0 +-1.7 -0.845 1.0 +0.272 1.82 1.0 +0.179 1.81 -1.0 +-0.951 0.5329999999999999 -1.0 +-1.07 0.534 1.0 +-0.8740000000000001 0.922 -1.0 +-0.0715 1.41 -1.0 +0.195 -0.29600000000000004 1.0 +0.373 -1.17 -1.0 +0.8079999999999999 -0.831 1.0 +-0.504 1.03 -1.0 +1.39 0.262 -1.0 +1.12 0.862 -1.0 +0.755 0.22 1.0 +1.57 -0.114 -1.0 +1.32 1.36 1.0 +0.802 0.133 -1.0 +-0.5579999999999999 0.5720000000000001 1.0 +0.27 0.696 1.0 +-0.606 -0.546 1.0 +-0.556 -0.505 1.0 +-1.74 0.254 -1.0 +0.14300000000000002 0.337 1.0 +1.19 -0.40399999999999997 -1.0 +-0.16 -1.36 -1.0 +1.29 0.153 -1.0 +1.47 -0.312 1.0 +-0.853 -1.17 -1.0 +-0.377 0.607 1.0 +0.816 0.306 1.0 +1.16 -1.08 1.0 +0.37799999999999995 -0.56 1.0 +-0.5670000000000001 -1.14 -1.0 +0.934 0.258 1.0 +1.01 -0.743 1.0 +0.96 -0.9 1.0 +-0.249 0.0234 1.0 +1.45 -0.26 -1.0 +-0.49700000000000005 1.07 -1.0 +0.8270000000000001 0.755 -1.0 +0.604 0.374 -1.0 +2.51 1.11 1.0 +0.154 -0.653 1.0 +-0.264 -0.768 1.0 +-0.573 -0.789 1.0 +0.792 0.988 -1.0 +-0.0338 1.52 -1.0 +1.59 -0.522 -1.0 +-0.61 -0.306 1.0 +0.184 1.7 1.0 +0.0317 -0.821 1.0 +0.7809999999999999 0.415 -1.0 +0.11800000000000001 -1.37 -1.0 +-1.57 0.9420000000000001 -1.0 +0.415 1.8 -1.0 +0.552 2.05 -1.0 +-1.21 -0.934 -1.0 +-2.14 -0.47600000000000003 1.0 +0.0154 -1.41 -1.0 +-0.0789 -1.35 -1.0 +-0.397 1.32 -1.0 +-0.466 0.145 1.0 +0.23800000000000002 -1.51 -1.0 +-0.433 -1.13 -1.0 +1.3 0.555 -1.0 +0.0991 -1.05 -1.0 +0.59 -0.7959999999999999 1.0 +-0.235 -0.617 1.0 +0.7959999999999999 0.429 1.0 +-1.97 -0.271 1.0 +0.284 2.37 -1.0 +-0.39 -0.209 1.0 +0.165 -0.405 1.0 +-0.424 2.01 -1.0 +-0.934 -1.58 -1.0 +0.218 -0.368 1.0 +-0.28300000000000003 1.88 1.0 +-0.34700000000000003 -1.5 -1.0 +-0.342 -1.31 -1.0 +0.313 0.18899999999999997 1.0 +-0.341 -0.0358 1.0 +-0.00698 -1.15 -1.0 +-1.73 -1.3 1.0 +-0.451 0.775 1.0 +-1.62 -0.7120000000000001 1.0 +-0.862 -0.0613 1.0 +-0.828 -0.8390000000000001 -1.0 +-1.01 1.07 -1.0 +1.02 0.858 -1.0 +-0.255 0.387 1.0 +0.11800000000000001 0.13 1.0 +-0.7859999999999999 -0.64 1.0 +-0.0895 1.65 1.0 +2.46 1.23 1.0 +-0.8029999999999999 -0.705 -1.0 +-0.618 -1.2 -1.0 +0.713 1.02 -1.0 +0.976 -1.53 1.0 +1.47 1.21 1.0 +-1.97 -0.674 1.0 +-0.012 1.42 -1.0 +0.748 -0.5579999999999999 1.0 +-1.45 -0.203 1.0 +0.19899999999999998 -0.86 -1.0 +-1.68 -0.51 1.0 +0.56 0.491 -1.0 +0.21600000000000003 -1.12 -1.0 +-0.23199999999999998 -0.843 1.0 +1.66 1.42 1.0 +0.301 2.01 -1.0 +1.33 0.825 -1.0 +1.18 0.08 -1.0 +-1.59 0.138 -1.0 +1.78 -0.508 -1.0 +1.37 -1.52 1.0 +0.802 0.431 1.0 +0.9129999999999999 -0.318 1.0 +0.00564 -1.14 -1.0 +-1.08 1.31 -1.0 +0.379 1.37 -1.0 +-0.106 -0.9329999999999999 -1.0 +0.8540000000000001 -0.8270000000000001 1.0 +-1.47 -0.212 1.0 +1.24 -1.33 1.0 +-0.314 -0.9840000000000001 -1.0 +-0.474 -0.9109999999999999 -1.0 +0.659 -0.057 1.0 +1.83 0.349 -1.0 +1.51 -1.15 1.0 +-0.237 -1.4 -1.0 +0.016 -1.13 -1.0 +0.26 1.91 -1.0 +-0.10400000000000001 -1.09 -1.0 +-0.7240000000000001 -1.48 -1.0 +1.24 -0.145 -1.0 +-0.7759999999999999 -1.26 -1.0 +1.16 0.0489 -1.0 +1.05 -2.08 1.0 +0.14300000000000002 1.05 -1.0 +-1.42 -1.51 1.0 +1.21 0.642 -1.0 +0.551 1.48 -1.0 +0.191 1.72 -1.0 +0.345 -0.8909999999999999 -1.0 +-0.396 -1.38 -1.0 +-1.59 -0.7659999999999999 1.0 +0.903 -1.17 1.0 +1.57 -0.0908 -1.0 +1.18 0.9129999999999999 -1.0 +-0.32299999999999995 -0.264 1.0 +-0.302 -1.22 -1.0 +-1.26 -0.29600000000000004 1.0 +0.8740000000000001 -1.41 1.0 +-0.885 1.05 -1.0 +-1.52 -0.74 1.0 +-0.716 -0.19399999999999998 1.0 +-0.579 -0.397 1.0 +-1.52 0.38799999999999996 -1.0 +1.57 -0.5660000000000001 -1.0 +-0.7020000000000001 -1.2 -1.0 +-0.366 -0.882 -1.0 +1.45 -1.78 1.0 +-1.22 0.831 -1.0 +1.39 -1.11 1.0 +0.9690000000000001 0.9209999999999999 -1.0 +0.659 1.07 -1.0 +-0.23800000000000002 0.126 1.0 +-0.5760000000000001 -0.46299999999999997 1.0 +0.171 -0.8859999999999999 -1.0 +0.893 0.345 -1.0 +-1.49 0.885 -1.0 +0.258 0.309 1.0 +-0.609 -0.917 -1.0 +1.05 0.6509999999999999 -1.0 +-0.8270000000000001 -0.506 1.0 +0.374 -1.39 -1.0 +-0.784 0.9690000000000001 -1.0 +1.08 1.42 1.0 +0.146 -0.151 1.0 +-1.93 0.603 -1.0 +-0.7340000000000001 -1.09 -1.0 +-1.19 -0.529 1.0 +0.0176 0.0932 1.0 +1.72 -0.44799999999999995 -1.0 +1.37 0.625 -1.0 +1.3 0.18600000000000003 -1.0 +-0.0618 0.358 1.0 +-0.929 -1.39 -1.0 +1.14 -0.0771 -1.0 +-0.85 -0.92 -1.0 +-1.78 -1.49 1.0 +-1.86 -0.977 1.0 +-0.6990000000000001 -0.0005780000000000001 1.0 +-0.995 -1.39 -1.0 +-0.233 0.593 1.0 +-0.415 1.48 -1.0 +-1.66 -0.792 1.0 +-0.152 -0.669 -1.0 +-0.6729999999999999 0.313 1.0 +-1.24 -0.72 1.0 +0.73 -0.37 -1.0 +-1.25 0.10400000000000001 1.0 +1.85 1.38 1.0 +0.134 -0.922 -1.0 +0.546 -0.195 1.0 +0.604 1.03 -1.0 +-1.13 -1.08 1.0 +-1.13 0.193 1.0 +-1.62 1.02 -1.0 +-1.07 -1.11 1.0 +-0.44299999999999995 1.86 -1.0 +0.0438 0.536 1.0 +-0.262 -1.2 -1.0 +0.156 0.152 1.0 +-0.311 0.995 1.0 +0.639 1.2 -1.0 +-1.42 -0.36 1.0 +-1.43 0.7070000000000001 -1.0 +1.52 -1.34 1.0 +-1.91 -2.24 1.0 +0.132 2.0 -1.0 +2.06 1.12 1.0 +0.512 -1.34 -1.0 +-0.138 -0.951 -1.0 +1.15 0.8240000000000001 -1.0 +0.233 -1.29 -1.0 +-0.124 -1.27 -1.0 +0.0584 -0.638 1.0 +0.486 0.245 1.0 +-0.0227 0.163 1.0 +1.15 1.16 1.0 +0.8340000000000001 1.24 -1.0 +-2.03 -1.65 1.0 +0.0205 1.23 -1.0 +0.522 1.36 1.0 +-0.8009999999999999 -0.24 1.0 +-0.17800000000000002 -0.23800000000000002 1.0 +1.16 1.05 -1.0 +-1.94 -1.03 1.0 +1.45 0.273 -1.0 +-1.07 1.16 -1.0 +0.413 -1.3 -1.0 +1.32 0.0928 -1.0 +0.322 1.68 1.0 +-0.292 -1.32 -1.0 +0.20600000000000002 1.19 -1.0 +-0.639 -1.37 -1.0 +0.40700000000000003 1.67 1.0 +0.14400000000000002 1.56 -1.0 +-1.58 0.588 -1.0 +-1.58 0.807 -1.0 +0.6829999999999999 -0.0347 1.0 +-0.6679999999999999 -1.13 -1.0 +-2.05 -1.4 1.0 +-0.679 -0.0957 1.0 +-0.924 1.45 -1.0 +-0.547 -1.31 -1.0 +0.0296 1.2 -1.0 +2.23 0.895 1.0 +-0.56 -1.41 -1.0 +-0.5579999999999999 -1.67 -1.0 +0.0598 -1.26 -1.0 +0.51 -1.15 -1.0 +-0.122 -0.0671 1.0 diff --git a/data/datasets/test/integration/exec_df_iris.csv b/data/datasets/test/integration/exec_df_iris.csv new file mode 100644 index 000000000..d26d5a74b --- /dev/null +++ b/data/datasets/test/integration/exec_df_iris.csv @@ -0,0 +1,16 @@ +sepal-length sepal-width petal-length petal-width class +6.7 3.0 5.2 2.3 2 +6.0 2.2 5.0 1.5 2 +6.2 2.8 4.8 1.8 2 +7.7 3.8 6.7 2.2 2 +7.2 3.0 5.8 1.6 2 +5.5 2.4 3.8 1.1 1 +6.0 2.7 5.1 1.6 1 +5.5 2.5 4.0 1.3 1 +5.6 2.9 3.6 1.3 1 +5.7 2.9 4.2 1.3 1 +5.0 3.2 1.2 0.2 0 +4.9 3.1 1.5 0.1 0 +5.3 3.7 1.5 0.2 0 +4.8 3.1 1.6 0.2 0 +5.0 3.3 1.4 0.2 0 \ No newline at end of file diff --git a/tests/integration/jest/machineApi.test.ts b/tests/integration/jest/machineApi.test.ts index f438563bd..453c8808f 100644 --- a/tests/integration/jest/machineApi.test.ts +++ b/tests/integration/jest/machineApi.test.ts @@ -7,6 +7,8 @@ import * as machineApi from './machineApi'; import * as labApi from './labApi'; import * as dbBuilder from "./util/db"; import * as util from "./util/testUtils"; +import FormData = require('form-data'); +const fs = require('fs'); const db = dbBuilder.openDbConnection(); @@ -83,18 +85,185 @@ describe('machine', () => { expect(labCodeRun.status).toEqual('completed') expect(labCodeRun._id).toBeTruthy() expect(labCodeRun.result).toEqual('hello world\n') + expect(labCodeRun.files).toEqual([]) }); - // I'll need a specific dataset id for this test to work. - // it('Test code run API endpoint recognizes df.', async () => { - // var labCodeRun = await labApi.postCodeExecutions({ src_code: "print(df.head(1))", dataset_id: }) - // expect(labCodeRun.status).toBeTruthy() - // expect(labCodeRun.status).toEqual('completed') - // expect(labCodeRun._id).toBeTruthy() - // expect(labCodeRun.result).toBeTruthy() - // }); + it('Test the code run API handles generated files correctly.', async () => { + var labCodeRun = await labApi.postCodeExecutions({ + src_code: "with open('test.txt', 'w') as file:\n file.write('hello world')\nwith open('test2.txt', 'w') as file:\n file.write('hello world 2')" + }) + expect(labCodeRun.status).toBeTruthy() + expect(labCodeRun.status).toEqual('completed') + expect(labCodeRun._id).toBeTruthy() + expect(labCodeRun.result).toEqual('') + expect(labCodeRun.files[0]).toHaveProperty("_id") + expect(labCodeRun.files[0]).toHaveProperty("filename") + expect(labCodeRun.files[1]).toHaveProperty("_id") + expect(labCodeRun.files[1]).toHaveProperty("filename") + + }) + + it('Test code run API endpoint recognizes df.', async () => { + // upload a test dataset + let filename = 'exec_df_iris.csv' + let filepath = `${util.DATASET_PATH}/${filename}` + let form = new FormData(); + let metadata = JSON.stringify({ + 'name': filename, + 'username': 'testuser', + 'timestamp': Date.now(), + 'dependent_col': 'class', + 'prediction_type': 'classification', + 'categorical_features': [], + 'ordinal_features': [] + }) + + console.log(metadata) + form.append('_metadata', metadata) + form.append('_files', fs.createReadStream(filepath)); + + console.log('form:', form); + + let result; + + try { + result = await labApi.putDataset(form); + console.log('result:'); + console.log(result); + } catch (e) { + var json = await e.response.json() + expect(json.error).toBeUndefined() + expect(e).toBeUndefined() + } + + expect(result).toHaveProperty('dataset_id'); + + var dataset_id = result.dataset_id; + + var labCodeRun = await labApi.postCodeExecutions({ + src_code: "print(df.head(1))", + dataset_id: dataset_id + }); + + expect(labCodeRun.status).toBeTruthy() + expect(labCodeRun.status).toEqual('completed') + expect(labCodeRun._id).toBeTruthy() + expect(labCodeRun.result).toBeTruthy() + expect(labCodeRun._dataset_id).toEqual(dataset_id) + }); + + it('Test code run API endpoint recognizes model.', async () => { + + jest.setTimeout(util.JEST_TIMEOUT*10) + + // upload a test dataset + let filename = 'bananamodel.csv' + let filepath = `${util.DATASET_PATH}/${filename}` + let form = new FormData(); + let metadata = JSON.stringify({ + 'name': filename, + 'username': 'testuser', + 'timestamp': Date.now(), + 'dependent_col': 'class', + 'prediction_type': 'classification', + 'categorical_features': [], + 'ordinal_features': [] + }) + + console.log(metadata) + form.append('_metadata', metadata) + form.append('_files', fs.createReadStream(filepath)); + + console.log('form:', form); + + let result; + + try { + result = await labApi.putDataset(form); + console.log('result:'); + console.log(result); + } catch (e) { + var json = await e.response.json() + expect(json.error).toBeUndefined() + expect(e).toBeUndefined() + } + + expect(result).toHaveProperty('dataset_id'); + + let dataset_id = result.dataset_id; - // it('Test code run API endpoint recognizes model') + let algoName = 'LogisticRegression' + let algoParams = { + "penalty": "l1", + "C": 1.0, + "dual": false, + "dataset": dataset_id + }; + + // get algorithms + var algorithms = await labApi.fetchAlgorithms(); + expect(algorithms.length).toBeGreaterThanOrEqual(util.MIN_EXPECTED_LAB_ALGO_COUNT); + var algoId = algorithms.find(function(element) { return element.name == algoName; })._id; + expect(algoId).toBeTruthy(); + + // submit a simple experiment + try { + var submitResult = await labApi.submitExperiment(algoId, algoParams); + } catch (e) { + console.log("submit experiment exception") + var json = await e.response.json(); + expect(json).toBeFalsy(); + expect(e).toBeFalsy(); + } + + expect(submitResult).toBeTruthy(); + + // expect that the experiment started running + var experimentResult = await labApi.fetchExperiment(submitResult._id) + //console.log("experimentResults: ", experimentResults) + expect(experimentResult._status).toBeTruthy() + expect(experimentResult._status).toEqual('running') + expect(experimentResult._prediction_type).toEqual('classification') + + // wait for the experiment to finish running, probably a better way to do this then delay... + var count = 0 + console.log("starting timeout...") + // while (experimentResult._status === ('running') && count < 10) { + while (experimentResult._status === ('running') && count < 30) { + util.delay(10000) + count = count + 1 + experimentResult = await labApi.fetchExperiment(experimentResult._id) + console.log("experimentResult._status, count (" + count + "): ", experimentResult._status) + } + console.log("finished timeout...") + + // check that the expected results are there + //console.log("experimentResult: ", experimentResult) + expect(experimentResult._status).toBeTruthy() + expect(experimentResult._status).toEqual('success') + + // check that the experiment results are available + var model = await labApi.fetchExperimentModel(experimentResult._id) + // example model value: + // {"_id": "65026b6456a434004af258e4", "filename": "model_65026b5f56a434004af258e1.pkl", "mimetype": "application/octet-stream", "timestamp": 1694657380209} + expect(model).toBeTruthy() + + var labCodeRun = await labApi.postCodeExecutions({ + src_code: "print(model)", + dataset_id: dataset_id, + experiment_id: experimentResult._id, + }); + + console.log('code execution model result:'); + console.log(labCodeRun); + + expect(labCodeRun.status).toBeTruthy(); + expect(labCodeRun.status).toEqual('completed'); + expect(labCodeRun._id).toBeTruthy(); + expect(labCodeRun.result).toBeTruthy(); + expect(labCodeRun._dataset_id).toEqual(dataset_id); + expect(labCodeRun.result).toMatch(new RegExp(`^${algoName}?`)) + }); it('Test the package install API endpoint with good package.', async () => { var labCodeInstall = await labApi.postPackageInstall({ command: 'install', packages: ['numpy'] }) From c37369bc6068a280d2e0fd0e100242c6a7b96fca Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Thu, 14 Sep 2023 15:06:13 -0700 Subject: [PATCH 10/11] Add machine node_modules to unit_tester --- tests/unit/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/Dockerfile b/tests/unit/Dockerfile index 7fea98b8b..033ffa6e1 100644 --- a/tests/unit/Dockerfile +++ b/tests/unit/Dockerfile @@ -71,3 +71,9 @@ WORKDIR /appsrc/lab/webapp COPY lab/webapp/package.json /appsrc/lab/webapp/ RUN dos2unix /appsrc/lab/webapp/package.json RUN npm install --silent --progress=false + +# install lab/machine/node_modules to an anon volume +WORKDIR /appsrc/machine +COPY machine/package.json /appsrc/machine/ +RUN dos2unix /appsrc/machine/package.json +RUN npm install --silent --progress=false From 94b5f01742ae7907e70ebc1b3e250d9a724789ff Mon Sep 17 00:00:00 2001 From: jay-m-dev Date: Thu, 14 Sep 2023 15:28:13 -0700 Subject: [PATCH 11/11] Add machine node_modules as volume on unit tester --- docker-compose-unit-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-unit-test.yml b/docker-compose-unit-test.yml index ef289afd3..10880ab2a 100644 --- a/docker-compose-unit-test.yml +++ b/docker-compose-unit-test.yml @@ -16,5 +16,6 @@ services: - "./:/appsrc" - "/appsrc/lab/webapp/node_modules" - "/appsrc/lab/node_modules" + - "/appsrc/machine/node_modules"