Skip to content

Commit

Permalink
Add RecommendNextComponentsMock plugin. Closes #63 (#65)
Browse files Browse the repository at this point in the history
* Add Components Recommender mock plugin. Closes #63

* WIP- Refactor PythonPluginBase and script to common

* WIP- Fix eslint issues

* WIP- Add PythonPluginBase to README

* WIP- Fix test case description in mock plugin

* WIP- Fix typo and add missing type annotations in python plugin files

* Fix run_python_plugin.py in notes
  • Loading branch information
umesh-timalsina authored Feb 24, 2021
1 parent d2afe9a commit 12643ea
Show file tree
Hide file tree
Showing 13 changed files with 937 additions and 783 deletions.
542 changes: 542 additions & 0 deletions src/common/plugins/CircuitAnalysisBases.py

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions src/common/plugins/PythonPluginBase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*globals define*/

define([
'plugin/PluginBase',
'module'
], function (
PluginBase,
module
) {
const START_PORT = 5555;
const COMMAND = 'python';
const path = require('path');
const SCRIPT_FILE = path.join(path.dirname(module.uri), 'run_python_plugin.py');

class PythonPluginBase extends PluginBase {
constructor(pluginMetadata) {
super();
this.pluginMetadata = pluginMetadata;
}

async main(callback) {
const CoreZMQ = require('webgme-bindings').CoreZMQ;

// due to the limited options on the script return values, we need this hack
this.result.setSuccess(null);

const corezmq = new CoreZMQ(
this.project,
this.core,
this.logger,
{ port: START_PORT, plugin: this}
);

const port = await corezmq.startServer();

this.logger.info(`zmq-server listening at port ${port}`);

try {
await this.callScript(COMMAND, SCRIPT_FILE, port);
await corezmq.stopServer();
callback(null, this.result);
} catch (err) {
this.logger.error(err.stack);
corezmq.stopServer()
.finally(() => {
// Result success is false at invocation.
callback(err, this.result);
});
}
}

async callScript(program, scriptPath, port) {
const cp = require('child_process');
let options = {},
args = [
scriptPath,
this.getId(),
port,
`"${this.commitHash}"`,
`"${this.branchName}"`,
`"${this.core.getPath(this.activeNode)}"`,
`"${this.activeSelection.map(node => this.core.getPath(node)).join(',')}"`,
`"${this.namespace}"`,
];

const childProc = cp.spawn(program, args, options);

return new Promise((resolve, reject) => {
childProc.stdout.on('data', data => {
this.logger.info(data.toString());
});

childProc.stderr.on('data', data => {
this.logger.error(data.toString());
});

childProc.on('close', (code) => {
if (code > 0) {
// This means an execution error or crash, so we are failing the plugin
reject(new Error(`${program} ${args.join(' ')} exited with code ${code}.`));
this.result.setSuccess(false);
} else {
if(this.result.getSuccess() === null) {
// The result have not been set inside the python, but it suceeded, so we go with the true value
this.result.setSuccess(true);
}
resolve();
}
});

childProc.on('error', (err) => {
// This is a hard execution error, like the child process cannot be instantiated...
this.logger.error(err);
this.result.setSuccess(false);
reject(err);
});
});
}
}

return PythonPluginBase;
});
14 changes: 13 additions & 1 deletion src/common/plugins/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This file contains documentation about the contained shared modules.
This file contains documentation about the contained shared plugins.

