From 360199b84360df976ca9c9630d93a1018bb1e40d Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" <mdryden@chem.utoronto.ca> Date: Fri, 5 Jun 2015 10:44:26 -0400 Subject: [PATCH] Repackage for Microdrop v1.0 --- SConstruct | 46 ------ __init__.py | 172 ++++++++++++++++++++++ hooks/Linux/on_plugin_install.sh | 3 + hooks/Windows/on_plugin_install.bat | 1 + hooks/Windows/on_plugin_install.exe | Bin 0 -> 48640 bytes microdrop/__init__.py | 172 ---------------------- on_plugin_install.py | 12 ++ release.py | 22 +++ requirements.txt | 1 + site_scons/__init__.py | 0 site_scons/git_util.py | 93 ------------ site_scons/site_tools/disttar/__init__.py | 1 - site_scons/site_tools/disttar/disttar.py | 153 ------------------- 13 files changed, 211 insertions(+), 465 deletions(-) delete mode 100644 SConstruct create mode 100644 hooks/Linux/on_plugin_install.sh create mode 100644 hooks/Windows/on_plugin_install.bat create mode 100644 hooks/Windows/on_plugin_install.exe delete mode 100644 microdrop/__init__.py create mode 100644 on_plugin_install.py create mode 100755 release.py create mode 100644 requirements.txt delete mode 100644 site_scons/__init__.py delete mode 100644 site_scons/git_util.py delete mode 100644 site_scons/site_tools/disttar/__init__.py delete mode 100644 site_scons/site_tools/disttar/disttar.py diff --git a/SConstruct b/SConstruct deleted file mode 100644 index aa3fd85..0000000 --- a/SConstruct +++ /dev/null @@ -1,46 +0,0 @@ -import re -import os -import sys - -import yaml - -from git_util import GitUtil -from path_helpers import path - - -def get_plugin_version(): - version = GitUtil(None).describe() - m = re.search('^v(?P<major>\d+)\.(?P<minor>\d+)(-(?P<micro>\d+))?', version) - if m.group('micro'): - micro = m.group('micro') - else: - micro = '0' - version_string = "%s.%s.%s" % (m.group('major'), - m.group('minor'), micro) - return version_string - - -SOFTWARE_VERSION = get_plugin_version() -env = Environment(tools = ["default", "disttar"], - DISTTAR_EXCLUDEDIRS=['.git'], - DISTTAR_EXCLUDERES=[r'\.sconsign\.dblite'], - DISTTAR_EXCLUDEEXTS=['.gz', '.pyc', '.tgz', '.swp']) - -plugin_root = path('.').abspath() -properties_target = plugin_root.joinpath('properties.yml') -properties = {'plugin_name': 'wheeler.%s' % plugin_root.name, - 'package_name': str(plugin_root.name), - 'version': SOFTWARE_VERSION} -properties_target.write_bytes(yaml.dump(properties)) -archive_name = '%s-%s.tar.gz' % (properties['package_name'], SOFTWARE_VERSION) - -# This will build an archive using what ever DISTTAR_FORMAT that is set. -tar = env.DistTar('%s' % properties['package_name'], [env.Dir('#')]) -renamed_tar = env.Command(env.File(archive_name), None, - Move(archive_name, tar[0])) -Depends(renamed_tar, tar) -Clean(renamed_tar, tar) - -if 'PLUGIN_ARCHIVE_DIR' in os.environ: - target_archive_dir = os.environ['PLUGIN_ARCHIVE_DIR'] - Install(target_archive_dir, renamed_tar) diff --git a/__init__.py b/__init__.py index e69de29..9c97ea6 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,172 @@ +""" +Copyright 2011 Ryan Fobel + +This file is part of dmf_control_board. + +dmf_control_board is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +dmf_control_board is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with dmf_control_board. If not, see <http://www.gnu.org/licenses/>. +""" +from collections import OrderedDict +from datetime import datetime + +import gtk +import zmq +from flatland import String, Boolean, Float, Form +from logger import logger +from gui.protocol_grid_controller import ProtocolGridController +from plugin_helpers import (AppDataController, StepOptionsController, + get_plugin_info) +from plugin_manager import (IPlugin, IWaveformGenerator, Plugin, implements, + PluginGlobals, ScheduleRequest, emit_signal, + get_service_instance) +from app_context import get_app +from path_helpers import path + + +PluginGlobals.push_env('microdrop.managed') + +class ZeroMQServicePlugin(Plugin, AppDataController, StepOptionsController): + """ + This class is automatically registered with the PluginManager. + """ + implements(IPlugin) + version = get_plugin_info(path(__file__).parent.parent).version + plugins_name = get_plugin_info(path(__file__).parent.parent).plugin_name + + ''' + AppFields + --------- + + A flatland Form specifying application options for the current plugin. + Note that nested Form objects are not supported. + + Since we subclassed AppDataController, an API is available to access and + modify these attributes. This API also provides some nice features + automatically: + -all fields listed here will be included in the app options dialog + (unless properties=dict(show_in_gui=False) is used) + -the values of these fields will be stored persistently in the microdrop + config file, in a section named after this plugin's name attribute + ''' + AppFields = Form.of( + String.named('service_address').using(default='', optional=True), + ) + + ''' + StepFields + --------- + + A flatland Form specifying the per step options for the current plugin. + Note that nested Form objects are not supported. + + Since we subclassed StepOptionsController, an API is available to access and + modify these attributes. This API also provides some nice features + automatically: + -all fields listed here will be included in the protocol grid view + (unless properties=dict(show_in_gui=False) is used) + -the values of these fields will be stored persistently for each step + ''' + StepFields = Form.of( + Boolean.named('service_enabled').using(default=False, optional=True), + ) + + def __init__(self): + self.name = self.plugins_name + self.context = zmq.Context.instance() + self.socks = OrderedDict() + self.timeout_id = None + self._start_time = None + + def on_plugin_enable(self): + # We need to call AppDataController's on_plugin_enable() to update the + # application options data. + AppDataController.on_plugin_enable(self) + self.context = zmq.Context() + self.reset_socks() + if get_app().protocol: + pgc = get_service_instance(ProtocolGridController, env='microdrop') + pgc.update_grid() + + def close_socks(self): + # Close any currently open sockets. + for name, sock in self.socks.iteritems(): + sock.close() + self.socks = OrderedDict() + + def reset_socks(self): + self.close_socks() + app_values = self.get_app_values() + if self.timeout_id is not None: + gtk.timeout_remove(self.timeout_id) + self.timeout_id = None + if app_values['service_address']: + # Service address is available + self.socks['req'] = zmq.Socket(self.context, zmq.REQ) + self.socks['req'].connect(app_values['service_address']) + + def on_app_options_changed(self, plugin_name): + if plugin_name == self.name: + self.reset_socks() + + def on_plugin_disable(self): + self.close_socks() + if get_app().protocol: + pgc = get_service_instance(ProtocolGridController, env='microdrop') + pgc.update_grid() + + def step_complete(self, return_value=None): + app = get_app() + if app.running or app.realtime_mode: + emit_signal('on_step_complete', [self.name, return_value]) + + def on_step_run(self): + options = self.get_step_options() + self.reset_socks() + if options['service_enabled'] and self.socks['req'] is None: + # Service is supposed to be called for this step, but the socket is + # not ready. + self.step_complete(return_value='Fail') + elif options['service_enabled'] and self.socks['req'] is not None: + logger.info('[ZeroMQServicePlugin] Send signal to service to ' + 'start.') + # Request start of service. + self.socks['req'].send('start') + if not self.socks['req'].poll(timeout=4000): + self.reset_socks() + logger.error('[ZeroMQServicePlugin] Timed-out waiting for ' + 'a response.') + else: + # Response is ready. + response = self.socks['req'].recv() + if response == 'started': + logger.info('[ZeroMQServicePlugin] Service started ' + 'successfully.') + self.socks['req'].send('notify_completion') + + response = self.socks['req'].recv() + logger.info('[ZeroMQServicePlugin] Service response: %s', response) + if response == 'completed': + logger.info('[ZeroMQServicePlugin] Service completed task ' + 'successfully.') + self.step_complete() + else: + logger.error('[ZeroMQServicePlugin] Unexpected response: %s' % + response) + self.step_complete(return_value='Fail') + else: + self.step_complete() + + def enable_service(self): + pass + +PluginGlobals.pop_env() diff --git a/hooks/Linux/on_plugin_install.sh b/hooks/Linux/on_plugin_install.sh new file mode 100644 index 0000000..19bf666 --- /dev/null +++ b/hooks/Linux/on_plugin_install.sh @@ -0,0 +1,3 @@ +#!/bin/sh +PYTHON_EXE=$1 +$PYTHON_EXE ../../on_plugin_install.py diff --git a/hooks/Windows/on_plugin_install.bat b/hooks/Windows/on_plugin_install.bat new file mode 100644 index 0000000..617a828 --- /dev/null +++ b/hooks/Windows/on_plugin_install.bat @@ -0,0 +1 @@ +%1 ..\..\on_plugin_install.py diff --git a/hooks/Windows/on_plugin_install.exe b/hooks/Windows/on_plugin_install.exe new file mode 100644 index 0000000000000000000000000000000000000000..2e6aa4f40e0ba6fabfae628dd3976df1f76b4eb7 GIT binary patch literal 48640 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 literal 0 HcmV?d00001 diff --git a/microdrop/__init__.py b/microdrop/__init__.py deleted file mode 100644 index 9c97ea6..0000000 --- a/microdrop/__init__.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Copyright 2011 Ryan Fobel - -This file is part of dmf_control_board. - -dmf_control_board is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -dmf_control_board is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with dmf_control_board. If not, see <http://www.gnu.org/licenses/>. -""" -from collections import OrderedDict -from datetime import datetime - -import gtk -import zmq -from flatland import String, Boolean, Float, Form -from logger import logger -from gui.protocol_grid_controller import ProtocolGridController -from plugin_helpers import (AppDataController, StepOptionsController, - get_plugin_info) -from plugin_manager import (IPlugin, IWaveformGenerator, Plugin, implements, - PluginGlobals, ScheduleRequest, emit_signal, - get_service_instance) -from app_context import get_app -from path_helpers import path - - -PluginGlobals.push_env('microdrop.managed') - -class ZeroMQServicePlugin(Plugin, AppDataController, StepOptionsController): - """ - This class is automatically registered with the PluginManager. - """ - implements(IPlugin) - version = get_plugin_info(path(__file__).parent.parent).version - plugins_name = get_plugin_info(path(__file__).parent.parent).plugin_name - - ''' - AppFields - --------- - - A flatland Form specifying application options for the current plugin. - Note that nested Form objects are not supported. - - Since we subclassed AppDataController, an API is available to access and - modify these attributes. This API also provides some nice features - automatically: - -all fields listed here will be included in the app options dialog - (unless properties=dict(show_in_gui=False) is used) - -the values of these fields will be stored persistently in the microdrop - config file, in a section named after this plugin's name attribute - ''' - AppFields = Form.of( - String.named('service_address').using(default='', optional=True), - ) - - ''' - StepFields - --------- - - A flatland Form specifying the per step options for the current plugin. - Note that nested Form objects are not supported. - - Since we subclassed StepOptionsController, an API is available to access and - modify these attributes. This API also provides some nice features - automatically: - -all fields listed here will be included in the protocol grid view - (unless properties=dict(show_in_gui=False) is used) - -the values of these fields will be stored persistently for each step - ''' - StepFields = Form.of( - Boolean.named('service_enabled').using(default=False, optional=True), - ) - - def __init__(self): - self.name = self.plugins_name - self.context = zmq.Context.instance() - self.socks = OrderedDict() - self.timeout_id = None - self._start_time = None - - def on_plugin_enable(self): - # We need to call AppDataController's on_plugin_enable() to update the - # application options data. - AppDataController.on_plugin_enable(self) - self.context = zmq.Context() - self.reset_socks() - if get_app().protocol: - pgc = get_service_instance(ProtocolGridController, env='microdrop') - pgc.update_grid() - - def close_socks(self): - # Close any currently open sockets. - for name, sock in self.socks.iteritems(): - sock.close() - self.socks = OrderedDict() - - def reset_socks(self): - self.close_socks() - app_values = self.get_app_values() - if self.timeout_id is not None: - gtk.timeout_remove(self.timeout_id) - self.timeout_id = None - if app_values['service_address']: - # Service address is available - self.socks['req'] = zmq.Socket(self.context, zmq.REQ) - self.socks['req'].connect(app_values['service_address']) - - def on_app_options_changed(self, plugin_name): - if plugin_name == self.name: - self.reset_socks() - - def on_plugin_disable(self): - self.close_socks() - if get_app().protocol: - pgc = get_service_instance(ProtocolGridController, env='microdrop') - pgc.update_grid() - - def step_complete(self, return_value=None): - app = get_app() - if app.running or app.realtime_mode: - emit_signal('on_step_complete', [self.name, return_value]) - - def on_step_run(self): - options = self.get_step_options() - self.reset_socks() - if options['service_enabled'] and self.socks['req'] is None: - # Service is supposed to be called for this step, but the socket is - # not ready. - self.step_complete(return_value='Fail') - elif options['service_enabled'] and self.socks['req'] is not None: - logger.info('[ZeroMQServicePlugin] Send signal to service to ' - 'start.') - # Request start of service. - self.socks['req'].send('start') - if not self.socks['req'].poll(timeout=4000): - self.reset_socks() - logger.error('[ZeroMQServicePlugin] Timed-out waiting for ' - 'a response.') - else: - # Response is ready. - response = self.socks['req'].recv() - if response == 'started': - logger.info('[ZeroMQServicePlugin] Service started ' - 'successfully.') - self.socks['req'].send('notify_completion') - - response = self.socks['req'].recv() - logger.info('[ZeroMQServicePlugin] Service response: %s', response) - if response == 'completed': - logger.info('[ZeroMQServicePlugin] Service completed task ' - 'successfully.') - self.step_complete() - else: - logger.error('[ZeroMQServicePlugin] Unexpected response: %s' % - response) - self.step_complete(return_value='Fail') - else: - self.step_complete() - - def enable_service(self): - pass - -PluginGlobals.pop_env() diff --git a/on_plugin_install.py b/on_plugin_install.py new file mode 100644 index 0000000..80fb487 --- /dev/null +++ b/on_plugin_install.py @@ -0,0 +1,12 @@ +from datetime import datetime +import logging + +from path_helpers import path +from pip_helpers import install + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + logging.info(str(datetime.now())) + requirements = path(__file__).parent.joinpath('requirements.txt').abspath() + logging.info(install(['-U', '-r', requirements])) diff --git a/release.py b/release.py new file mode 100755 index 0000000..47788c4 --- /dev/null +++ b/release.py @@ -0,0 +1,22 @@ +import tarfile +import yaml + +from microdrop_utility import Version + +package_name = 'dstat_zeromq_plugin' +plugin_name = 'wheeler.dstat_zeromq_plugin' + +# create a version sting based on the git revision/branch +version = str(Version.from_git_repository()) + +# write the 'properties.yml' file +properties = {'plugin_name': plugin_name, 'package_name': package_name, + 'version': version} +with open('properties.yml', 'w') as f: + f.write(yaml.dump(properties)) + +# create the tar.gz plugin archive +with tarfile.open("%s-%s.tar.gz" % (package_name, version), "w:gz") as tar: + for name in ['__init__.py', 'test_service.py', 'properties.yml', + 'requirements.txt']: + tar.add(name) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..02ec117 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyzmq diff --git a/site_scons/__init__.py b/site_scons/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/site_scons/git_util.py b/site_scons/git_util.py deleted file mode 100644 index 6fecf25..0000000 --- a/site_scons/git_util.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -from subprocess import Popen, PIPE, check_call, CalledProcessError -import re - -from path_helpers import path - - -class GitError(Exception): - pass - - -class GitUtil(object): - def __init__(self, root_path='.'): - self.root_path = path(root_path) - if root_path is None: - dir_node = path(os.getcwd()) - while not dir_node.dirs('.git') and dir_node: - dir_node = dir_node.parent - if not dir_node: - raise GitError('No git root found.') - self.root_path = dir_node - self._git = None - assert(self.root_path.dirs('.git')) - - - @property - def git(self): - if self._git: - return self._git - - cmds = ['git'] - if os.name == 'nt': - exceptions = (WindowsError,) - cmds += ['git.cmd'] - else: - exceptions = (OSError,) - - valid_cmd = False - for cmd in cmds: - try: - check_call([cmd], stdout=PIPE, stderr=PIPE) - except exceptions: - # The command was not found, try the next one - pass - except CalledProcessError: - valid_cmd = True - break - if not valid_cmd: - raise GitError, 'No valid git command found' - self._git = cmd - return self._git - - - def command(self, x): - try: - x.__iter__ - except: - x = re.split(r'\s+', x) - cwd = os.getcwd() - - os.chdir(self.root_path) - cmd = [self.git] + x - stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE).communicate() - os.chdir(cwd) - - if stderr: - raise GitError('Error executing git %s' % x) - return stdout.strip() - - - def describe(self): - return self.command('describe') - - - def summary(self, color=False): - if color: - format_ = '''--pretty=format:%Cred%h%Creset - %s %Cgreen(%cr)%Creset''' - else: - format_ = '''--pretty=format:%h - %s (%cr)''' - return self.command(['''log''', '''--graph''', format_, - '''--abbrev-commit''', '''--date=relative''']) - - - def rev_parse(self, ref='HEAD'): - return self.command(['rev-parse', ref]) - - - def show(self, ref='HEAD', color=False, extra_args=None): - extra_args = [extra_args, []][extra_args is None] - args = ['show', ref] - if color: - args += ['--color'] - return self.command(args + extra_args) diff --git a/site_scons/site_tools/disttar/__init__.py b/site_scons/site_tools/disttar/__init__.py deleted file mode 100644 index 4b0e0f0..0000000 --- a/site_scons/site_tools/disttar/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from disttar import * diff --git a/site_scons/site_tools/disttar/disttar.py b/site_scons/site_tools/disttar/disttar.py deleted file mode 100644 index 7f7a13e..0000000 --- a/site_scons/site_tools/disttar/disttar.py +++ /dev/null @@ -1,153 +0,0 @@ -# DistTarBuilder: tool to generate tar files using SCons -# Copyright (C) 2005, 2006 Matthew A. Nicholson -# Copyright (C) 2006-2010 John Pye -# -# This file is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 2.1 as published by the Free Software Foundation. -# -# This file is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import os,sys -from SCons.Script import * -import re - - -def file_handler(fpath, source, excluderes, excludeexts): - from path import path - - fpath = path(fpath) - if not fpath.ext in excludeexts: - failre = False - for r in excluderes: - #print "Match( %s against %s)" % (r,relpath) - if r.search(fpath): - failre = True - #print "Excluding '%s' from tarball" % fpath - break - if not failre: - #print "Adding source",fpath - source.append(str(fpath)) - - -def disttar_emitter(target, source, env): - source,origsource = [], source - - excludeexts = env.Dictionary().get('DISTTAR_EXCLUDEEXTS',[]) - excludedirs = env.Dictionary().get('DISTTAR_EXCLUDEDIRS',[]) - re1 = env.Dictionary().get('DISTTAR_EXCLUDERES',[]) - excluderes = [re.compile(r) for r in re1] - - # assume the sources are directories... need to check that - for item in origsource: - if os.path.isfile(str(item)): - file_handler(item, source, excluderes, excludeexts) - else: - for root, dirs, files in os.walk(str(item)): - # don't make directory dependences as that triggers full build - # of that directory - if root in source: - #print "Removing directory %s" % root - source.remove(root) - - # loop through files in a directory - for name in files: - relpath = os.path.join(root,name) - file_handler(relpath, source, excluderes, excludeexts) - - for d in excludedirs: - if d in dirs: - dirs.remove(d) # don't visit CVS directories etc - - return target, source - -def disttar_string(target, source, env): - """This is what gets printed on the console. We'll strip out the list - or source files, since it tends to get very long. If you want to see the - contents, the easiest way is to uncomment the line 'Adding to TAR file' - below. """ - return 'DistTar(%s,...)' % target[0] - -def disttar(target, source, env): - """tar archive builder""" - - import tarfile - - env_dict = env.Dictionary() - - if env_dict.get("DISTTAR_FORMAT") in ["gz", "bz2"]: - tar_format = env_dict["DISTTAR_FORMAT"] - else: - tar_format = "" - - # split the target directory, filename, and stuffix - base_name = str(target[0]).split('.tar')[0] - (target_dir, dir_name) = os.path.split(base_name) - - # create the target directory if it does not exist - if target_dir and not os.path.exists(target_dir): - os.makedirs(target_dir) - - # open our tar file for writing - print >> sys.stderr, 'DistTar: Writing %s' % str(target[0]) - print >> sys.stderr, ' with contents: %s' % [str(s) for s in source] - tar = tarfile.open(str(target[0]), "w:%s" % tar_format) - - # write sources to our tar file - for item in source: - item = str(item) - sys.stderr.write(".") - #print "Adding to TAR file: %s/%s" % (dir_name,item) - tar.add(item,'%s/%s' % (dir_name,item)) - - # all done - sys.stderr.write("\n") #print "Closing TAR file" - tar.close() - -def disttar_suffix(env, sources): - """tar archive suffix generator""" - - env_dict = env.Dictionary() - if env_dict.has_key("DISTTAR_FORMAT") and env_dict["DISTTAR_FORMAT"] in ["gz", "bz2"]: - return ".tar." + env_dict["DISTTAR_FORMAT"] - else: - return ".tar" - -def generate(env): - """ - Add builders and construction variables for the DistTar builder. - """ - - disttar_action=SCons.Action.Action(disttar, disttar_string) - env['BUILDERS']['DistTar'] = Builder( - action=disttar_action - , emitter=disttar_emitter - , suffix = disttar_suffix - , target_factory = env.fs.Entry - ) - - env.AppendUnique( - DISTTAR_FORMAT = 'gz' - ) - -def exists(env): - """ - Make sure this tool exists. - """ - try: - import os - import tarfile - except ImportError: - return False - else: - return True - -# vim:set ts=4 sw=4 noexpandtab: -- GitLab