From 400cbdbd9212743201cf42532d315c23ea649f71 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 29 Sep 2021 16:10:29 +0200 Subject: [PATCH] first prototype --- .gitignore | 129 ++++++++++++++++++++++++++++++++++++++++++++ eval.py | 78 +++++++++++++++++++++++++++ fake.py | 41 ++++++++++++++ listentry.py | 34 ++++++++++++ mathentry.py | 87 ++++++++++++++++++++++++++++++ scam.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ tools.py | 59 ++++++++++++++++++++ utils.py | 5 ++ 8 files changed, 583 insertions(+) create mode 100644 .gitignore create mode 100644 eval.py create mode 100644 fake.py create mode 100644 listentry.py create mode 100644 mathentry.py create mode 100755 scam.py create mode 100644 tools.py create mode 100644 utils.py 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__ + +