diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/eval.py b/eval.py new file mode 100644 index 0000000..2e9eb10 --- /dev/null +++ b/eval.py @@ -0,0 +1,78 @@ +import ast +import operator + +from utils import typename + + +BIN_OPS = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Mod: operator.mod +} + +UNARY_OPS = { + ast.UAdd: operator.pos, + ast.USub: operator.neg +} + + +def forgiving_eval(value): + try: + return arithmetic_eval(value) + except: + return value + + +def arithmetic_eval(s): + node = ast.parse(s, mode="eval") + return ast_node_eval(node.body) + + +def ast_node_eval(node): + if isinstance(node, ast.Expression): + return ast_node_eval(node.body) + elif isinstance(node, ast.Str): + return node.s + elif isinstance(node, ast.Num): + return node.n + elif isinstance(node, ast.BinOp): + op = get_operator(node, BIN_OPS) + left = ast_node_eval(node.left) + right = ast_node_eval(node.right) + return op(left, right) + elif isinstance(node, ast.UnaryOp): + op = get_operator(node, UNARY_OPS) + operand = ast_node_eval(node.operand) + return op(operand) + else: + tn = typename(node) + raise ArithmeticEvalError(f"Unsupported node type {tn}") + + +def get_operator(node, ops): + op_type = type(node.op) + try: + op = ops[op_type] + except KeyError as e: + nn = typename(node) + on = typename(node.op) + raise ArithmeticEvalError(f"Unsupported {nn} {on}") from e + else: + return op + + +class ArithmeticEvalError(Exception): + pass + + + +#TODO: +#print like SyntaxError: +# "something with an error here" +# ^ +#this needs full string and offset of current node within full string stored + + + diff --git a/fake.py b/fake.py new file mode 100644 index 0000000..bfa1991 --- /dev/null +++ b/fake.py @@ -0,0 +1,41 @@ + +data = { + "TEST0_psen_db": { + "x": None, + "aaa": "aaa", + "bbbbbb": 2, + "c": "cccccc" + }, + + "TEST1_psen_db1": { + "x": None, + "bbbbbb": 2, + "c": "cccccc" + }, + + "TEST2TEST2_psen_db": { + "x": None, + "y": [1,2,3], + "aaa": "aaa", + "c": "cccccc" + }, + + "ignored": {} +} + + +server_info = { + "active_instances": list(data.keys()) +} + + +class PipelineClient: + def __init__(*args, **kwargs): + pass + def get_server_info(self): + return server_info + def get_instance_config(self, name): + return data[name] + + + diff --git a/listentry.py b/listentry.py new file mode 100644 index 0000000..0254299 --- /dev/null +++ b/listentry.py @@ -0,0 +1,34 @@ +import wx +from mathentry import MathEntry + + +class ListEntry(wx.BoxSizer): + + def __init__(self, parent, id=wx.ID_ANY, value=(), style=wx.TE_RIGHT): + super().__init__(wx.HORIZONTAL) + self.parent = parent + self.style = style + self.entries = [] + self.SetValue(value) + + + def SetValue(self, value): + self.entries.clear() + self.Clear(True) + for v in value: + new = MathEntry(self.parent, value=v, style=self.style) + self.entries.append(new) + self.Add(new, flag=wx.EXPAND) + + + def GetValue(self): + return [e.GetValue() for e in self.entries] + + def Disable(self): + return [e.Disable() for e in self.entries] + + def Enable(self): + return [e.Enable() for e in self.entries] + + + diff --git a/mathentry.py b/mathentry.py new file mode 100644 index 0000000..8119ac3 --- /dev/null +++ b/mathentry.py @@ -0,0 +1,87 @@ +import wx +from eval import arithmetic_eval +from utils import typename + + +class MathEntry(wx.TextCtrl): + + def __init__(self, *args, value="", **kwargs): + if "style" in kwargs: + kwargs["style"] |= wx.TE_PROCESS_ENTER + else: + kwargs["style"] = wx.TE_PROCESS_ENTER + + self.value_type = type(value) + value = str(value) + wx.TextCtrl.__init__(self, *args, value=value, **kwargs) + + self._alarm = False + self._last_good_value = self.GetValue() + + self.Bind(wx.EVT_TEXT_ENTER, self.on_enter) + self.Bind(wx.EVT_KEY_UP, self.on_escape) + + + def GetValue(self): + val = super().GetValue() + try: + val = self.value_type(val) + except ValueError: + pass + return val + + + def SetValue(self, val): + val = str(val) + super().SetValue(val) + + + def on_enter(self, event): + val = super().GetValue() + + self._unset_alarm() + + try: + val = arithmetic_eval(val) + self.value_type = type(val) + except SyntaxError as e: + en = typename(e) + msg = e.args[0] + msg = f"{en}: {msg}" + self._set_alarm(msg) + self.SetInsertionPoint(e.offset) + except Exception as e: + en = typename(e) + msg = f"{en}: {e}" + self._set_alarm(msg) + self.SetInsertionPointEnd() + else: + self.SetValue(val) + self._last_good_value = val + + event.Skip() + + + def on_escape(self, event): + code = event.GetKeyCode() + if code != wx.WXK_ESCAPE: + event.Skip() + return + + if self._alarm: + self.SetValue(self._last_good_value) + self._unset_alarm() + + + def _set_alarm(self, msg): + self._alarm = True + self.SetToolTip(msg) + self.SetForegroundColour(wx.RED) + + def _unset_alarm(self): + self._alarm = False + self.SetToolTip(None) + self.SetForegroundColour(wx.NullColour) + + + diff --git a/scam.py b/scam.py new file mode 100755 index 0000000..0156f18 --- /dev/null +++ b/scam.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +import wx + +#from cam_server_client import PipelineClient +from fake import PipelineClient + +from tools import EXPANDING, STRETCH, make_filled_vbox, make_filled_hbox +from mathentry import MathEntry +from listentry import ListEntry + + + +pc = PipelineClient("http://sf-daqsync-01:8889") +si = pc.get_server_info() +ai = si["active_instances"] +pls = (i for i in ai if "psen_db" in i) +pls = sorted(pls) + + + +class MainFrame(wx.Frame): + + def __init__(self, parent=None, title="test"): + super().__init__(parent, title=title) + + panel_main = MainPanel(self) + + self.sizer = sizer = make_filled_vbox([panel_main]) + self.SetSizerAndFit(sizer) + + + +class MainPanel(wx.Panel): + + def __init__(self, parent): + super().__init__(parent) + + self.cb_pls = cb_pls = wx.ComboBox(self, choices=pls) + self.entries = entries = SettingsList(self) + self.btn_print = btn_print = wx.Button(self, label="Print") + + cb_pls.Bind(wx.EVT_COMBOBOX, self.on_select) + btn_print.Bind(wx.EVT_BUTTON, self.on_print) + + widgets = [cb_pls, entries, btn_print] + sizer = make_filled_vbox(widgets) + self.SetSizer(sizer) + + + def on_select(self, event): + instance = self.cb_pls.GetValue() + cfg = pc.get_instance_config(instance) + self.entries.update(cfg) + self._adjust_size() + + + def _adjust_size(self): + parent = self.GetParent() + parent.sizer.Layout() + parent.Fit() + + + def on_print(self, event): + data = self.entries.get() + print(data) + + + +class SettingsList(wx.GridSizer): + + def __init__(self, parent, hgap=5, vgap=5): + super().__init__(cols=2, hgap=hgap, vgap=vgap) + self.parent = parent + self.children = [] + + def update(self, cfg): + self.clear() + for k, v in sorted(cfg.items()): + self.add(k, v) + + def clear(self): + self.Clear(True) + + def add(self, *args): + new = Setting(self.parent, *args) + self.children.append(new) + self.Add(new.state) + self.Add(new.text, 0, wx.EXPAND|wx.ALL) + + def get(self): + res = {} + for i in self.children: + name = i.get_name() + print(name) + if i.get_state(): + value = i.get_value() + else: + value = None + res[name] = value + return res + + + +class Setting: + + def __init__(self, parent, label, value): + self.state = state = wx.CheckBox(parent, label=label) + + if isinstance(value, list): + self.text = text = ListEntry(parent, style=wx.TE_RIGHT) + else: + self.text = text = MathEntry(parent, style=wx.TE_RIGHT) + + state.Bind(wx.EVT_CHECKBOX, self.on_state_change) + + if value is None: + state.SetValue(False) + text.Disable() + else: + state.SetValue(True) + text.SetValue(value) + + + def get_name(self): + return self.state.GetLabel() + + def get_state(self): + return self.state.GetValue() + + def get_value(self): + return self.text.GetValue() + + + def on_state_change(self, event): + if self.get_state(): + self.text.Enable() + else: + self.text.Disable() + + + + + +app = wx.App() +frame = MainFrame() +frame.Show() +app.MainLoop() + + + diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..c9d8f8c --- /dev/null +++ b/tools.py @@ -0,0 +1,59 @@ +import wx + + +WX_DEFAULT_RESIZABLE_DIALOG_STYLE = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.MINIMIZE_BOX|wx.MAXIMIZE_BOX + + +class EXPANDING: pass +class STRETCH: pass + + + +def post_event(event, source): + evt = wx.PyCommandEvent(event.typeId, source.GetId()) + wx.PostEvent(source, evt) + + +def copy_to_clipboard(val): + clipdata = wx.TextDataObject() + clipdata.SetText(val) + wx.TheClipboard.Open() + wx.TheClipboard.SetData(clipdata) + wx.TheClipboard.Close() + + + +def make_filled_vbox(widgets, proportion=0, flag=wx.ALL|wx.EXPAND, border=0, box=None): + return make_filled_box(wx.VERTICAL, widgets, proportion, flag, border, box) + +def make_filled_hbox(widgets, proportion=1, flag=wx.ALL|wx.EXPAND, border=0, box=None): + return make_filled_box(wx.HORIZONTAL, widgets, proportion, flag, border, box) + + +def make_filled_box(orient, widgets, proportion, flag, border, box): + if box is None: + box = wx.BoxSizer(orient) + + OTHER_PROP = { + 0: 1, + 1: 0 + } + + expand = False + + for i in widgets: + if i is STRETCH: + box.AddStretchSpacer() + elif i is EXPANDING: + expand = True # store for (and then apply to) next widget + else: + prop = proportion + if expand: + expand = False # apply only once + prop = OTHER_PROP[prop] # other proportion makes widget expanding + box.Add(i, proportion=prop, flag=flag, border=border) + + return box + + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..c3d4936 --- /dev/null +++ b/utils.py @@ -0,0 +1,5 @@ + +def typename(obj): + return type(obj).__name__ + +