diff --git a/etc/run.sh b/etc/run.sh index 04da760..089decf 100644 --- a/etc/run.sh +++ b/etc/run.sh @@ -16,24 +16,7 @@ set -eou pipefail shopt -s nullglob -_root_dir=$(cd "$(dirname "$0")/.." && pwd) -_run_dir=$_root_dir/run -_docker_image=docker://radiasoft/slicops -declare -A _port_map=( - [api]=0 - [repeater]=2 - [server]=3 - [vue]=1 -) -_bashrc=$_run_dir/bashrc.sh -_python_version=3.13.9 -_sif=${SLICOPS_APPTAINER_SIF:-$_run_dir/slicops.sif} -_sim_dir=/home/vagrant/.local/epics/extensions/synApps/support/areaDetector-R3-12-1/ADSimDetector/iocs/simDetectorIOC/iocBoot/iocSimDetector -_vue_dir=$_root_dir/ui -_start_msg="To develop, start the servers in this order in separate terminals: -bash etc/run.sh sim # sim-detector for DEV_CAMERA -bash etc/run.sh vue # vue server for javascript -bash etc/run.sh api # tornado for business logic and main server" +declare -A _port_map _dispatch_op() { declare op=$1 @@ -251,12 +234,42 @@ _err() { exit 1 } +_globals() { + declare r=$(cd "$(dirname "$0")/.." && pwd) + _run_dir=$r/run + _bashrc=$_run_dir/bashrc.sh + _docker_image=docker://radiasoft/slicops + _port_map=( + [api]=0 + [repeater]=2 + [server]=3 + [vue]=1 + ) + _python_version=3.13.9 + _sif=${SLICOPS_APPTAINER_SIF:-} + if [[ ! $_sif ]]; then + # POSIT: path defined by SLAC + _sif=/sdf/group/ad/org/lfd/hla/apptainer/slicops.sif + if [[ ! -r $_sif ]]; then + _sif=$_run_dir/slicops.sif + fi + fi + # POSIT: same as radiasoft/download/installers/epics-area-detector + _sim_dir=/home/vagrant/.local/epics/extensions/synApps/support/areaDetector-R3-12-1/ADSimDetector/iocs/simDetectorIOC/iocBoot/iocSimDetector + _vue_dir=$r/ui + _start_msg="To develop, start the servers in this order in separate terminals: +bash etc/run.sh sim # sim-detector for DEV_CAMERA (for dev only) +bash etc/run.sh vue # vue/vite server for javascript (for dev only) +bash etc/run.sh api # tornado serving vue/vite and APIs" +} + _log() { _msg $(date +%H%M%S) "$@" } _main() { declare op=${1:-} + _globals _env_check _dispatch_op "$op" "${@:2}" } diff --git a/slicops/ctx.py b/slicops/ctx.py index b66eac7..9b3f7a5 100644 --- a/slicops/ctx.py +++ b/slicops/ctx.py @@ -129,29 +129,29 @@ def is_field_value_valid(self, name, value): self.__field(name).value_check(value), slicops.field.InvalidFieldValue ) - def field_get(self, name): - return self.__field(name).value_get() - def field_names(self): # keys are always the same return tuple(self.__ctx.fields.keys()) - def field_set(self, name, value): + def field_value(self, name): + return self.__field(name).value() + + def field_value_set(self, name, value): # TODO(robnagler) optimize this case to not validate constraints/ui # could possibly optimize the ui and constraints parts when a copy # vs new with _defaults() (which should get validated first time) self.__field_update(name, self.__field(name), PKDict(value=value)) - def field_set_via_api(self, name, value, on_method): + def field_value_set_via_api(self, name, value, on_method): def _update(old, new): rv = PKDict(field_name=name, on_method=on_method, txn=self) if on_method.kind == "click": - if new.group_get("ui", "clickable"): + if new.group_attr("ui", "clickable"): return rv pkdlog("on_click_{} exists and clickable=False", c.field_name) return None if on_method.kind == "change": - rv.pkupdate(value=n.value_get(), old_value=o.value_get()) + rv.pkupdate(value=n.value(), old_value=o.value()) if rv.value == rv.old_value: return None return rv @@ -161,7 +161,7 @@ def _update(old, new): try: o = self.__field(name) - if not o.group_get("ui", "writable"): + if not o.group_attr("ui", "writable"): raise pykern.util.APIError( "field={} is not writable value={}", name, value ) @@ -172,13 +172,21 @@ def _update(old, new): raise raise pykern.util.APIError("invalid value for field={} error={}", name, e) - def group_get(self, field, group, attr=None): - return self.__ctx.fields[field].group_get(group, attr) + def group_attr(self, field_or_dotted, group=None, attr=None): + if group is None: + p = field_or_dotted.split(".") + (f, group, attr) = tuple(p + [None] * (3 - len(p))) + else: + f = field_or_dotted + return self.__field(f).group_attr(group, attr) + + def group_attr_set(self, dotted, value): + self.multi_group_attr_set((dotted, value)) - def multi_get(self, fields): - return PKDict((k, self.__field(k).value_get()) for k in fields) + def multi_field_value(self, fields): + return PKDict((k, self.__field(k).value()) for k in fields) - def multi_set(self, *args): + def multi_group_attr_set(self, *args): def _args(): if len(args) > 1: # (("a", 1), ("b", 2), ..) diff --git a/slicops/field.py b/slicops/field.py index 1f1d0ac..7ea0239 100644 --- a/slicops/field.py +++ b/slicops/field.py @@ -59,7 +59,7 @@ def _copy(): def as_dict(self): return PKDict((k, copy.deepcopy(self._attrs[k])) for k in self.__TOP_ATTRS) - def group_get(self, group, attr=None): + def group_attr(self, group, attr=None): if group not in self.__GROUP_ATTRS: raise AssertionError(f"invalid group={group} must be {self.__GROUP_ATTRS}") g = self._attrs[group] @@ -77,6 +77,9 @@ def renew(self, overrides): # Updating an instance inherits everything return self.__class__(self, overrides) + def value(self): + return self._attrs.value + def value_check(self, value): if value is None or hasattr(value, "__len__") and len(value) == 0: if self._attrs.constraints.nullable: @@ -90,9 +93,6 @@ def value_check(self, value): rv.kwargs.field_name = self._attrs.name return rv - def value_get(self): - return self._attrs.value - def value_set(self, value): v = self.value_check(value) if isinstance(v, InvalidFieldValue): @@ -225,7 +225,7 @@ def _defaults(self, *overrides): return super()._defaults( PKDict( name="Button", - ui=PKDict(widget="button", clickable=True), + ui=PKDict(widget="button", css_kind="primary", clickable=True), # value is always None value=None, ), diff --git a/slicops/sliclet/__init__.py b/slicops/sliclet/__init__.py index 0e0b60e..308d77a 100644 --- a/slicops/sliclet/__init__.py +++ b/slicops/sliclet/__init__.py @@ -241,7 +241,7 @@ def _click(updates): def _updates(): m = self.__on_methods for k, v in field_values.items(): - if c := txn.field_set_via_api(k, v, m.get(k)): + if c := txn.field_value_set_via_api(k, v, m.get(k)): yield c with self.lock_for_update(log_op="ctx_write") as txn: diff --git a/slicops/sliclet/fractals.py b/slicops/sliclet/fractals.py index bb9c8c4..cbefb97 100644 --- a/slicops/sliclet/fractals.py +++ b/slicops/sliclet/fractals.py @@ -14,7 +14,7 @@ class Fractals(slicops.sliclet.yaml_db.YAMLDb): def on_change_mode(self, txn, value, **kwargs): j = value == "Julia" - txn.multi_set( + txn.multi_group_attr_set( ("density_i.ui.visible", j), ("density_r.ui.visible", j), ) diff --git a/slicops/sliclet/screen.py b/slicops/sliclet/screen.py index 0862080..873144d 100644 --- a/slicops/sliclet/screen.py +++ b/slicops/sliclet/screen.py @@ -99,7 +99,7 @@ def handle_destroy(self): self.__device_destroy() def on_change_camera(self, txn, value, **kwargs): - self.__device_change(txn, txn.field_get("beam_path"), value) + self.__device_change(txn, txn.field_value("beam_path"), value) def on_change_beam_path(self, txn, value, **kwargs): self.__beam_path_change(txn, value) @@ -137,7 +137,9 @@ def handle_init(self, txn): self.__device = None self.__handler = None self.__single_button = False - txn.multi_set(("beam_path.constraints.choices", slicops.device_db.beam_paths())) + txn.multi_group_attr_set( + ("beam_path.constraints.choices", slicops.device_db.beam_paths()) + ) self.__beam_path_change(txn, None) self.__device_change(txn, None, None) b = c = None @@ -146,12 +148,14 @@ def handle_init(self, txn): c = _cfg.dev.camera # the values are None by default, but this initializes # the state of the choices, buttons and fields appropriately - txn.field_set("beam_path", b) + txn.field_value_set("beam_path", b) self.__beam_path_change(txn, b) - txn.field_set("camera", c) + txn.field_value_set("camera", c) def handle_start(self, txn): - self.__device_setup(txn, txn.field_get("beam_path"), txn.field_get("camera")) + self.__device_setup( + txn, txn.field_value("beam_path"), txn.field_value("camera") + ) def __beam_path_change(self, txn, value): def _choices(): @@ -159,31 +163,33 @@ def _choices(): return () return slicops.device_db.device_names(_DEVICE_TYPE, value) - txn.multi_set( + txn.multi_group_attr_set( ("camera.constraints.choices", _choices()), ("camera.value", None), ) # This technically shouldn't happen if value is None: - txn.multi_set( + txn.multi_group_attr_set( _DEVICE_DISABLE + (("camera.ui.enabled", False), ("camera.ui.visible", False)) ) else: - txn.multi_set((("camera.ui.enabled", True), ("camera.ui.visible", True))) + txn.multi_group_attr_set( + (("camera.ui.enabled", True), ("camera.ui.visible", True)) + ) if not self.__device: # No device change return c = self.__device.device_name if txn.is_field_value_valid("camera", c): # Camera is the same so restore the value, no device change - txn.field_set("camera", c) + txn.field_value_set("camera", c) else: self.__device_change(txn, value, None) def __device_change(self, txn, beam_path, camera): self.__device_destroy(txn) - txn.multi_set(_DEVICE_DISABLE) + txn.multi_group_attr_set(_DEVICE_DISABLE) self.__device_setup(txn, beam_path, camera) def __device_destroy(self, txn=None): @@ -230,7 +236,7 @@ def __device_setup(self, txn, beam_path, camera): s = PKDict(_DEVICE_ENABLE + (("csi_name.value", self.__device.meta.csi_name),)) if self.__device.has_accessor("target_status"): s.update(_TARGET_VISIBLE) - txn.multi_set(s) + txn.multi_group_attr_set(s) self.__new_image_set(txn) def __handle_acquire(self, acquire): @@ -238,7 +244,7 @@ def __handle_acquire(self, acquire): self.__current_value["acquire"] = acquire n = not acquire # Leave plot alone - txn.multi_set( + txn.multi_group_attr_set( ("single_button.ui.enabled", n), ("start_button.ui.enabled", n), ( @@ -257,14 +263,14 @@ def __handle_image(self, image): self.__current_value["image"] = image if self.__update_plot(txn) and self.__single_button: self.__set(txn, "acquire", False, _BUTTONS_DISABLE) - txn.multi_set( + txn.multi_group_attr_set( ("single_button.ui.enabled", True), ("start_button.ui.enabled", True), ) def __new_image_set(self, txn): self.__image_set = slicops.plot.ImageSet( - txn.multi_get( + txn.multi_field_value( ( "beam_path", "camera", @@ -278,7 +284,7 @@ def __new_image_set(self, txn): def __handle_target_status(self, status): with self.lock_for_update() as txn: self.__current_value["target"] = status - txn.multi_set( + txn.multi_group_attr_set( ("target_status", status.name), ( "target_in_button.ui.enabled", @@ -306,7 +312,7 @@ def __set(self, txn, accessor, value, txn_set, method=None): if v is not None and v == value: # No button disable since nothing changed return - txn.multi_set(txn_set) + txn.multi_group_attr_set(txn_set) try: if method is None: self.__device.put(accessor, value) @@ -326,9 +332,9 @@ def __update_plot(self, txn): return False if (p := self.__image_set.add_frame(i, pykern.pkcompat.utcnow())) is None: return False - if not txn.group_get("plot", "ui", "visible"): - txn.multi_set(_PLOT_ENABLE) - txn.field_set("plot", p) + if not txn.group_attr("plot", "ui", "visible"): + txn.multi_group_attr_set(_PLOT_ENABLE) + txn.field_value_set("plot", p) return True def __user_alert(self, txn, fmt, *args): diff --git a/slicops/sliclet/yaml_db.py b/slicops/sliclet/yaml_db.py index 4ae90c7..5cd12c0 100644 --- a/slicops/sliclet/yaml_db.py +++ b/slicops/sliclet/yaml_db.py @@ -76,7 +76,7 @@ def _visibility(value): p = PKDict(raw_pixels=None) v = False try: - if not (l := txn.field_get(n)): + if not (l := txn.field_value(n)): return None p.raw_pixels = numpy.load(l) v = True @@ -84,13 +84,13 @@ def _visibility(value): except Exception as e: pkdlog("numpy.load error={} path={} link={} stack={}", e, l, n, pkdexc()) finally: - txn.field_set(plot, p) - txn.multi_set(tuple(_visibility(v))) + txn.field_value_set(plot, p) + txn.multi_group_attr_set(tuple(_visibility(v))) def __read_db(self, txn): def _numpy_files(): for k in txn.field_names(): - if l := txn.group_get(k, "links"): + if l := txn.group_attr(k, "links"): if v := self.__numpy_file(txn, k, l): yield k, v @@ -107,7 +107,7 @@ def _set(db): # If cache (read/wrote last time) is unchanged, # there will be no updates. Avoids churn if k in db and db[k] != self.__db_cache.get(k): - txn.field_set(k, db[k]) + txn.field_value_set(k, db[k]) yield k, db[k] if not (r := slicops.pkcli.yaml_db.read(self.name)): @@ -120,13 +120,13 @@ def _set(db): def __write(self, txn): def _keys(): for k in txn.field_names(): - g = txn.group_get(k, "ui") + g = txn.group_attr(k, "ui") if g.get("clickable") or not g.get("writable"): continue yield k # TODO(robnagler) work: maybe should happen outside lock - self.__db_cache = PKDict((k, txn.field_get(k)) for k in _keys()) + self.__db_cache = PKDict((k, txn.field_value(k)) for k in _keys()) slicops.pkcli.yaml_db.write(self.name, self.__db_cache) diff --git a/slicops/unit_util.py b/slicops/unit_util.py index 9f27529..23fa7fe 100644 --- a/slicops/unit_util.py +++ b/slicops/unit_util.py @@ -32,7 +32,7 @@ async def ctx_update(self): pkunit.pkfail("subscription ended unexpectedly") return r - async def ctx_field_set(self, **kwargs): + async def ctx_field_value_set(self, **kwargs): from pykern.pkcollections import PKDict from pykern import pkdebug diff --git a/tests/ctx_test.py b/tests/ctx_test.py index 7fc67ce..8bc744d 100644 --- a/tests/ctx_test.py +++ b/tests/ctx_test.py @@ -27,11 +27,14 @@ def test_txn(): c = ctx.Ctx("input", "Input", path=pkunit.data_dir().join("simple.in")) txn = ctx.Txn(c) with pkunit.pkexcept(ValueError): - txn.field_set("increment", 0) - txn.multi_set( - ("run_mode.constraints.choices", ("a", "b", "c")), + txn.field_value_set("increment", 0) + txn.multi_group_attr_set( + ("run_mode.constraints.choices", v := ("a", "b", "c")), ("run_mode.value", None), ) + v = PKDict(zip(v, v)) + pkunit.pkeq(v, txn.group_attr("run_mode.constraints.choices")) + pkunit.pkeq(v, txn.group_attr("run_mode", "constraints", "choices")) r = PKDict() txn.commit(lambda x: r.pkupdate(fields=x.fields)) pkunit.pkeq(None, r.fields.run_mode.value) diff --git a/tests/sliclet/screen_test.py b/tests/sliclet/screen_test.py index 6080094..5c1a48d 100644 --- a/tests/sliclet/screen_test.py +++ b/tests/sliclet/screen_test.py @@ -42,7 +42,7 @@ async def _buttons(s, expect, msg): pkunit.pkeq("DEV_BEAM_PATH", r.fields.beam_path.value) pkunit.pkeq("DEV_CAMERA", r.fields.camera.value) await _buttons(s, (True, False, True), "start/single enabled") - await s.ctx_field_set(start_button=None) + await s.ctx_field_value_set(start_button=None) await _buttons(s, (False, False, False), "all disabled after start") await _buttons(s, (False, True, False), "acquire should fire") p = (await s.ctx_update()).fields.plot.value @@ -51,7 +51,7 @@ async def _buttons(s, expect, msg): # x fit should be 10 pkunit.pkeq(10.00, round(p.x.fit.results.sig, 2)) pkunit.pkeq(13.00, round(p.y.fit.results.sig, 2)) - await s.ctx_field_set( + await s.ctx_field_value_set( beam_path="CU_SPEC", curve_fit_method="super_gaussian", stop_button=None, @@ -61,8 +61,8 @@ async def _buttons(s, expect, msg): # there's no device so buttons on not visible pkunit.pkeq(False, r.fields.start_button.ui.visible) with pkunit.pkexcept("unknown choice"): - await s.ctx_field_set(camera="DEV_CAMERA") + await s.ctx_field_value_set(camera="DEV_CAMERA") # TODO(robnagler) better error handling await _put(ux, "camera", "DEV_CAMERA", Exception) - await s.ctx_field_set(camera="YAG01") + await s.ctx_field_value_set(camera="YAG01") r = await s.ctx_update() pkunit.pkeq("YAGS:IN20:211", r.fields.csi_name.value) diff --git a/tests/sliclet/yaml_db_test.py b/tests/sliclet/yaml_db_test.py index 09ed445..7182d90 100644 --- a/tests/sliclet/yaml_db_test.py +++ b/tests/sliclet/yaml_db_test.py @@ -21,12 +21,12 @@ async def test_basic(): pkunit.pkeq(3.14, r.fields.divisor.value) pkunit.pkeq("method_1", r.fields.run_mode.value) pkunit.pkeq(5, r.fields.increment.value) - await s.ctx_field_set(divisor=1.1) + await s.ctx_field_value_set(divisor=1.1) r = await s.ctx_update() pkunit.pkeq(["divisor"], list(r.fields.keys())) pkunit.pkeq(1.1, r.fields.divisor.value) pkunit.pkeq(3.14, yaml_db.read("yaml_db").divisor) - r = await s.ctx_field_set(save=None) + r = await s.ctx_field_value_set(save=None) # save button await s.ctx_update() pkunit.pkeq(1.1, yaml_db.read("yaml_db").divisor) @@ -34,9 +34,9 @@ async def test_basic(): yaml_db.write("yaml_db", "divisor=3") r = await s.ctx_update() pkunit.pkeq(3.0, r.fields.divisor.value) - await s.ctx_field_set(run_mode="method_2") + await s.ctx_field_value_set(run_mode="method_2") r = await s.ctx_update() - await s.ctx_field_set(revert=None) + await s.ctx_field_value_set(revert=None) await s.ctx_update() # no update, bc no change pkunit.pkeq("method_1", yaml_db.read("yaml_db").run_mode)