## JSONImporter (uses changeset.js)
Written by Brian Broll for [`deepforge-keras`](https://github.com/deepforge-dev/deepforge-keras).
Expand Down Expand Up @@ -62,3 +62,15 @@ In the above JSON, `id` can be one of the following:
The rest of the fields correspond to the similarly named concepts in WebGME. For more information, check out https://webgme.readthedocs.io/en/latest/meta_modeling/meta_modeling_concepts.html (or the [source docs](https://editor.webgme.org/docs/source/Core.html) for usage with the Core).

Example usage for this utility can be found in the [tests](/test/common/plugins/JSONImporter.spec.js) and in the [CreateKerasMeta](/src/plugins/CreateKerasMeta/CreateKerasMeta.js) plugin.

## CircuitAnalysisBases
The base implementations for all circuit analysis/conversion Plugins. This module implements a conversion from a Circuit(WebGME) to a Circuit(PySpice) and a base class for running analysis on the Circuit.

Currently these implementations are used by the following plugins:

1. [ConvertCircuitToNetlist](../../plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist/__init__.py): Converts a WebGME Circuit to its equivalent SPICE Netlist (uses CircuitToPySpiceBase)
2. [RecommendNextComponentsMock](../../plugins/RecommendNextComponentsMock/RecommendNextComponentsMock/__init__.py): A mock implementation for recommending components to be added to the Circuit (uses AnalyzeCircuit)


## PythonPluginBase
`PluginBase` for Python plugins, which uses [run_python_plugin.py](./run_python_plugin.py) to discover and execute the Python script for the plugin.
91 changes: 91 additions & 0 deletions src/common/plugins/run_python_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
This script is called by the plugin-wrapper, PythonPluginBase.js, which passes down the
plugin context via arguments. These can be modified to include more information if needed.
Notes:
- The current working directory when called from a plugin is the root of your webgme repo.
- At the point of invocation of this plugin - it is assumed that a coreZMQ-server is running at 127.0.0.1:PORT.
"""
import json
import logging
import sys
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path

from webgme_bindings import WebGME

WEBGME_SETUP = Path(f"{__file__}/../../../../webgme-setup.json").resolve()
IMPORT_MODULE_NAME = "electric_circuits.plugins"


def _import_python_class(plugin_file: Path, class_name: str) -> type:
spec = spec_from_file_location(IMPORT_MODULE_NAME, plugin_file)
base_module = module_from_spec(spec)
spec.loader.exec_module(base_module)

return getattr(base_module, class_name)


def get_python_plugin_classes() -> dict:
plugin_classes = {}
with open(WEBGME_SETUP, "r") as webgme_setup:
plugins = json.load(webgme_setup)["components"]["plugins"]

for plugin_name in plugins:
plugin_file = Path(
f"{__file__}/../../../../{plugins[plugin_name]['src']}/{plugin_name}/__init__.py"
).resolve()

if plugin_file.exists():
plugin_classes[plugin_name] = _import_python_class(
plugin_file, plugin_name
)

return plugin_classes


PLUGINS = get_python_plugin_classes()

if len(PLUGINS) == 0:
raise Exception("No Python Plugins available in the current deployment")

logger = logging.getLogger(sys.argv[1])


logger.info("sys.args: {0}".format(sys.argv))

PORT = sys.argv[2]
COMMIT_HASH = sys.argv[3].strip('"')
BRANCH_NAME = sys.argv[4].strip('"')
ACTIVE_NODE_PATH = sys.argv[5].strip('"')
ACTIVE_SELECTION_PATHS = []

if sys.argv[6] != '""':
ACTIVE_SELECTION_PATHS = sys.argv[6].strip('"').split(",")
if ACTIVE_SELECTION_PATHS[0] == "":
ACTIVE_SELECTION_PATHS.pop(0)

NAMESPACE = sys.argv[7].strip('"')


logger.debug("commit-hash: {0}".format(COMMIT_HASH))
logger.debug("branch-name: {0}".format(BRANCH_NAME))
logger.debug("active-node-path: {0}".format(ACTIVE_NODE_PATH))
logger.debug("active-selection-paths: {0}".format(ACTIVE_SELECTION_PATHS))
logger.debug("name-space: {0}".format(NAMESPACE))

# Create an instance of WebGME and the plugin
webgme = WebGME(PORT, logger)
plugin = PLUGINS[sys.argv[1]](
webgme,
COMMIT_HASH,
BRANCH_NAME,
ACTIVE_NODE_PATH,
ACTIVE_SELECTION_PATHS,
NAMESPACE,
)

# Do the work
plugin.main()

# Finally disconnect from the zmq-server
webgme.disconnect()
127 changes: 7 additions & 120 deletions src/plugins/ConvertCircuitToNetlist/ConvertCircuitToNetlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,136 +8,23 @@
*/

define([
'q',
'plugin/PluginConfig',
'text!./metadata.json',
'plugin/PluginBase',
'module'
'electric-circuits/plugins/PythonPluginBase',
], function (
Q,
PluginConfig,
pluginMetadata,
PluginBase,
module) {
PluginBase
) {
'use strict';

pluginMetadata = JSON.parse(pluginMetadata);
const path = require('path');
// Modify these as needed..
const START_PORT = 5555;
const COMMAND = 'python';
const SCRIPT_FILE = path.join(path.dirname(module.uri), 'run_plugin.py');

/**
* Initializes a new instance of PythonBindings.
* @class
* @augments {PluginBase}
* @classdesc This class represents the plugin PythonBindings.
* @constructor
*/
function ConvertCircuitToNetlist() {
// Call base class' constructor.
PluginBase.call(this);
this.pluginMetadata = pluginMetadata;
class ConvertCircuitToNetlist extends PluginBase {
constructor() {
super(pluginMetadata);
}
}

/**
* Metadata associated with the plugin. Contains id, name, version, description, icon, configStructue etc.
* This is also available at the instance at this.pluginMetadata.
* @type {object}
*/
ConvertCircuitToNetlist.metadata = pluginMetadata;

// Prototypical inheritance from PluginBase.
ConvertCircuitToNetlist.prototype = Object.create(PluginBase.prototype);
ConvertCircuitToNetlist.prototype.constructor = ConvertCircuitToNetlist;

/**
* Main function for the plugin to execute. This will perform the execution.
* Notes:
* - Always log with the provided logger.[error,warning,info,debug].
* - Do NOT put any user interaction logic UI, etc. inside this method.
* - callback always has to be called even if error happened.
*
* @param {function(null|Error|string, plugin.PluginResult)} callback - the result callback
*/
ConvertCircuitToNetlist.prototype.main = function (callback) {
const CoreZMQ = require('webgme-bindings').CoreZMQ;
const cp = require('child_process');
const logger = this.logger;

// due to the limited options on the script return values, we need this hack
this.result.setSuccess(null);

const callScript = (program, scriptPath, port) => {
let deferred = Q.defer(),
options = {},
args = [
scriptPath,
port,
`"${this.commitHash}"`,
`"${this.branchName}"`,
`"${this.core.getPath(this.activeNode)}"`,
`"${this.activeSelection.map(node => this.core.getPath(node)).join(',')}"`,
`"${this.namespace}"`,
];

const childProc = cp.spawn(program, args, options);

childProc.stdout.on('data', data => {
logger.info(data.toString());
// logger.debug(data.toString());
});

childProc.stderr.on('data', data => {
logger.error(data.toString());
});

childProc.on('close', (code) => {
if (code > 0) {
// This means an execution error or crash, so we are failing the plugin
deferred.reject(new Error(`${program} ${args.join(' ')} exited with code ${code}.`));
this.result.setSuccess(false);
} else {
if(this.result.getSuccess() === null) {
// The result have not been set inside the python, but it suceeded, so we go with the true value
this.result.setSuccess(true);
}
deferred.resolve();
}
});

childProc.on('error', (err) => {
// This is a hard execution error, like the child process cannot be instantiated...
logger.error(err);
this.result.setSuccess(false);
deferred.reject(err);
});

return deferred.promise;
};

const corezmq = new CoreZMQ(this.project, this.core, this.logger, {port: START_PORT, plugin: this});
corezmq.startServer()
.then((port) => {
logger.info(`zmq-server listening at port ${port}`);
return callScript(COMMAND, SCRIPT_FILE, port);
})
.then(() => {
return corezmq.stopServer();
})
.then(() => {
callback(null, this.result);
})
.catch((err) => {
this.logger.error(err.stack);
corezmq.stopServer()
.finally(() => {
// Result success is false at invocation.
callback(err, this.result);
});
});
};

return ConvertCircuitToNetlist;
});
Loading

0 comments on commit 12643ea

Please sign in to comment.