diff --git a/pixi.lock b/pixi.lock index 0ee7439..5cb1631 100644 --- a/pixi.lock +++ b/pixi.lock @@ -126,7 +126,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py313hc6c575f_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py313h4900a6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py313hbfd7664_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py313h3b26573_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-h26ae035_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313hd42f317_3.conda @@ -134,7 +134,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py313h78bf25f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py313h98bfbea_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py313h6123c0d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.15-hf47f18c_103_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py313h2139695_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.10.0-py313hd42f317_1.conda @@ -147,7 +147,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py313he2a5f1a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py313h995f894_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py313h995f894_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py313h7f1de9a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-h7cc23a3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-h7cc23a3_2.conda @@ -220,7 +220,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda @@ -266,16 +266,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzlocal-5.4.4-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - pypi: https://files.pythonhosted.org/packages/08/0f/0daf459b604f61253b914b28f0e6dd16cdc8a7aa962cf901e482f5cbba67/pymongo-4.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl @@ -284,7 +285,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/2e/43/102114edc44314b669904ce51f1473137b2830bf4862dfc6d2b1ad3fcf91/confluent_kafka-2.15.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl @@ -358,7 +358,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py313ha52865f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py313h2551ef2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py313h2af15c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py313h2af15c8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h995f894_3.conda @@ -372,7 +372,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py313ha4a6282_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py313h5cbdff7_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py313h901f96d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py313h901f96d_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py313ha296275_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py313hf402d47_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda @@ -490,7 +490,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py313hc6c575f_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py313h4900a6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py313hbfd7664_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py313h3b26573_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -500,7 +500,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py313h78bf25f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py313h98bfbea_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py313h6123c0d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py313hd9d88b3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.15-hf47f18c_103_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py313h2139695_0.conda @@ -516,16 +516,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py313hb172dc5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py313he2a5f1a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py313hd42f317_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py313h995f894_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py313h995f894_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py313h7037e92_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py313h995f894_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py313ha48717d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py313h7f1de9a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py313hd42f317_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py313hd42f317_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -580,36 +580,488 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.5.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.15-py313hd8ed1ab_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cross-web-0.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ecdsa-0.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/event-model-1.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.141.1-h76d476e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.32-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.141.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.32.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/graphql-core-3.2.12-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.19-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/import-linter-2.15-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inflection-0.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.17.1-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.20.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.1.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.6.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json-merge-patch-0.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/minio-7.2.20-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mongoquery-1.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.26.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-extra-types-2.11.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.11.0-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.15.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.15-h4df99d1_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.3.post1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ragged-0.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/redis-py-8.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.20.5-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sparse-0.19.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stamina-26.1.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.6.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/strawberry-graphql-0.327.7-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/textual-plotext-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.7.0-pyhc455866_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.9.15-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-base-0.2.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-client-0.2.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-server-0.2.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.16.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.27.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.16.0-h69aa097_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.16.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzlocal-5.4.4-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yamale-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.4.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda + - pypi: ./ + - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa + - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 + - pypi: https://files.pythonhosted.org/packages/08/0f/0daf459b604f61253b914b28f0e6dd16cdc8a7aa962cf901e482f5cbba67/pymongo-4.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/26/c2/2ec71837ac862d823de280e72c245686b37de6dde0a18a02c9c18f589c39/setuptools_dso-2.12.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2e/43/102114edc44314b669904ce51f1473137b2830bf4862dfc6d2b1ad3fcf91/confluent_kafka-2.15.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6d/5c/9507ea3c732f8a259f45ddf190a741e9bf7d3fe229b5d473917ee6efbf1e/pint-0.26.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/aa/3b0faa7eea6a2957af7ee116f08b5edd99eff7e7461e22ac613b0a8d38ea/epicscorelibs-7.0.10.99.0.1-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/9d/872279feea98802a9c3e5f2680cb1b38cd49d7e249cd2220d07a69c7079a/suitcase_mongo-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/d3/a3e8393cdf6e40e94b3a91ab8afdb010769829b520b2fbfdc4697f31a1c3/bluesky_kafka-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bc/68/c45fbb7bfe21b81ccf923b02549e2b1504539dacdbbb3444c01aa00c2ee2/pvxslibs-1.5.2-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dd/fb/4939594c20028eca7ee54cea21de25762ef6934a5fb168b8130599921c88/p4p-4.2.2-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e1/c1/a314376de17d02eb3e798929114cefeea29776174dab06b8162ce65ca82d/databroker-2.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/80/c6dbf1f8d14fbb5615d2e99b798d61b76eb937f09e1e948d0e8aaf8873b2/ophyd-1.11.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/4f/f94ac1b84d2169cf2ebf64353ce98fd743f85d30678059c514d9b3d6644c/compress_pickle-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fe/47/deb64c73aec25af7699247e021153a6bfe9a08452f7f7337dcee4aa07a2b/historydict-1.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + gui: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/awkward-cpp-56-py312h9b2b4d3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.4-hb7a77c6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h2aa3ae6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-h720e601_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h6ffeea8_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h38ae05a_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.27.3-h6f4d18d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.16.0-h21f4ec5_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.8-h46fcd08_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.7-h720e601_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-h720e601_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.1-h102d43b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.833-hc7390e0_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.3-h206d751_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-h71f81a8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.7.0-py312h3f22e6b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-h505cf86_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-h9908984_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312he9c40d5_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hebe6cf0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-3.3.4-hea3f660_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py312h9be0db6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py312h5cc1888_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-he8c428d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.22-py312h80505a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.4.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.3-h4db4eae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.65.0-py312hfd18d64_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hb7133d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hfd2156b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py312h04c1168_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.1-py312h9be0db6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbc21106_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19.1-h9073bf1_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.2.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h0dc7533_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-25.0.0-hcc2f0d9_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-25.0.0-h635bf11_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-25.0.0-h53684a4_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-25.0.0-h635bf11_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-25.0.0-h66fbfdd_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-11_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-h39a168f_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-ha411449_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-h018ffa1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-11_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang-cpp23.1-23.1.1-default_h0acdd01_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libclang13-23.1.1-default_h7037f76_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.22.0-ha042cf0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-hd45a770_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.129-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h373387f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-h5348a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.1-hecca717_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.7.0-h81df57d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h5e6c136_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-16.2.0-ha9f2e26_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-16.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-16.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-16.2.0-h6b99dfc_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.90.0-h569388d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-16.2.0-he0feb66_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.8.0-hbc29df5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.8.0-hdbdcf42_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.82.1-h792040b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libharfbuzz-14.4.0-h23af247_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h0cb94f2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-11_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libllvm23-23.1.1-h474f4eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h74cf4be_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libntlm-1.8-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.34-pthreads_hf13c14d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopengl-1.7.0-ha4b6fd6_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h3133023_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-25.0.0-h7376487_4_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.19-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h922cc85_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpq-18.6-h9d76c99_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-7.35.1-h622638d_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpsl-0.23.1-hd9e3e90_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpython-3.12.14-h0c77377_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libraqm-0.11.0-h6406941_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h60473fc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-hebe6cf0_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.4-h13e7031_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-h6154650_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-16.2.0-h934c35e_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-16.2.0-hdf11a46_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-hcc2c06a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.357.0-h0e34353_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-hb83e432_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.38-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-h51789e4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.4-hf3af7cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.4-h7df9aa5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.45-h8e12856_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/llvmlite-0.49.0-py312hb491018_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312hc6a72db_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-hee9eb32_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.4.0-ha2cb11d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.2-ha2cb11d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.2-py312h9be0db6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ndindex-1.10.1-py312ha6a3dbb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.67.0-py312h80505a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.2-py312h0000c9d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.3-py312he827f4e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-heb1ab33_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.13-hbde042b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312hafd94c1_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-h26ae035_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py312h1b36aeb_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h7cc23a3_1004.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py312hf520645_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.10.0-py312h1b36aeb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.2.0-py312h8a5ba0d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qt6-main-6.11.2-pl5321h9df5c37_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h94463f1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.5-h7e3ee7f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py312h4ae17e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-keysyms-0.4.1-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-renderutil-0.3.10-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-wm-0.4.2-hb711507_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.48-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-h280c20c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-h0d788c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.13-he1eb515_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-h7cc23a3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-h7cc23a3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.7-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.3-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.5-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-h7cc23a3_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.7-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2025.1-hebe6cf0_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-hebe6cf0_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h901266b_12.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hce19668_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py312h1b36aeb_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_x86_64-microarch-level-1-5_level1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.20.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/algotom-1.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.15.1-pyh5ded981_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/awkward-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bluesky-tiled-plugins-2.0.11-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.5.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.15-py313hd8ed1ab_103.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cross-web-0.7.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.14-py312hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.8.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.8.0-pyhc364b38_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.8.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecdsa-0.19.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/event-model-1.24.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.141.1-h76d476e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.32-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.141.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.32.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -617,34 +1069,31 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/graphql-core-3.2.12-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.19-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.4-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/import-linter-2.15-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-resources-7.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/inflection-0.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.3.0-pyha191276_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.17.1-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.20.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.1.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/json-merge-patch-0.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.10.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 @@ -653,53 +1102,42 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/minio-7.2.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mongoquery-1.4.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio2-1.7.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.26.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-extra-types-2.11.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.11.0-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.15.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.15-h4df99d1_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-9_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.3.post1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/qtpy-2.4.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ragged-0.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/redis-json-dict-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/redis-py-8.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.20.5-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda @@ -719,7 +1157,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stamina-26.1.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.6.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/strawberry-graphql-0.327.7-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda @@ -728,8 +1165,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.9.15-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-base-0.2.18-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-client-0.2.18-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-server-0.2.18-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.16.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.27.2-pyhcf101f3_0.conda @@ -741,64 +1176,64 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/yamale-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.4.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa + - pypi: git+https://github.com/jwlodek/ntnda-qt-viewer#5e55d709f47876132a6e9f9413d423ba72a3fef4 - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - - pypi: https://files.pythonhosted.org/packages/08/0f/0daf459b604f61253b914b28f0e6dd16cdc8a7aa962cf901e482f5cbba67/pymongo-4.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/c2/2ec71837ac862d823de280e72c245686b37de6dde0a18a02c9c18f589c39/setuptools_dso-2.12.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2e/43/102114edc44314b669904ce51f1473137b2830bf4862dfc6d2b1ad3fcf91/confluent_kafka-2.15.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/47/b45332d3d6427811909d934f8675956ee5b9e10e46f689464d6ea23ab858/pvxslibs-1.5.2-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5d/32/6e723c9fa2847ba268c02377f75a4ae3432ec82fcf1e9776e2b9ca5f18e7/suitcase_utils-0.5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/85/115246eeec396feedc81bed42f832c97bf433a803688a4bf0e5d763e808a/bluesky_queueserver_api-0.0.13-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/66/a3/54b9d35d9f2e6b4a2546212c1b0df4412a97c4323223a1f9ace8ee22689f/bluesky_queueserver-0.0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/5c/9507ea3c732f8a259f45ddf190a741e9bf7d3fe229b5d473917ee6efbf1e/pint-0.26.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7c/aa/3b0faa7eea6a2957af7ee116f08b5edd99eff7e7461e22ac613b0a8d38ea/epicscorelibs-7.0.10.99.0.1-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7c/60/998d447c893f31d8967c6e17cd018423bde995602240bf72b8f0aa58fd83/bluesky_widgets-0.0.18-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/9d/872279feea98802a9c3e5f2680cb1b38cd49d7e249cd2220d07a69c7079a/suitcase_mongo-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/d3/a3e8393cdf6e40e94b3a91ab8afdb010769829b520b2fbfdc4697f31a1c3/bluesky_kafka-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bc/68/c45fbb7bfe21b81ccf923b02549e2b1504539dacdbbb3444c01aa00c2ee2/pvxslibs-1.5.2-cp313-cp313-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/9f/c0d4811ce61696113b7b0b2f8dfa1218c4b47caad8383ce8269555884d90/bluesky_httpserver-0.0.12-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dd/fb/4939594c20028eca7ee54cea21de25762ef6934a5fb168b8130599921c88/p4p-4.2.2-cp313-cp313-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d7/a3/31112ce258d74be4636e6d1f73717587e19fd311c70477ae807136f3a48c/confluent_kafka-2.15.1-cp312-cp312-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/de/8b/65dccc36873290e89c0d1d3c1121f161ff28c37201e0d008cfa664d606b5/bluesky_live-0.0.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c1/a314376de17d02eb3e798929114cefeea29776174dab06b8162ce65ca82d/databroker-2.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/45/863765ab799e3197d1863c2035bf0ce1f7e925d33a5c1d1153cc01c888e3/p4p-4.2.2-cp312-cp312-manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ec/14/1c52a2a3ca612d77550fef0404a496f080bfd600cf4c7c6a2a8ab4be2dc6/pamela-1.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ed/62/640ada66e62ae1646447ff246d73f73f82abf3b9e19b94ee63e6bb99177c/pymongo-4.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ef/80/c6dbf1f8d14fbb5615d2e99b798d61b76eb937f09e1e948d0e8aaf8873b2/ophyd-1.11.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/4f/f94ac1b84d2169cf2ebf64353ce98fd743f85d30678059c514d9b3d6644c/compress_pickle-2.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fb/e5/f5c38d4899cd8edd5b522071c5658202077d113c3225484d779cf257970e/numpydoc-1.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/47/deb64c73aec25af7699247e021153a6bfe9a08452f7f7337dcee4aa07a2b/historydict-1.2.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - gui: + gui-dev: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -806,7 +1241,10 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/adbc-driver-manager-1.12.0-py312ha6a3dbb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.16.1-h7cc23a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-26.1.0-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/asyncpg-0.31.0-py312h5253ce2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/awkward-cpp-56-py312h9b2b4d3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.4-hb7a77c6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h2aa3ae6_4.conda @@ -826,28 +1264,41 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.7.0-py312h3f22e6b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-h505cf86_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-h9908984_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312he9c40d5_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hebe6cf0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-3.3.4-hea3f660_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py312h9be0db6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py312h8b7ec3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py312h5cc1888_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-he8c428d_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.22-py312h80505a6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/double-conversion-3.4.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fastar-0.12.0-py312h15b83fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.3-h4db4eae_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.65.0-py312hfd18d64_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.17-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hb7133d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hfd2156b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py312h04c1168_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py312hda0ef95_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5plugin-7.1.0-py312hff6e9eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.8.0-py312h5cc1888_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-h7cc23a3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.1-py312h9be0db6_1.conda @@ -856,6 +1307,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.46.1-default_hbd61a6d_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.2.0-hdb68285_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h0dc7533_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-postgresql-1.12.0-hd2595c0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-sqlite-1.12.0-hfab6643_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.5-h088129d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-25.0.0-hcc2f0d9_4_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-25.0.0-h635bf11_4_cpu.conda @@ -928,6 +1381,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.2-hcc2c06a_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.3-hcfc3c73_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.52.1-h280c20c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.357.0-h0e34353_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-hb83e432_2.conda @@ -941,20 +1395,27 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312hc6a72db_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-hee9eb32_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.4.0-ha2cb11d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.2-ha2cb11d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.2-py312h9be0db6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgspec-0.21.1-py312h5cc1888_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ndindex-1.10.1-py312ha6a3dbb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numba-0.67.0-py312h80505a6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numcodecs-0.16.5-py312hf79963d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.2-py312h0000c9d_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.3-py312he827f4e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/obstore-0.11.1-py312h9f92c4b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-heb1ab33_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjph-0.31.0-h8d634f6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openldap-2.6.13-hbde042b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312hafd94c1_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -963,10 +1424,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h7cc23a3_1004.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py312hf520645_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-duckdb-1.3.2-py312h1289d80_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.10.0-py312h1b36aeb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.2.0-py312h8a5ba0d_2.conda @@ -975,15 +1438,22 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h94463f1_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-hd6e31c0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-2026.6.3-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.16.7-hbcb56a7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.5-h7e3ee7f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py312h4ae17e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-18.0.0-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py312hd9148b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py312hcb3ea6e_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py312h1b36aeb_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -1015,33 +1485,62 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_x86_64-microarch-level-1-5_level1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-postgresql-1.12.0-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.12.0-pyha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiofiles-25.1.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosqlite-0.22.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.20.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/algotom-1.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.15.1-pyh5ded981_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asgi-correlation-id-5.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/awkward-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.asyncio.runner-1.2.0-pyh5ded981_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bluesky-tiled-plugins-2.0.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.5.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.14-py312hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cross-web-0.7.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.8.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.4.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ecdsa-0.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.3.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/event-model-1.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.141.1-h76d476e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.32-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.141.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.32.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -1049,22 +1548,29 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/graphql-core-3.2.12-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.19-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.19-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/import-linter-2.15-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-resources-7.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/inflection-0.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.3.0-pyha191276_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.17.1-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.20.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.1.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/json-merge-patch-0.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda @@ -1076,33 +1582,53 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/minio-7.2.20-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mongoquery-1.4.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-5.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio2-1.7.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nomkl-1.0-h5ca1d4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.26.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-extra-types-2.11.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.11.0-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-asyncio-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.15.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.3.post1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/qtpy-2.4.3-pyhd8ed1ab_1.conda @@ -1110,15 +1636,29 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/redis-json-dict-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/redis-py-8.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.20.5-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sparse-0.19.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stamina-26.1.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.6.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/strawberry-graphql-0.327.7-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda @@ -1127,6 +1667,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tifffile-2026.9.15-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-base-0.2.18-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-client-0.2.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tiled-server-0.2.18-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.16.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.27.2-pyhcf101f3_0.conda @@ -1136,85 +1678,61 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzlocal-5.4.4-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yamale-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.4.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e - - pypi: git+https://github.com/jwlodek/ntnda-qt-viewer?rev=main#1291f36ad13f54152d1607a4622cc9f912e0d0cb + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa + - pypi: git+https://github.com/jwlodek/ntnda-qt-viewer#5e55d709f47876132a6e9f9413d423ba72a3fef4 - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/c2/2ec71837ac862d823de280e72c245686b37de6dde0a18a02c9c18f589c39/setuptools_dso-2.12.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/47/b45332d3d6427811909d934f8675956ee5b9e10e46f689464d6ea23ab858/pvxslibs-1.5.2-cp312-cp312-manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/32/6e723c9fa2847ba268c02377f75a4ae3432ec82fcf1e9776e2b9ca5f18e7/suitcase_utils-0.5.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/85/115246eeec396feedc81bed42f832c97bf433a803688a4bf0e5d763e808a/bluesky_queueserver_api-0.0.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/66/a3/54b9d35d9f2e6b4a2546212c1b0df4412a97c4323223a1f9ace8ee22689f/bluesky_queueserver-0.0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/5c/9507ea3c732f8a259f45ddf190a741e9bf7d3fe229b5d473917ee6efbf1e/pint-0.26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/60/998d447c893f31d8967c6e17cd018423bde995602240bf72b8f0aa58fd83/bluesky_widgets-0.0.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/9d/872279feea98802a9c3e5f2680cb1b38cd49d7e249cd2220d07a69c7079a/suitcase_mongo-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/d3/a3e8393cdf6e40e94b3a91ab8afdb010769829b520b2fbfdc4697f31a1c3/bluesky_kafka-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/9f/c0d4811ce61696113b7b0b2f8dfa1218c4b47caad8383ce8269555884d90/bluesky_httpserver-0.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/a3/31112ce258d74be4636e6d1f73717587e19fd311c70477ae807136f3a48c/confluent_kafka-2.15.1-cp312-cp312-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/8b/65dccc36873290e89c0d1d3c1121f161ff28c37201e0d008cfa664d606b5/bluesky_live-0.0.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c1/a314376de17d02eb3e798929114cefeea29776174dab06b8162ce65ca82d/databroker-2.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/45/863765ab799e3197d1863c2035bf0ce1f7e925d33a5c1d1153cc01c888e3/p4p-4.2.2-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ec/14/1c52a2a3ca612d77550fef0404a496f080bfd600cf4c7c6a2a8ab4be2dc6/pamela-1.2.0-py2.py3-none-any.whl @@ -1267,7 +1785,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py311h904a5e5_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/charls-2.4.4-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py311h724c32c_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py311h364832d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py311h364832d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py311h0d48ccd_3.conda @@ -1282,7 +1800,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py311hbc40be8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py311h59bccc5_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py311hdfc3925_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py311hdfc3925_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py311ha6e1976_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py311hfef529e_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-2.1.0-nompi_h654f344_110.conda @@ -1406,7 +1924,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py311h6b0c994_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py311h1e9b377_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py311h8032f78_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py311hf303fd3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -1416,7 +1934,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py311h38be061_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py311h66caee6_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py311hc8f4f90_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py311ha6e1976_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py311ha6e1976_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py311h242c0d0_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.16-h5f976f7_2_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.1.2-py311hcbcfc3a_1.conda @@ -1433,17 +1951,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py311h2a99c40_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py311hbe70eeb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py311h194be0f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py311haee01d2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-4.2.0-hd2095e1_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py311h0d48ccd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py311h0d48ccd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py311hdf67eae_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py311h0d48ccd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py311he01d507_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py311h4b93e55_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py311h194be0f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py311h194be0f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -1499,7 +2017,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda @@ -1582,7 +2100,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.26.0-pyhd8ed1ab_0.conda @@ -1604,7 +2122,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.11.16-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda @@ -1655,8 +2173,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda @@ -1666,8 +2184,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/29/ce9c8e81a6b9be7722ac00cb592ce6b652405eecb178bf410810732957ec/epicscorelibs-7.0.10.99.0.1-cp311-cp311-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1b/dd/a9fe6a0a09512da23951c68bf36466aeecd89def3183dc095edbc807ddc5/pint-0.25.3-py3-none-any.whl @@ -1679,7 +2198,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/2e/a4/1ca765c63a60e3096ae2ad132eb25f6f7eab0377418cd0472808befd9665/matplotlib-3.11.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/e1/c1170950f3b87f18f4f54361617cf41be2fe7d56f74e00e05da14dc50cc1/pymongo-4.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl @@ -1751,7 +2269,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py312h9be0db6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py312h8b7ec3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py312h8b7ec3f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py312h5cc1888_3.conda @@ -1765,7 +2283,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py312h04c1168_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py312hda0ef95_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py312h80505a6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda @@ -1883,7 +2401,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -1893,7 +2411,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py312hf520645_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda @@ -1909,16 +2427,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py312h4ae17e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py312h5253ce2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py312hd9148b4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py312h5cc1888_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py312hcb3ea6e_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py312h1b36aeb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py312h1b36aeb_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -1973,7 +2491,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda @@ -2057,7 +2575,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda @@ -2080,7 +2598,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda @@ -2135,8 +2653,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda @@ -2146,8 +2664,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl @@ -2155,7 +2674,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/47/b45332d3d6427811909d934f8675956ee5b9e10e46f689464d6ea23ab858/pvxslibs-1.5.2-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl @@ -2231,7 +2749,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py313ha52865f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py313h2551ef2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py313h2af15c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py313h2af15c8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h995f894_3.conda @@ -2245,7 +2763,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py313ha4a6282_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py313h5cbdff7_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py313h901f96d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py313h901f96d_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py313ha296275_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py313hf402d47_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda @@ -2363,7 +2881,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py313hc6c575f_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py313h4900a6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py313hbfd7664_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py313h3b26573_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -2373,7 +2891,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py313h78bf25f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py313h98bfbea_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py313h6123c0d_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py313hd9d88b3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.15-hf47f18c_103_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py313h2139695_0.conda @@ -2389,16 +2907,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py313hb172dc5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py313he2a5f1a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py313hd42f317_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py313h995f894_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py313h995f894_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py313h7037e92_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py313h995f894_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py313ha48717d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py313h7f1de9a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py313hd42f317_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py313hd42f317_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -2453,7 +2971,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda @@ -2537,7 +3055,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda @@ -2560,7 +3078,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.15-h4df99d1_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda @@ -2615,8 +3133,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda @@ -2626,9 +3144,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - pypi: https://files.pythonhosted.org/packages/08/0f/0daf459b604f61253b914b28f0e6dd16cdc8a7aa962cf901e482f5cbba67/pymongo-4.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/b5/3515126cb6dce557cb928cc3147a77d5f84b9cade3c8548b802792e6bbe4/redis_json_dict-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl @@ -2637,7 +3156,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/2e/43/102114edc44314b669904ce51f1473137b2830bf4862dfc6d2b1ad3fcf91/confluent_kafka-2.15.1-cp313-cp313-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl @@ -2698,23 +3216,29 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.7.0-py312h3f22e6b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-h505cf86_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-h9908984_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312he9c40d5_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hebe6cf0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-3.3.4-hea3f660_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py312h9be0db6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py312h5cc1888_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.22-py312h80505a6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.3-h4db4eae_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.65.0-py312hfd18d64_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.17-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hb7133d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hfd2156b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py312h04c1168_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-py310h44b86e0_2.conda @@ -2791,7 +3315,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312hc6a72db_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-hee9eb32_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.4.0-ha2cb11d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.2-ha2cb11d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.2-py312h9be0db6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ndindex-1.10.1-py312ha6a3dbb_1.conda @@ -2800,10 +3326,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/numexpr-2.14.2-py312h0000c9d_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.5.3-py312he827f4e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-heb1ab33_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312hafd94c1_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -2812,7 +3339,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h7cc23a3_1004.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.10.0-py312h1b36aeb_1.conda @@ -2826,9 +3354,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py312h4ae17e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-18.0.0-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-h0d788c3_1.conda @@ -2845,6 +3374,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_x86_64-microarch-level-1-5_level1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.20.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/algotom-1.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-doc-0.0.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.8.0-pyhd8ed1ab_0.conda @@ -2854,12 +3385,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/awkward-2.13.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bluesky-tiled-plugins-2.0.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.5.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda @@ -2869,9 +3402,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.8.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.8.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.22.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ecdsa-0.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/event-model-1.24.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-core-0.141.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -2887,6 +3425,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.19-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imageio-2.37.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-2.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-9.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-resources-7.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda @@ -2906,6 +3445,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/lazy-loader-0.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.2.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-4.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.6.1-pyhd8ed1ab_0.conda @@ -2920,34 +3460,50 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.11.0-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.32-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.3.post1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ragged-0.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/redis-json-dict-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/redis-py-8.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.1.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sparse-0.19.2-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-9.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stamina-26.1.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tenacity-9.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/textual-8.2.8-pyhcf101f3_0.conda @@ -2965,80 +3521,50 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzlocal-5.4.4-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - - pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/26/c2/2ec71837ac862d823de280e72c245686b37de6dde0a18a02c9c18f589c39/setuptools_dso-2.12.4-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/47/b45332d3d6427811909d934f8675956ee5b9e10e46f689464d6ea23ab858/pvxslibs-1.5.2-cp312-cp312-manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/85/115246eeec396feedc81bed42f832c97bf433a803688a4bf0e5d763e808a/bluesky_queueserver_api-0.0.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/66/a3/54b9d35d9f2e6b4a2546212c1b0df4412a97c4323223a1f9ace8ee22689f/bluesky_queueserver-0.0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/5c/9507ea3c732f8a259f45ddf190a741e9bf7d3fe229b5d473917ee6efbf1e/pint-0.26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/9d/872279feea98802a9c3e5f2680cb1b38cd49d7e249cd2220d07a69c7079a/suitcase_mongo-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/d3/a3e8393cdf6e40e94b3a91ab8afdb010769829b520b2fbfdc4697f31a1c3/bluesky_kafka-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/9f/c0d4811ce61696113b7b0b2f8dfa1218c4b47caad8383ce8269555884d90/bluesky_httpserver-0.0.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/a3/31112ce258d74be4636e6d1f73717587e19fd311c70477ae807136f3a48c/confluent_kafka-2.15.1-cp312-cp312-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/c1/a314376de17d02eb3e798929114cefeea29776174dab06b8162ce65ca82d/databroker-2.0.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/45/863765ab799e3197d1863c2035bf0ce1f7e925d33a5c1d1153cc01c888e3/p4p-4.2.2-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ec/14/1c52a2a3ca612d77550fef0404a496f080bfd600cf4c7c6a2a8ab4be2dc6/pamela-1.2.0-py2.py3-none-any.whl @@ -3076,9 +3602,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.7.0-py312h3f22e6b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-h505cf86_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-h9908984_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312he9c40d5_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_10.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.8-hebe6cf0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-blosc2-3.3.4-hea3f660_1.conda @@ -3094,9 +3622,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.3-h4db4eae_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.65.0-py312hfd18d64_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.17-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hb7133d2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hfd2156b_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda @@ -3195,8 +3724,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312hc6a72db_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-hee9eb32_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.2-py312h9be0db6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ndindex-1.10.1-py312ha6a3dbb_1.conda @@ -3210,7 +3739,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -3219,7 +3748,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h7cc23a3_1004.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py312hf520645_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda @@ -3237,8 +3767,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-18.0.0-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda @@ -3289,6 +3819,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.5.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.5.0-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda @@ -3302,6 +3833,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/dateparser-1.4.3-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/dill-0.4.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.8.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/event-model-1.24.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda @@ -3360,17 +3892,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.53-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-settings-2.11.0-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.21.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pylatexenc-2.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda @@ -3382,6 +3916,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/redis-json-dict-0.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/redis-py-8.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rich-15.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-84.0.0-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-scm-10.2.3-pyh5ded981_0.conda @@ -3411,8 +3946,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzlocal-5.4.4-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-2.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uncertainties-3.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/vcs_versioning-2.3.4-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda @@ -3420,7 +3956,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl @@ -3432,24 +3968,18 @@ environments: - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/7b/55f41ec7746713db21aff5e794fc98b5a2e4c40fc72d69f6a1cdb04df360/epicscorelibs-7.0.10.99.0.1-cp312-cp312-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/68/b9/f44b139096467996b4d85589d9880fd55f0c80a204d298a1b73d0adc2654/scanspec-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6d/5c/9507ea3c732f8a259f45ddf190a741e9bf7d3fe229b5d473917ee6efbf1e/pint-0.26.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/cb/c13d8a74419dde9590ed6fab293b516f68316ac87d06569b79b5f446d519/pydantic_numpy-8.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/9d/872279feea98802a9c3e5f2680cb1b38cd49d7e249cd2220d07a69c7079a/suitcase_mongo-0.7.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/d3/a3e8393cdf6e40e94b3a91ab8afdb010769829b520b2fbfdc4697f31a1c3/bluesky_kafka-0.10.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl @@ -3506,7 +4036,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3c89d7e_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.1.1-py312h703531f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.4.0-py312h9be0db6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py312h8b7ec3f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py312h8b7ec3f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cyrus-sasl-2.1.28-hac629b4_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py312h5cc1888_3.conda @@ -3518,14 +4048,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.18.3-h4db4eae_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.65.0-py312hfd18d64_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.17-h7cc23a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hb7133d2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hfd2156b_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.3.1-py312h04c1168_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py312hda0ef95_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.15-h54a6638_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py312h80505a6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.16.0-nompi_py312hfaa9938_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h19486de_110.conda @@ -3628,8 +4158,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py312hc6a72db_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-hee9eb32_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.4.0-ha2cb11d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.2-ha2cb11d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.2-py312h9be0db6_2.conda @@ -3649,7 +4179,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.4-h781a0a9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.1-h443056b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.12.0-py312he35bb96_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.3.0-py312h38079b3_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_3.conda @@ -3659,7 +4189,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-25.0.0-py312h7900ff3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-25.0.0-py312h2054cf2_0_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycryptodome-3.23.0-py312hf189cdb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py312hf520645_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.14-h5f976f7_3_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-blosc2-4.13.1-py312h9344012_0.conda @@ -3678,17 +4208,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-image-0.26.0-np2py312h4ae17e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.18.1-py312h106c528_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py312h5253ce2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h1df4ec4_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.1.0-py312hd9148b4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-18.0.0-py312h5cc1888_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py312h5cc1888_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.2.0-py312hcb3ea6e_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.26.0-hc1c935e_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-17.1-py312h89dfda2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py312h1b36aeb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py312h1b36aeb_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-cursor-0.1.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-image-0.4.0-hb711507_2.conda @@ -3746,7 +4276,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.7.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-2.0.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-2.0.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/canonicaljson-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.7.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda @@ -3848,7 +4378,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.2.1-pyh8b19718_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-4.6.2-pyha770c72_0.conda @@ -3873,7 +4403,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-timeout-2.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.6.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.2.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.12.14-hd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-jose-3.5.0-pyhff2d567_0.conda @@ -3934,8 +4464,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.53.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.53.0-h86e8c93_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/vcs_versioning-2.3.4-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.7.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.9.1-pyhd8ed1ab_0.conda @@ -3945,7 +4475,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-4.1.0-pyhcf101f3_0.conda - pypi: ./ - pypi: git+https://github.com/NSLS2/nslsii?rev=main#93ef2e4a24892699debc56d6f9891e57dc2205d5 - - pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e + - pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa - pypi: git+https://github.com/jwlodek/ophyd-async?rev=add-codec-and-process-plugin-allow-for-filtering#d148390b321cd2d711d5de72ab07d2616c26ee39 - pypi: https://files.pythonhosted.org/packages/21/bd/4d1f59c9287ec5f93f9d879db3ac06785ba7c4d04a7120678d894e0c53d0/caproto-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/ef/3b82c9a5c6fe4bcc7ebafbc4a06efe4f631929441bddd5d53541434fb941/pyOlog-4.5.1-py3-none-any.whl @@ -4931,39 +5461,39 @@ packages: run_exports: {} size: 329540 timestamp: 1789746824998 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py311h364832d_0.conda - sha256: 94387b1c0c884496687ec94f578e415c430c5f495fb5975479fb884a4bf7241e - md5: 1bc00c3a3e8c776830a25be878f579a4 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py311h364832d_0.conda + sha256: 0dbe00acbd25fb7ced45200d044b9199d1f6a806bcecac9b6818f7bdbc4a0650 + md5: 53d6a3984dfa6b2a76abccc29ef87d50 depends: - python - - __glibc >=2.17,<3.0.a0 - libgcc >=15 + - __glibc >=2.17,<3.0.a0 - python_abi 3.11.* *_cp311 license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/coverage?source=compressed-mapping + - pkg:pypi/coverage?source=hash-mapping run_exports: {} - size: 428751 - timestamp: 1789915303358 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py312h8b7ec3f_0.conda - sha256: 4a8043346633a5324746c7f370083b3c3aa8f7c8dd5b9af50e9255238c30f57d - md5: a440583cd1dbfae9841862fee5c9e3bd + size: 427996 + timestamp: 1787965687641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py312h8b7ec3f_0.conda + sha256: aaea290a54335ad42ca4a416936c51acbd6f5be7cc2ed6c49fb629f3e8946a31 + md5: 385233507bd4b374469cc6563734c22b depends: - python - - __glibc >=2.17,<3.0.a0 - libgcc >=15 + - __glibc >=2.17,<3.0.a0 - python_abi 3.12.* *_cp312 license: Apache-2.0 license_family: APACHE purls: - - pkg:pypi/coverage?source=compressed-mapping + - pkg:pypi/coverage?source=hash-mapping run_exports: {} - size: 417037 - timestamp: 1789915303358 -- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py313h2af15c8_0.conda - sha256: 38000f8796939c8dcc4c385c52e7c00da72c7cd2cf071462275b4f3d73efee33 - md5: d57a44a18f604ad03769f340693918d9 + size: 416603 + timestamp: 1787965687641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.0-py313h2af15c8_0.conda + sha256: c95c677deaae32f2cca15f5c6e1a9267d6c0ba072f82d6667596946c5b30dff3 + md5: e53c02d7a82ec851a22628cdd241f182 depends: - python - libgcc >=15 @@ -4972,9 +5502,24 @@ packages: license: Apache-2.0 license_family: APACHE purls: + - pkg:pypi/coverage?source=hash-mapping + run_exports: {} + size: 425215 + timestamp: 1787965687641 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.16.1-py312h8b7ec3f_0.conda + sha256: 4a8043346633a5324746c7f370083b3c3aa8f7c8dd5b9af50e9255238c30f57d + md5: a440583cd1dbfae9841862fee5c9e3bd + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + purls: - pkg:pypi/coverage?source=compressed-mapping run_exports: {} - size: 426365 + size: 417037 timestamp: 1789915303358 - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-50.0.1-py311hc91d8b8_1.conda noarch: python @@ -5238,9 +5783,9 @@ packages: - libfreetype6 >=2.14.3 size: 175239 timestamp: 1786641011029 -- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.17-h7cc23a3_0.conda - sha256: 3ba79ebfbd14ca5db4affaf10ee8001064e907de5baabc2dbe5bcd9aec856c9f - md5: f9d5302bca196ee87e62f393f2626562 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-h7cc23a3_2.conda + sha256: 76dd097bd65d812a9104edb0e266386e152e65928ac5c8eb4f480ce24affc470 + md5: 16d6e5a0f8dbc17bd6669dba6f49bad3 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 @@ -5248,9 +5793,9 @@ packages: purls: [] run_exports: weak: - - fribidi >=1.0.17,<2.0a0 - size: 64102 - timestamp: 1790011855888 + - fribidi >=1.0.16,<2.0a0 + size: 62401 + timestamp: 1788385624706 - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.3.1-h54a6638_0.conda sha256: f7026323ac769ef432c747a6c22abd39c583d0d4f6985a7100fb371e2167f214 md5: 04be12e7dd050aaf364ece2393988e85 @@ -5427,13 +5972,13 @@ packages: - graphite2 >=1.3.15,<2.0a0 size: 102835 timestamp: 1786118485753 -- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py311hdfc3925_0.conda - sha256: 9226ade5e334e8928df681d7c2ced04cd2aba10c3d6ca669a0da8edb7345a5a6 - md5: 7607c7d577cec4a34616295f166594e6 +- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py311hdfc3925_2.conda + sha256: a361f5a3d83c97d15ceeae2afd51674b64d91da150577d120b46a220491f8b6d + md5: b1c861bfe35df4c7109bb3794e93d619 depends: - python - - __glibc >=2.17,<3.0.a0 - libgcc >=15 + - __glibc >=2.17,<3.0.a0 - libstdcxx >=15 - python_abi 3.11.* *_cp311 license: MIT @@ -5441,15 +5986,15 @@ packages: purls: - pkg:pypi/greenlet?source=compressed-mapping run_exports: {} - size: 281345 - timestamp: 1789809463197 -- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda - sha256: ec381ab27f211928afd0a976fbf765563a751c3a4cf2fa39174a65a783c85d0d - md5: aeb718d6d95669b4dcc686750b6c034f + size: 281355 + timestamp: 1789389652145 +- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py312h80505a6_2.conda + sha256: acac55cd05b95a05bdc4909c445ebd7ba45c08bb5577209844c82f9e3cc2a9cf + md5: 178f778819c84abf97dd8d19c6cfd0a0 depends: - python - - libgcc >=15 - libstdcxx >=15 + - libgcc >=15 - __glibc >=2.17,<3.0.a0 - python_abi 3.12.* *_cp312 license: MIT @@ -5457,24 +6002,40 @@ packages: purls: - pkg:pypi/greenlet?source=compressed-mapping run_exports: {} - size: 276831 - timestamp: 1789809465443 -- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py313h901f96d_0.conda - sha256: e59ae3c2cc292f53e31edfe411e570095e1fc9ddebf7711f764c717a51ff2f04 - md5: 2f880d08996c53af3588e071dae3ad4d + size: 276881 + timestamp: 1789389651167 +- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.5-py313h901f96d_2.conda + sha256: e27dcc94bd58b17e9692785c4154fe4a78b1408d067c886a2b40b0b41fd352ed + md5: 286dc5bb3e4485ca515dd26e55accd83 depends: - python - - __glibc >=2.17,<3.0.a0 - libgcc >=15 - libstdcxx >=15 + - __glibc >=2.17,<3.0.a0 - python_abi 3.13.* *_cp313 license: MIT license_family: MIT purls: - pkg:pypi/greenlet?source=compressed-mapping run_exports: {} - size: 277318 - timestamp: 1789809460170 + size: 277351 + timestamp: 1789389650848 +- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.6-py312h80505a6_0.conda + sha256: ec381ab27f211928afd0a976fbf765563a751c3a4cf2fa39174a65a783c85d0d + md5: aeb718d6d95669b4dcc686750b6c034f + depends: + - python + - libgcc >=15 + - libstdcxx >=15 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/greenlet?source=compressed-mapping + run_exports: {} + size: 276831 + timestamp: 1789809465443 - conda: https://conda.anaconda.org/conda-forge/linux-64/grimp-3.17-py311ha6e1976_1.conda sha256: 12d09dbaf1a169d97e0b2ad339b6df81b71b55d6b0fe157dc1f0d1332571864a md5: 4d48fe8173874712a9c6a0d11181ad27 @@ -7624,9 +8185,9 @@ packages: run_exports: {} size: 26100 timestamp: 1772445154165 -- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_1.conda - sha256: 44001a8f40445bf63e96537d041d0c40d46facb67448d039aec4fee9262f05ab - md5: bfc8bc50a9803addad9bb033bacb80f3 +- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.11.2-py312h6a12a82_0.conda + sha256: cd57e0ffcd5256c2d577ccde473f96b00e7464c2bc6192d89f2dcf91e8723858 + md5: 257bea13ef0ae63ecebd590015deba3c depends: - matplotlib-base >=3.11.2,<3.11.3.0a0 - pyside6 >=6.7.2 @@ -7638,11 +8199,11 @@ packages: purls: - pkg:pypi/matplotlib?source=compressed-mapping run_exports: {} - size: 14909 - timestamp: 1789824128347 -- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_1.conda - sha256: 066b5b0cee5edb51ad64eb0c55db99fc85a6dc92821c49531b778da1c6d9867e - md5: 0745f1af620efcb1095d8ddabcb2ed8b + size: 14933 + timestamp: 1789612556571 +- conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.11.2-py312hfe9aac7_0.conda + sha256: b2ab91315af2a9e59f3569d8e50d686d8aa4223e1a26f65567e1a57b42171ec3 + md5: 981bce85a1c78cb638ab5bac81dd4bc8 depends: - __glibc >=2.17,<3.0.a0 - contourpy >=1.0.1 @@ -7670,8 +8231,8 @@ packages: purls: - pkg:pypi/matplotlib?source=compressed-mapping run_exports: {} - size: 8946081 - timestamp: 1789824111301 + size: 9061063 + timestamp: 1789612544376 - conda: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.4.0-ha2cb11d_1.conda sha256: 3d56733fe0f44159787ad086d5e7bdab8b69e6a5a9b6b873a8beb3519f3d8d07 md5: f0f6c8691a9ed4099d1f287a444e251a @@ -8378,16 +8939,16 @@ packages: run_exports: {} size: 298256 timestamp: 1786838625986 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py311h1e9b377_0.conda - sha256: 85459025d8f749316df29ab837729155d8c54c48af2953ff6226107a7e4c1c9f - md5: 94bc77ee4421c9ff83fe336cb39f55b9 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py311h8032f78_1.conda + sha256: a6023e417a9c4c517bba4e43a28a3c9b621677aaf59017e70c6a856cc22a4202 + md5: fb92751a76609c3e96ba31d1b18f6142 depends: - python - numpy >=1.26.0 - python-dateutil >=2.8.2 - - libgcc >=15 - - libstdcxx >=15 - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 - numpy >=1.23,<3 - python_abi 3.11.* *_cp311 constrains: @@ -8432,21 +8993,21 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping run_exports: {} - size: 15318970 - timestamp: 1789803360037 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py312h91ec553_0.conda - sha256: a4c045408aea6536148307158fb873d8d8f5f1295931ea12494997eae778b096 - md5: 2143c2cef115b49a934d526f7da91696 + size: 15238790 + timestamp: 1785276229113 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py312h8ecdadd_1.conda + sha256: 393e529c0574c020a9b790275af10ff35e21cbb4e12740888bd3f214f2f07034 + md5: 85eb29ade84d1bd97be5ec55607efb00 depends: - python - numpy >=1.26.0 - python-dateutil >=2.8.2 - - libgcc >=15 - __glibc >=2.17,<3.0.a0 - - libstdcxx >=15 - - numpy >=1.25,<3 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.23,<3 - python_abi 3.12.* *_cp312 constrains: - adbc-driver-postgresql >=1.2.0 @@ -8490,21 +9051,21 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping run_exports: {} - size: 14976520 - timestamp: 1789803399961 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.6-np2py313h4900a6c_0.conda - sha256: 8a4ee0d733e2c4be00ad3c80a3bc97a28837f611e45e7f4384d061f392827841 - md5: 871596bf7cdadf8ccff03437e9b92173 + size: 14936796 + timestamp: 1785276227779 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.5-py313hbfd7664_1.conda + sha256: 3fc0e22d9c4338b2becb6a2cce230a57f94730dfb6d3be395089084411c458e3 + md5: a78866632a871f274cd2015bccdd2ca0 depends: - python - numpy >=1.26.0 - python-dateutil >=2.8.2 + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=15 - - libstdcxx >=15 - - numpy >=1.25,<3 + - libstdcxx >=14 + - numpy >=1.23,<3 - python_abi 3.13.* *_cp313 constrains: - adbc-driver-postgresql >=1.2.0 @@ -8548,10 +9109,10 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/pandas?source=compressed-mapping + - pkg:pypi/pandas?source=hash-mapping run_exports: {} - size: 15128892 - timestamp: 1789803430881 + size: 15049335 + timestamp: 1785276231848 - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-h8b3dc9c_1.conda sha256: 9ebb10a4eb3be51ae67d85c679f5a7a50cb040ed25c783e6331a225ea09991d4 md5: 06f6113cf1ff4a54b65f87ead132a5f1 @@ -8895,9 +9456,9 @@ packages: run_exports: {} size: 1681620 timestamp: 1768755547718 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py311ha6e1976_2.conda - sha256: d551f5186ae3a5cad2138358b70a345733f3f8a4d204d694c4949324f9802dba - md5: 115906f329806275341f67b598d52ef5 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py311ha6e1976_1.conda + sha256: 155f6d81262674afcc33f59b8ea2ae1be7d2ba54934f02e8f606dffa53749067 + md5: 5e2d89867688b49795363286e5d9360e depends: - python - typing-extensions >=4.6.0,!=4.7.0 @@ -8907,14 +9468,15 @@ packages: constrains: - __glibc >=2.17 license: MIT + license_family: MIT purls: - - pkg:pypi/pydantic-core?source=compressed-mapping + - pkg:pypi/pydantic-core?source=hash-mapping run_exports: {} - size: 1858528 - timestamp: 1789996097964 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_2.conda - sha256: 64ef841c88756f8d1ca9c241c0ab1585ad5a9cb8d0bfe0555c201c2ad0e3c701 - md5: f7a033135df967ea983fe4833856d2b1 + size: 1858432 + timestamp: 1787928981213 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py312hc767a74_1.conda + sha256: a805a64e0e7e5ed22e8f49f0038a5d8cab4b96cf094c8822da75d6823f2377e8 + md5: 2854d36e093d53d0cf81ec433ffd9c4c depends: - python - typing-extensions >=4.6.0,!=4.7.0 @@ -8924,28 +9486,30 @@ packages: constrains: - __glibc >=2.17 license: MIT + license_family: MIT purls: - - pkg:pypi/pydantic-core?source=compressed-mapping + - pkg:pypi/pydantic-core?source=hash-mapping run_exports: {} - size: 1877770 - timestamp: 1789996105430 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_2.conda - sha256: 45d77cd9afc4ec61192ecf97127d6afb56bd562692162ab13fe84f987206bf20 - md5: 02307b4e01607c72fab0f5726c96d3c2 + size: 1877711 + timestamp: 1787928982283 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.5-py313ha296275_1.conda + sha256: ba744cf30856ccda82fa4dca2058294fd3574c0f36e737b16411b49eaa5f24cd + md5: 41286094042803a6cd590db9d13f5aa1 depends: - python - typing-extensions >=4.6.0,!=4.7.0 - - __glibc >=2.17,<3.0.a0 - libgcc >=15 + - __glibc >=2.17,<3.0.a0 - python_abi 3.13.* *_cp313 constrains: - __glibc >=2.17 license: MIT + license_family: MIT purls: - - pkg:pypi/pydantic-core?source=compressed-mapping + - pkg:pypi/pydantic-core?source=hash-mapping run_exports: {} - size: 1877183 - timestamp: 1789996106423 + size: 1877111 + timestamp: 1787928978287 - conda: https://conda.anaconda.org/conda-forge/linux-64/pyside6-6.11.2-py311h242c0d0_1.conda sha256: 13d65a479bb6429875770485a86cb5c5987efc1877d225062d27f70dfae9365e md5: 65fffae90f6781d3d875536e54f7000f @@ -9792,14 +10356,14 @@ packages: - snappy >=1.2.2,<1.3.0a0 size: 45829 timestamp: 1762948049098 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py311h194be0f_1.conda - sha256: 307ce8017bc357ea724d28c62703f8da7799edb46376211086b9b4d1b45931e4 - md5: aace28faaca43eacde40f1e8e684fd77 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py311haee01d2_0.conda + sha256: 550a053398cfa6d6b08d0e14afbce11efa237d145b041b82aa098e22c998595d + md5: e49f2c35799ede9a0f47fb664327a275 depends: - python - - greenlet >=1 - - typing_extensions >=4.6.0 - - libgcc >=15 + - greenlet !=0.4.17 + - typing-extensions >=4.6.0 + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - python_abi 3.11.* *_cp311 license: MIT @@ -9807,42 +10371,59 @@ packages: purls: - pkg:pypi/sqlalchemy?source=compressed-mapping run_exports: {} - size: 3802871 - timestamp: 1789931141286 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda - sha256: df55fca9f5bf05dd15a1071ba4181445d9a04b8f8da94452d04059d551cd1e0e - md5: 0b76c8690c2b042e1ddfbc307dfcaf25 + size: 3779949 + timestamp: 1786535232010 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py312h5253ce2_0.conda + sha256: c6677580087354e02e98fe3d356ca3de3d6f0521da831ea949b7a42e52bfd60d + md5: 3d301c6754633aacf499ac6ee63bd2b8 depends: - python - - greenlet >=1 - - typing_extensions >=4.6.0 - - libgcc >=15 + - greenlet !=0.4.17 + - typing-extensions >=4.6.0 - __glibc >=2.17,<3.0.a0 + - libgcc >=14 - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/sqlalchemy?source=compressed-mapping run_exports: {} - size: 3746826 - timestamp: 1789931149339 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py313hd42f317_1.conda - sha256: b278c7e7444644e0ec5189655248bb9975c8303a4737e7d4aee89e0181e549cd - md5: 3179bc149b1742984e13c60b640663e9 + size: 3721699 + timestamp: 1786535232010 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.52-py313h54dd161_0.conda + sha256: 94273d030da802fc3ffd356f9c336cde94d401f0bcdfc59087607d84d2926047 + md5: a48af6c3d6d0c2ea106bd1d0e944f1fe + depends: + - python + - greenlet !=0.4.17 + - typing-extensions >=4.6.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/sqlalchemy?source=compressed-mapping + run_exports: {} + size: 3863012 + timestamp: 1786535232010 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.54-py312h1b36aeb_1.conda + sha256: df55fca9f5bf05dd15a1071ba4181445d9a04b8f8da94452d04059d551cd1e0e + md5: 0b76c8690c2b042e1ddfbc307dfcaf25 depends: - python - greenlet >=1 - typing_extensions >=4.6.0 - libgcc >=15 - __glibc >=2.17,<3.0.a0 - - python_abi 3.13.* *_cp313 + - python_abi 3.12.* *_cp312 license: MIT license_family: MIT purls: - pkg:pypi/sqlalchemy?source=compressed-mapping run_exports: {} - size: 3888039 - timestamp: 1789931143461 + size: 3746826 + timestamp: 1789931149339 - conda: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-4.2.0-hd2095e1_1.conda sha256: 09c242d480632acca31f2a510c9a9f65bb8cca1e82b73d2d0261ec837fff48be md5: 3bd5f5f3f6f6431b9f19b35ad4a8ed21 @@ -9875,9 +10456,9 @@ packages: - tk >=8.6.13,<8.7.0a0 size: 3566806 timestamp: 1787272857910 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py311h0d48ccd_0.conda - sha256: 770b9c1537d67859c2f591c461f5b6929fe1d8531c63ab6da568ad7d706621f8 - md5: 59c42333f75723bea30f200f0cd68228 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py311h0d48ccd_2.conda + sha256: 48182fc1a129a48c2617214b7f2c55c9096c7c2ee750d59133390c898504fe95 + md5: f9f309d3b0210c55683fc68fd0aeac98 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 @@ -9888,11 +10469,11 @@ packages: purls: - pkg:pypi/tornado?source=compressed-mapping run_exports: {} - size: 917252 - timestamp: 1789911064889 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py312h5cc1888_0.conda - sha256: f895c3ce9ca97435f08b376a4978df10815d0f3c3531feabbe668858b41e250f - md5: b0414832500d750321eddce3822bea7b + size: 889311 + timestamp: 1789076973947 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py312h5cc1888_2.conda + sha256: 01ffcb9966caa35a344216a89f37a0bc7b281cd6bf50b803d55a9f6bc04cb072 + md5: 7a327c3f25e34e434b681906f47e53bc depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 @@ -9903,11 +10484,11 @@ packages: purls: - pkg:pypi/tornado?source=compressed-mapping run_exports: {} - size: 895392 - timestamp: 1789911035492 -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.10-py313h995f894_0.conda - sha256: 66d85753b02254ae528eed5c1908829c685f1c33fb3b58b023bf2bc7557a7cd0 - md5: 94985bd39e52fedc2270adb00e4779e6 + size: 867409 + timestamp: 1789076971563 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.8-py313h995f894_2.conda + sha256: 21e501e55dc68358289d6286ee59a33db7abb3a74a2cae25ab1147e0d10b73f3 + md5: 133fcb5368568442d345d8b5c5151ff8 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 @@ -9918,8 +10499,8 @@ packages: purls: - pkg:pypi/tornado?source=compressed-mapping run_exports: {} - size: 924176 - timestamp: 1789911065267 + size: 894107 + timestamp: 1789076977398 - conda: https://conda.anaconda.org/conda-forge/linux-64/ty-0.0.69-h4e94fc0_0.conda noarch: python sha256: cb674ec728a1fd03d8d795e1b53120bd9de7b2eded79cfb79e1ccc02a1b52a44 @@ -9990,9 +10571,9 @@ packages: run_exports: {} size: 14910 timestamp: 1769438729201 -- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-18.0.0-py312h5cc1888_0.conda - sha256: 3b0c8ea41cd161043eb774bcb653e497811e4e6307e5149da7072c4fd70d356e - md5: 070e15c0fbb80b2f2fe405419b763f68 +- conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h5cc1888_1.conda + sha256: c8cb48db10add790655cc28c163adec56312fd176934f206b0da62bbb64902c5 + md5: a6301c668e50512c804c5b3807c960f2 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=15 @@ -10001,10 +10582,10 @@ packages: license: Apache-2.0 license_family: Apache purls: - - pkg:pypi/unicodedata2?source=compressed-mapping + - pkg:pypi/unicodedata2?source=hash-mapping run_exports: {} - size: 419998 - timestamp: 1789928085789 + size: 414021 + timestamp: 1788554113694 - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.22.1-py311h0d48ccd_2.conda sha256: 83bff0738a975a5301e84176ab2e0151bcd7e22102db2ef8caaf846dc149b09a md5: 9ad72850f7ddcabe9736c9aee7307b05 @@ -10166,48 +10747,65 @@ packages: run_exports: {} size: 421068 timestamp: 1788618424663 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py311h194be0f_0.conda - sha256: ab721e89ba4eb5023480b71aa18eac112295322a0fb21041248ed8e831860758 - md5: 0dc74d7ba694c1a362f4142392d18d4f +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py311h194be0f_2.conda + sha256: 3b88876347ff414c3e65b0b395f2545b819f899e25e9822fd7a4f01e0690e3ab + md5: 5e403bdcdd7729123bb62edfe38abda1 depends: - python - libgcc >=15 - __glibc >=2.17,<3.0.a0 - python_abi 3.11.* *_cp311 license: BSD-2-Clause + license_family: BSD purls: - pkg:pypi/wrapt?source=compressed-mapping run_exports: {} - size: 147870 - timestamp: 1789986014542 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py312h1b36aeb_0.conda - sha256: b07ed016c0befa20362bbd1b41cd6d730407f27f171ad15811347741a9553bb7 - md5: d06605fe1a45b11cb4f21d2438a3e3a9 + size: 145206 + timestamp: 1789161061090 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py312h1b36aeb_2.conda + sha256: 0ab5f935929b386b0f22926b81c923bffb69a91d16137f292f6e61c971067c7a + md5: 1c6a986ce95c1919e83c4a8650891e38 depends: - python - - __glibc >=2.17,<3.0.a0 - libgcc >=15 + - __glibc >=2.17,<3.0.a0 - python_abi 3.12.* *_cp312 license: BSD-2-Clause + license_family: BSD purls: - pkg:pypi/wrapt?source=compressed-mapping run_exports: {} - size: 145019 - timestamp: 1789986016000 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py313hd42f317_0.conda - sha256: 03a858fa56874f87c3181234c305e2ec83867b6b806c5fc85e9cc102fa0089bd - md5: 10ba77b688ad23f452400ce55fe63236 + size: 141802 + timestamp: 1789161059029 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.0-py313hd42f317_2.conda + sha256: 1bd4b9bfac89111f4be7948f61629abd16a158b43b0efa453ace0d903481bb5e + md5: d872d1da2011ebdd02eb3d88aabf2e7b depends: - python - libgcc >=15 - __glibc >=2.17,<3.0.a0 - python_abi 3.13.* *_cp313 license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=compressed-mapping + run_exports: {} + size: 144538 + timestamp: 1789161048815 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-2.4.1-py312h1b36aeb_0.conda + sha256: b07ed016c0befa20362bbd1b41cd6d730407f27f171ad15811347741a9553bb7 + md5: d06605fe1a45b11cb4f21d2438a3e3a9 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=15 + - python_abi 3.12.* *_cp312 + license: BSD-2-Clause purls: - pkg:pypi/wrapt?source=compressed-mapping run_exports: {} - size: 148000 - timestamp: 1789986017258 + size: 145019 + timestamp: 1789986016000 - conda: https://conda.anaconda.org/conda-forge/linux-64/xcb-util-0.4.1-h4f16b4b_2.conda sha256: ad8cab7e07e2af268449c2ce855cbb51f43f4664936eff679b1f3862e6e4b01d md5: fdc27cb255a7a2cc73b7919a968b48f0 @@ -11135,6 +11733,18 @@ packages: run_exports: {} size: 16597 timestamp: 1787678456483 +- conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.1.8-pyhd8ed1ab_0.conda + sha256: 5287481ba685f37fb1ff514dfb27e599cb89d665569007651620642f9505650e + md5: 14a1c0682312ba585d88dacdd21f3358 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cachetools?source=compressed-mapping + run_exports: {} + size: 21776 + timestamp: 1788215593744 - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-7.2.0-pyhd8ed1ab_0.conda sha256: f437fbd00e7a357e854b1d59720bb56c5444c427aa3967d9e4a8767c95eeb438 md5: 3d61ab93a58be28d1b0036416d46770b @@ -12649,9 +13259,9 @@ packages: run_exports: {} size: 1199593 timestamp: 1785914492234 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.11-pyh5ded981_0.conda - sha256: ba9e3a7ba041b569fb1c369ad5d16e47d79a0768b7a3a9bf469a5730c1c90cd8 - md5: 8fbbcac647ae8d387f011d2f0fe9041c +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.11.8-pyh5ded981_0.conda + sha256: 8cbae1fed8437e63c9bcc691927402edb3809fccb26b15eb095df7dd2eb73e5f + md5: 48e9ecf08b0578a4313f13b2e2b0fd72 depends: - python >=3.11 - python @@ -12660,8 +13270,8 @@ packages: purls: - pkg:pypi/platformdirs?source=compressed-mapping run_exports: {} - size: 28536 - timestamp: 1789914745574 + size: 27718 + timestamp: 1788959137036 - conda: https://conda.anaconda.org/conda-forge/noarch/plotext-5.3.2-pyhff2d567_0.conda sha256: b3fd4a4526ce0bd0d24c287d54f56fa8caaa3f7190163421db031de07b422c9f md5: 9e160af9c896f5dccd43815a226a26c8 @@ -13012,6 +13622,20 @@ packages: run_exports: {} size: 233310 timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.0-pyhcf101f3_0.conda + sha256: e48089f2927811994b916d31953563097bc7e8d856965fbdeea3b4d407e3b89f + md5: e87738e32eaf5dfa98a5d49f611d6312 + depends: + - python >=3.10 + - filelock >=3.15.4 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/python-discovery?source=compressed-mapping + run_exports: {} + size: 38928 + timestamp: 1787953569064 - conda: https://conda.anaconda.org/conda-forge/noarch/python-discovery-1.6.1-pyh5ded981_0.conda sha256: 396bf7b5a62eb3a12d20e16962acf36f9018332b2842d29548399df03b9deaa4 md5: ae0feb6a3f297952bd4f4270fe77f530 @@ -14030,6 +14654,24 @@ packages: run_exports: {} size: 89186 timestamp: 1788463774792 +- conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.7.9-pyh5ded981_0.conda + sha256: ca02653d02c45de60dec25400d0a11f2edc07a3dd191e7e1f5c3b4a454e27c8b + md5: 861471c1a801f8ee06568ffb6ec26cb7 + depends: + - python >=3.11 + - distlib >=0.3.7,<1 + - filelock >=3.24.2,<4 + - platformdirs >=3.9.1,<5 + - python-discovery >=1.6 + - typing_extensions >=4.13.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/virtualenv?source=compressed-mapping + run_exports: {} + size: 3895514 + timestamp: 1788948703859 - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-21.9.0-pyh5ded981_0.conda sha256: 568a27218e2494f812401eb4a7e24ad88ead88ccb2b76e55984b9660ffe50bd1 md5: e5f0d028a80952fd4c500bb96da6f398 @@ -14046,19 +14688,19 @@ packages: run_exports: {} size: 3991037 timestamp: 1789975521980 -- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.4-pyh5ded981_0.conda - sha256: cfd36e1495feebbe2b9dfae35989fea057913160c0e94ffb836c146b3e34e683 - md5: cc58f30689ed64810d19777174d877f4 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.8.3-pyhcf101f3_0.conda + sha256: 3b0599d59d70bbe792702d1b5404ac501c9c25bd7b2fd9ccbb5da8ad6587a703 + md5: f62991f907a9830ab003b12d07dd3478 depends: - - python >=3.11 + - python >=3.10 - python license: MIT license_family: MIT purls: - pkg:pypi/wcwidth?source=compressed-mapping run_exports: {} - size: 141589 - timestamp: 1789858677424 + size: 146490 + timestamp: 1788174996551 - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.48.0-pyhd8ed1ab_0.conda sha256: ba64b29b6d418024cb565081a1799262cb2780d2534ff4c14f76aa858c4286cd md5: 9a2124517bfd5d792e0f7b86eefdfeb8 @@ -14223,6 +14865,7 @@ packages: - bluesky-queueserver-api ; extra == 'gui' - bluesky-tiled-plugins ; extra == 'gui' - bluesky-widgets ; extra == 'gui' + - docstring-parser ; extra == 'gui' - matplotlib ; extra == 'gui' - ntnda-qt-viewer @ git+https://github.com/jwlodek/ntnda-qt-viewer@main ; extra == 'gui' - pyside6 ; extra == 'gui' @@ -14261,9 +14904,9 @@ packages: - requests - shortuuid requires_python: '>=3.11' -- pypi: git+https://github.com/bluesky/bluesky?rev=main#8bccfde7875aeb35748d3c9597ece32ce39ccd1e +- pypi: git+https://github.com/bluesky/bluesky?rev=main#1d0640485b94e451a9c05909e347d861999114aa name: bluesky - version: 1.15.2.dev213+g8bccfde78 + version: 1.15.2.dev230+g1d0640485 requires_dist: - cycler - event-model>=1.24.0 @@ -14280,7 +14923,7 @@ packages: - cloudpickle ; extra == 'dev' - copier ; extra == 'dev' - coverage ; extra == 'dev' - - databroker ; extra == 'dev' + - databroker==2.0.1 ; extra == 'dev' - doct ; extra == 'dev' - doctr ; extra == 'dev' - flake8 ; extra == 'dev' @@ -14350,9 +14993,9 @@ packages: - zict<3 ; extra == 'old-persistentdict' - bluesky[cmd,common,dev,ipython,olog,plotting,streamz,tools,zmq] ; extra == 'all' requires_python: '>=3.10' -- pypi: git+https://github.com/jwlodek/ntnda-qt-viewer?rev=main#1291f36ad13f54152d1607a4622cc9f912e0d0cb +- pypi: git+https://github.com/jwlodek/ntnda-qt-viewer#5e55d709f47876132a6e9f9413d423ba72a3fef4 name: ntnda-qt-viewer - version: 0.0.2 + version: 0.0.3.dev2+g5e55d709f requires_dist: - p4p>=4.2.2 - pyqtgraph>=0.14.0 @@ -14387,21 +15030,6 @@ packages: - pyqt6 ; extra == 'demo' - pytest ; extra == 'demo' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl - name: imagesize - version: 2.0.1 - sha256: ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02 - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - name: docutils - version: 0.22.4 - sha256: d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl - name: roman-numerals - version: 4.1.0 - sha256: 647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/08/0f/0daf459b604f61253b914b28f0e6dd16cdc8a7aa962cf901e482f5cbba67/pymongo-4.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: pymongo version: 4.18.1 @@ -14431,17 +15059,13 @@ packages: - pytest>=8.2 ; extra == 'test' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl - name: sphinxcontrib-htmlhelp - version: 2.1.0 - sha256: 166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 +- pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d requires_dist: - - ruff==0.5.5 ; extra == 'lint' - - mypy ; extra == 'lint' - - types-docutils ; extra == 'lint' - - sphinx>=5 ; extra == 'standalone' - - pytest ; extra == 'test' - - html5lib ; extra == 'test' + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/12/29/ce9c8e81a6b9be7722ac00cb592ce6b652405eecb178bf410810732957ec/epicscorelibs-7.0.10.99.0.1-cp311-cp311-manylinux2014_x86_64.whl name: epicscorelibs @@ -14894,18 +15518,6 @@ packages: requires_dist: - setuptools requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - name: sphinxcontrib-qthelp - version: 2.0.0 - sha256: b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb - requires_dist: - - ruff==0.5.5 ; extra == 'lint' - - mypy ; extra == 'lint' - - types-docutils ; extra == 'lint' - - sphinx>=5 ; extra == 'standalone' - - pytest ; extra == 'test' - - defusedxml>=0.7.1 ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl name: flexcache version: '0.3' @@ -15268,25 +15880,6 @@ packages: version: 4.0.16 sha256: a31a8774885b96fe825462f5d6496166f0c7cae111195b6465c801d230eb5a4e requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - name: sphinxcontrib-devhelp - version: 2.0.0 - sha256: aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 - requires_dist: - - ruff==0.5.5 ; extra == 'lint' - - mypy ; extra == 'lint' - - types-docutils ; extra == 'lint' - - sphinx>=5 ; extra == 'standalone' - - pytest ; extra == 'test' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl - name: pyparsing - version: 3.3.3 - sha256: ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4 - requires_dist: - - railroad-diagrams ; extra == 'diagrams' - - jinja2 ; extra == 'diagrams' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/3a/47/b45332d3d6427811909d934f8675956ee5b9e10e46f689464d6ea23ab858/pvxslibs-1.5.2-cp312-cp312-manylinux2014_x86_64.whl name: pvxslibs version: 1.5.2 @@ -15295,17 +15888,6 @@ packages: - setuptools-dso>=2.7a1 - epicscorelibs>=7.0.10.99.0.1,<7.0.10.99.1 requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl - name: alembic - version: 1.20.0 - sha256: 77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d - requires_dist: - - sqlalchemy>=2.0 - - mako - - typing-extensions>=4.12 - - tomli ; python_full_version < '3.11' - - tzdata ; extra == 'tz' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/3f/e1/c1170950f3b87f18f4f54361617cf41be2fe7d56f74e00e05da14dc50cc1/pymongo-4.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: pymongo version: 4.18.1 @@ -15335,11 +15917,6 @@ packages: - pytest>=8.2 ; extra == 'test' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl - name: snowballstemmer - version: 3.1.1 - sha256: 7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 - requires_python: '>=3.3' - pypi: https://files.pythonhosted.org/packages/4e/f6/71d6ec9f18da0b2201287ce9db6afb1a1f637dedb3f0703409558981c723/ldap3-2.9.1-py2.py3-none-any.whl name: ldap3 version: 2.9.1 @@ -15355,15 +15932,6 @@ packages: - setuptools-dso>=2.11a2 - numpy requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - name: ecdsa - version: 0.19.2 - sha256: 840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399 - requires_dist: - - six>=1.9.0 - - gmpy ; extra == 'gmpy' - - gmpy2 ; extra == 'gmpy2' - requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' - pypi: https://files.pythonhosted.org/packages/51/89/da0f32b679b8769dd472eb2927308d328bf75c296629fd9f95135afacd0a/aioca-2.1-py3-none-any.whl name: aioca version: '2.1' @@ -15372,17 +15940,6 @@ packages: - numpy - epicscorelibs>=7.0.3.99.4.0 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - name: sphinxcontrib-serializinghtml - version: 2.0.0 - sha256: 6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 - requires_dist: - - ruff==0.5.5 ; extra == 'lint' - - mypy ; extra == 'lint' - - types-docutils ; extra == 'lint' - - sphinx>=5 ; extra == 'standalone' - - pytest ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5d/32/6e723c9fa2847ba268c02377f75a4ae3432ec82fcf1e9776e2b9ca5f18e7/suitcase_utils-0.5.3-py3-none-any.whl name: suitcase-utils version: 0.5.3 @@ -15396,17 +15953,6 @@ packages: - caproto ; extra == 'test-fixtures' - ophyd ; extra == 'test-fixtures' - pytest>=3.9 ; extra == 'test-fixtures' -- pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - name: sphinxcontrib-applehelp - version: 2.0.0 - sha256: 4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 - requires_dist: - - ruff==0.5.5 ; extra == 'lint' - - mypy ; extra == 'lint' - - types-docutils ; extra == 'lint' - - sphinx>=5 ; extra == 'standalone' - - pytest ; extra == 'test' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5f/85/115246eeec396feedc81bed42f832c97bf433a803688a4bf0e5d763e808a/bluesky_queueserver_api-0.0.13-py3-none-any.whl name: bluesky-queueserver-api version: 0.0.13 @@ -15416,11 +15962,6 @@ packages: - websockets - httpx requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - name: pycryptodome - version: 3.23.0 - sha256: c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 - requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' - pypi: https://files.pythonhosted.org/packages/60/3b/e93d177001ac3ba3cb2ebed7727442e1ee61990271f2bf39caef63798ead/nose2-0.16.0-py3-none-any.whl name: nose2 version: 0.16.0 @@ -15443,24 +15984,6 @@ packages: version: 0.2.15 sha256: 11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl - name: rsa - version: 4.9.1 - sha256: 68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 - requires_dist: - - pyasn1>=0.1.3 - requires_python: '>=3.6,<4' -- pypi: https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - name: greenlet - version: 3.5.6 - sha256: 975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1 - requires_dist: - - sphinx ; extra == 'docs' - - furo ; extra == 'docs' - - objgraph ; extra == 'test' - - psutil ; extra == 'test' - - setuptools ; extra == 'test' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/66/a3/54b9d35d9f2e6b4a2546212c1b0df4412a97c4323223a1f9ace8ee22689f/bluesky_queueserver-0.0.25-py3-none-any.whl name: bluesky-queueserver version: 0.0.25 @@ -15590,11 +16113,6 @@ packages: - uncertainties>=3.1.6 ; extra == 'uncertainties' - xarray ; extra == 'xarray' requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: charset-normalizer - version: 3.5.1 - sha256: b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 - requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/72/89/0265b2b79424ed05b8d1e9c8fca71e1b150478e5b0c19aa50b0ae397326e/velocity_profile-1.0.0-py3-none-any.whl name: velocity-profile version: 1.0.0 @@ -15613,59 +16131,6 @@ packages: - scanspec ; extra == 'dev' - pydantic<2.0 ; extra == 'dev' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl - name: sphinx - version: 9.1.0 - sha256: c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978 - requires_dist: - - sphinxcontrib-applehelp>=1.0.7 - - sphinxcontrib-devhelp>=1.0.6 - - sphinxcontrib-htmlhelp>=2.0.6 - - sphinxcontrib-jsmath>=1.0.1 - - sphinxcontrib-qthelp>=1.0.6 - - sphinxcontrib-serializinghtml>=1.1.9 - - jinja2>=3.1 - - pygments>=2.17 - - docutils>=0.21,<0.23 - - snowballstemmer>=2.2 - - babel>=2.13 - - alabaster>=0.7.14 - - imagesize>=1.3 - - requests>=2.30.0 - - roman-numerals>=1.0.0 - - packaging>=23.0 - - colorama>=0.4.6 ; sys_platform == 'win32' - requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl - name: uvicorn - version: 0.53.0 - sha256: e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e - requires_dist: - - click>=7.0 - - h11>=0.8 - - typing-extensions>=4.0 ; python_full_version < '3.11' - - httptools>=0.8.0 ; extra == 'standard' - - python-dotenv>=0.13 ; extra == 'standard' - - pyyaml>=5.1 ; extra == 'standard' - - uvloop>=0.15.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'standard' - - watchfiles>=0.20 ; extra == 'standard' - - websockets>=13.0 ; extra == 'standard' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - name: babel - version: 2.18.0 - sha256: e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 - requires_dist: - - pytz>=2015.7 ; python_full_version < '3.9' - - tzdata ; sys_platform == 'win32' and extra == 'dev' - - backports-zoneinfo ; python_full_version < '3.9' and extra == 'dev' - - freezegun~=1.0 ; extra == 'dev' - - jinja2>=3.0 ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest>=6.0 ; extra == 'dev' - - pytz ; extra == 'dev' - - setuptools ; extra == 'dev' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/7c/60/998d447c893f31d8967c6e17cd018423bde995602240bf72b8f0aa58fd83/bluesky_widgets-0.0.18-py3-none-any.whl name: bluesky-widgets version: 0.0.18 @@ -15709,11 +16174,6 @@ packages: - setuptools-dso>=2.11a2 - numpy requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl - name: alabaster - version: 1.0.0 - sha256: fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: kiwisolver version: 1.5.1 @@ -15730,22 +16190,6 @@ packages: - ruamel-yaml>=0.18.5,<0.19.0 - semver>=3.0.1,<4.0.0 requires_python: '>=3.10,<3.14' -- pypi: https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl - name: urllib3 - version: 2.8.0 - sha256: 0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 - requires_dist: - - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' - - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' - - h2>=4,<5 ; extra == 'h2' - - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl - name: pyasn1 - version: 0.6.4 - sha256: deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/9b/5d/f25ac7d4fb77cbd53ddc6d05d833c6bf52b12770a44fa9a447eed470ca9a/msgpack_numpy-0.4.8-py2.py3-none-any.whl name: msgpack-numpy version: 0.4.8 @@ -15773,18 +16217,6 @@ packages: - numpy<3 - qtpy ; extra == 'qt' requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - name: requests - version: 2.34.2 - sha256: 2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 - requires_dist: - - charset-normalizer>=2,<4 - - idna>=2.5,<4 - - urllib3>=1.26,<3 - - certifi>=2023.5.7 - - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - - chardet>=3.0.2,<8 ; extra == 'use-chardet-on-py3' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a1/e6/e48cf0a272a5d4d17a09d44f92727e67f975ddfa94acc8464763d19a654d/fonttools-4.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: fonttools version: 4.65.0 @@ -15823,16 +16255,6 @@ packages: name: ply version: '3.11' sha256: 096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce -- pypi: https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl - name: mako - version: 1.4.1 - sha256: a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617 - requires_dist: - - markupsafe>=2.0 - - pytest ; extra == 'testing' - - babel ; extra == 'babel' - - lingua>=4.16 ; extra == 'lingua' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl name: tqdm version: 4.70.1 @@ -15847,6 +16269,17 @@ packages: - envwrap ; extra == 'telegram' - ipywidgets>=6 ; extra == 'notebook' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl + name: docstring-parser + version: 0.18.0 + sha256: b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b + requires_dist: + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'dev' + - pytest ; extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: matplotlib version: 3.11.2 @@ -15942,33 +16375,6 @@ packages: - pyparsing>=3 - python-dateutil>=2.7 requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - name: dnspython - version: 2.8.0 - sha256: 01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af - requires_dist: - - black>=25.1.0 ; extra == 'dev' - - coverage>=7.0 ; extra == 'dev' - - flake8>=7 ; extra == 'dev' - - hypercorn>=0.17.0 ; extra == 'dev' - - mypy>=1.17 ; extra == 'dev' - - pylint>=3 ; extra == 'dev' - - pytest-cov>=6.2.0 ; extra == 'dev' - - pytest>=8.4 ; extra == 'dev' - - quart-trio>=0.12.0 ; extra == 'dev' - - sphinx-rtd-theme>=3.0.0 ; extra == 'dev' - - sphinx>=8.2.0 ; extra == 'dev' - - twine>=6.1.0 ; extra == 'dev' - - wheel>=0.45.0 ; extra == 'dev' - - cryptography>=45 ; extra == 'dnssec' - - h2>=4.2.0 ; extra == 'doh' - - httpcore>=1.0.0 ; extra == 'doh' - - httpx>=0.28.0 ; extra == 'doh' - - aioquic>=1.2.0 ; extra == 'doq' - - idna>=3.10 ; extra == 'idna' - - trio>=0.30 ; extra == 'trio' - - wmi>=1.5.1 ; sys_platform == 'win32' and extra == 'wmi' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/bc/68/c45fbb7bfe21b81ccf923b02549e2b1504539dacdbbb3444c01aa00c2ee2/pvxslibs-1.5.2-cp313-cp313-manylinux2014_x86_64.whl name: pvxslibs version: 1.5.2 @@ -15989,27 +16395,6 @@ packages: version: 1.0.13 sha256: a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl - name: openpyxl - version: 3.1.5 - sha256: 5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 - requires_dist: - - et-xmlfile - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl - name: et-xmlfile - version: 2.0.0 - sha256: 7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl - name: sphinxcontrib-jsmath - version: 1.0.1 - sha256: 2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 - requires_dist: - - pytest ; extra == 'test' - - flake8 ; extra == 'test' - - mypy ; extra == 'test' - requires_python: '>=3.5' - pypi: https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl name: ipywidgets version: 8.1.9 @@ -16026,20 +16411,6 @@ packages: - pytest-cov ; extra == 'test' - pytz ; extra == 'test' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl - name: starlette - version: 1.6.0 - sha256: a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c - requires_dist: - - anyio>=3.6.2,<5 - - typing-extensions>=4.10.0 ; python_full_version < '3.13' - - httpx2>=2.0.0 ; extra == 'full' - - httpx>=0.27.0,<0.29.0 ; extra == 'full' - - itsdangerous ; extra == 'full' - - jinja2 ; extra == 'full' - - python-multipart>=0.0.18 ; extra == 'full' - - pyyaml ; extra == 'full' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl name: opentelemetry-api version: 1.44.0 @@ -16085,44 +16456,6 @@ packages: - uvicorn - typing-extensions ; python_full_version < '3.8' requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl - name: fastapi - version: 0.141.1 - sha256: bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3 - requires_dist: - - starlette>=0.46.0 - - pydantic>=2.9.0 - - typing-extensions>=4.8.0 - - typing-inspection>=0.4.2 - - annotated-doc>=0.0.2 - - fastapi-cli[standard]>=0.0.32 ; extra == 'standard' - - fastar>=0.9.0 ; extra == 'standard' - - httpx>=0.23.0,<1.0.0 ; extra == 'standard' - - jinja2>=3.1.5 ; extra == 'standard' - - python-multipart>=0.0.18 ; extra == 'standard' - - email-validator>=2.0.0 ; extra == 'standard' - - uvicorn[standard]>=0.12.0 ; extra == 'standard' - - pydantic-settings>=2.0.0 ; extra == 'standard' - - pydantic-extra-types>=2.0.0 ; extra == 'standard' - - fastapi-cli[standard-no-fastapi-cloud-cli]>=0.0.32 ; extra == 'standard-no-fastapi-cloud-cli' - - httpx>=0.23.0,<1.0.0 ; extra == 'standard-no-fastapi-cloud-cli' - - jinja2>=3.1.5 ; extra == 'standard-no-fastapi-cloud-cli' - - python-multipart>=0.0.18 ; extra == 'standard-no-fastapi-cloud-cli' - - email-validator>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' - - uvicorn[standard]>=0.12.0 ; extra == 'standard-no-fastapi-cloud-cli' - - pydantic-settings>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' - - pydantic-extra-types>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' - - fastapi-cli[standard]>=0.0.32 ; extra == 'all' - - httpx>=0.23.0,<1.0.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - python-multipart>=0.0.18 ; extra == 'all' - - itsdangerous>=1.1.0 ; extra == 'all' - - pyyaml>=5.3.1 ; extra == 'all' - - email-validator>=2.0.0 ; extra == 'all' - - uvicorn[standard]>=0.12.0 ; extra == 'all' - - pydantic-settings>=2.0.0 ; extra == 'all' - - pydantic-extra-types>=2.0.0 ; extra == 'all' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl name: opencv-python version: 5.0.0.93 @@ -16469,58 +16802,6 @@ packages: - setuptools-dso>=2.7a1 - epicscorelibs>=7.0.10.99.0.1,<7.0.10.99.1 requires_python: '>=2.7' -- pypi: https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: sqlalchemy - version: 2.0.54 - sha256: 68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19 - requires_dist: - - importlib-metadata ; python_full_version < '3.8' - - greenlet>=1 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' - - typing-extensions>=4.6.0 - - greenlet>=1 ; extra == 'asyncio' - - mypy>=0.910 ; extra == 'mypy' - - pyodbc ; extra == 'mssql' - - pymssql ; extra == 'mssql-pymssql' - - pyodbc ; extra == 'mssql-pyodbc' - - mysqlclient>=1.4.0 ; extra == 'mysql' - - mysql-connector-python ; extra == 'mysql-connector' - - mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10 ; extra == 'mariadb-connector' - - cx-oracle>=8 ; extra == 'oracle' - - oracledb>=1.0.1 ; extra == 'oracle-oracledb' - - psycopg2>=2.7 ; extra == 'postgresql' - - pg8000>=1.29.1 ; extra == 'postgresql-pg8000' - - greenlet>=1 ; extra == 'postgresql-asyncpg' - - asyncpg ; extra == 'postgresql-asyncpg' - - psycopg2-binary ; extra == 'postgresql-psycopg2binary' - - psycopg2cffi ; extra == 'postgresql-psycopg2cffi' - - psycopg>=3.0.7 ; extra == 'postgresql-psycopg' - - psycopg[binary]>=3.0.7 ; extra == 'postgresql-psycopgbinary' - - pymysql ; extra == 'pymysql' - - greenlet>=1 ; extra == 'aiomysql' - - aiomysql>=0.2.0 ; extra == 'aiomysql' - - greenlet>=1 ; extra == 'aioodbc' - - aioodbc ; extra == 'aioodbc' - - greenlet>=1 ; extra == 'asyncmy' - - asyncmy>=0.2.12 ; extra == 'asyncmy' - - greenlet>=1 ; extra == 'aiosqlite' - - aiosqlite ; extra == 'aiosqlite' - - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - - sqlcipher3-binary ; extra == 'sqlcipher' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl - name: python-jose - version: 3.5.0 - sha256: abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771 - requires_dist: - - ecdsa!=0.15 - - rsa>=4.0,!=4.1.1,!=4.4,<5.0 - - pyasn1>=0.5.0 - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - cryptography>=3.4.0 ; extra == 'cryptography' - - pycrypto>=2.6.0,<2.7.0 ; extra == 'pycrypto' - - pycryptodome>=3.3.1,<4.0.0 ; extra == 'pycryptodome' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/dd/fb/4939594c20028eca7ee54cea21de25762ef6934a5fb168b8130599921c88/p4p-4.2.2-cp313-cp313-manylinux2014_x86_64.whl name: p4p version: 4.2.2 @@ -16546,11 +16827,6 @@ packages: - pandas - xarray requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl - name: python-multipart - version: 0.0.32 - sha256: ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23 - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/e1/c1/a314376de17d02eb3e798929114cefeea29776174dab06b8162ce65ca82d/databroker-2.0.1-py2.py3-none-any.whl name: databroker version: 2.0.1 diff --git a/pixi.toml b/pixi.toml index 265f25b..6066aac 100644 --- a/pixi.toml +++ b/pixi.toml @@ -96,11 +96,15 @@ qtpy = ">=2.4.3,<3" # Install the `gui` extra (bluesky-widgets, ntnda-qt-viewer, ...) in this env. [feature.gui.pypi-dependencies] hextools = { path = ".", editable = true, extras = ["gui"] } +ntnda-qt-viewer = { git = "https://github.com/jwlodek/ntnda-qt-viewer" } [feature.gui.tasks] qserver-gui = "queue-monitor --http-server-uri https://xf27id1-hex-qs1.nsls2.bnl.gov:443" -hex-gui = "python -m hextools.gui --http-server-uri https://xf27id1-hex-qs1.nsls2.bnl.gov:443" +hex-gui = "python -m hextools.gui" + +[feature.gui-dev.tasks] +hex-gui-mock = "python -m hextools.gui --mock" [environments] # Base environments @@ -113,6 +117,7 @@ terminal = {features = ["profile", "terminal"], solve-group = "profile"} terminal-dev = { features = ["profile", "terminal", "dev"] } qs = { features = ["profile", "qs"], solve-group = "profile" } gui = { features = ["profile", "gui"], solve-group = "profile" } +gui-dev = { features = ["profile", "gui", "dev", "gui-dev"], solve-group = "profile" } # Python version specific environments for CI py311 = { features = ["dev", "py311"], solve-group = "py311" } diff --git a/pyproject.toml b/pyproject.toml index 5ef42ca..b8f2d99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ gui = [ "bluesky-queueserver", "bluesky-queueserver-api", "bluesky-httpserver", + "docstring-parser", "tiled[client]", "bluesky-tiled-plugins", "matplotlib", @@ -123,7 +124,7 @@ reportMissingImports = false # Ignore missing stubs in imported modules addopts = """ --tb=native -vv --strict-markers --doctest-modules --doctest-glob="*.rst" --doctest-glob="*.md" - --ignore=docs/examples --ignore=src/hextools/profiles + --ignore=docs/examples --ignore=src/hextools/profiles --ignore=src/hextools/gui """ # https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings filterwarnings = [ diff --git a/src/hextools/edxd.py b/src/hextools/edxd.py index 68bd3c0..1ea5f47 100644 --- a/src/hextools/edxd.py +++ b/src/hextools/edxd.py @@ -2,6 +2,7 @@ from bluesky.preprocessors import finalize_wrapper, reset_positions_decorator import numpy as np +from ophyd_async.core import StandardMovable from hextools.detectors.germ import GeRMDetector from hextools.photon_delivery_system import ensure_shutter_open, Shutter @@ -9,6 +10,7 @@ from hextools.utils import ensure_available from hextools.motors import AsyncMovable, CollimatorTable, EDXDTable from bluesky import plan_stubs as bps, plans as bp +from ophyd_async.epics.motor import Motor as AsyncEpicsMotor def configure_test_pulses( @@ -52,7 +54,7 @@ def edxd_count( def edxd_scan( - motor: AsyncMovable[float], + motor: StandardMovable[float], start: float, stop: float, num_points: int, @@ -82,7 +84,7 @@ def _body(): if save_as_tiff: germ.save_as_tiff() else: - germ.save_as_hdf5() + germ.save_as_hdf() _md = { "plan_name": "edxd_scan", @@ -100,23 +102,25 @@ def _cleanup(): return ( yield from bps.repeat( - yield from finalize_wrapper(_body(), _cleanup())), + finalize_wrapper(_body(), _cleanup()), num=num_iterations, delay=time_between_iterations ) + ) def edxd_grid_scan( - outer_motor: AsyncMovable[float], + outer_motor: StandardMovable[float], outer_start: float, outer_stop: float, outer_num_steps: int, - inner_motor: AsyncMovable[float], + inner_motor: StandardMovable[float], inner_start: float, inner_stop: float, inner_num_steps: int, count_time: float, snake: bool = False, + use_shutter: bool = True, reset_position: bool = True, description: str | None = None, germ: GeRMDetector | None = None, @@ -127,7 +131,7 @@ def edxd_grid_scan( fe_shutter = ensure_available(Shutter, fe_shutter=fe_shutter) photon_shutter = ensure_available(Shutter, photon_shutter=photon_shutter) germ = ensure_available(GeRMDetector, germ=germ) - germ.save_as_hdf5() + germ.save_as_hdf() def _body(): if use_shutter: @@ -144,7 +148,7 @@ def _body(): outer_motor, outer_start, outer_stop, outer_num_steps, inner_motor, inner_start, inner_stop, inner_num_steps, md=_md, - snake=snake + snake_axes=snake ) def _cleanup(): @@ -157,19 +161,6 @@ def _cleanup(): return (yield from finalize_wrapper(_body(), _cleanup())) - _md = {"plan_name": "edxd_grid_scan"} - if description is not None: - _md["description"] = description - yield from bp.grid_scan( - [germ], - outer_motor, outer_start, outer_stop, outer_num_steps, - inner_motor, inner_start, inner_stop, inner_num_steps, - md=_md, - snake=snake - ) - - - def edxd_2theta_tilt( theta: float, move_out: bool = False, diff --git a/src/hextools/gui/__init__.py b/src/hextools/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/hextools/gui/__main__.py b/src/hextools/gui/__main__.py new file mode 100644 index 0000000..e8a7de9 --- /dev/null +++ b/src/hextools/gui/__main__.py @@ -0,0 +1,704 @@ +"""HEX queue-monitor GUI. + +A recreation of the bluesky-widgets ``queue_monitor`` application, consolidated +into a single entrypoint so it can serve as the main HEX GUI. Run with:: + + python -m hextools.gui + +With ``--queueserver-uri`` (or ``QSERVER_HTTP_SERVER_URI``) the GUI connects to a +QueueServer and submits plans to its queue. Otherwise it starts IPython on a +profile (``--profile``, default ``collection``) with the GUI attached and runs +plans in-process via ``RE(plan(...))``. +""" + +from __future__ import annotations + +import argparse +import os +import time as ttime + +import IPython +from bluesky import RunEngine +from bluesky_widgets.models.run_engine_client import RunEngineClient +from bluesky_widgets.qt import Window, gui_qt +from bluesky_widgets.qt.run_engine_client import ( + QLabel, + QtReConsoleMonitor, + QtReEnvironmentControls, + QtReExecutionControls, + QtReManagerConnection, + QtRePlanEditor, + QtRePlanHistory, + QtRePlanQueue, + QtReQueueControls, + QtReRunningPlan, + QtReStatusMonitor, +) +from qtpy.QtCore import QObject, Qt, QTimer, Signal +from qtpy.QtWidgets import ( + QApplication, + QFileDialog, + QFrame, + QHBoxLayout, + QMainWindow, + QSplitter, + QStatusBar, + QTabWidget, + QVBoxLayout, + QWidget, +) +from pathlib import Path + +from hextools.gui.available_devices import QtAvailableDevices +from hextools.gui.device_progress_viewer import QtReWaitingHookMonitor +from hextools.gui.live_re_md_viewer import ( + QtProposalInfo, + QtReMetadataMonitor, +) +from hextools.gui.re_execution_controls import ( + QtReExecutionControls as QtReExecutionControlsLocal, +) + +from typing import Generic, TypeVar +from hextools.gui.misc import QtWeatherWidget, QtTabbedDetectorsWidget +from hextools.gui.plan_widget import QtPlanWidget +from hextools.gui.plan_status import ( + PlanMonitor, + QtPlanExecutionView, + QtPlanLogView, + QtPlanHistory, + QtPlanStatus, +) +from hextools.gui.shutter_status import QtShutterStatus +from hextools.gui._theme import apply_bnl_theme +from hextools.photon_delivery_system.dclm import change_beam_mode +from hextools.tomography.alignment import tomo_alignment_scan +from hextools.tomography.flyscans import tomo_1d_step_scan, tomo_2d_step_scan, tomo_flyscan +from hextools.tomography.radiography import take_radiograph +from hextools.edxd import edxd_scan +from hextools.photon_delivery_system import change_energy +from bluesky.plan_stubs import mv +from ophyd_async.epics.adkinetix import KinetixDetector +from ophyd_async.epics.advimba import VimbaDetector +from ophyd_async.fastcs.panda import HDFPanda +from hextools.detectors.phantom import PhantomDetector +from hextools.machine import NSLS2StorageRing +from hextools.motors import ( + FOV_2_4_mm_Camera, + FOV_20_40_mm_Camera, + OpticsTable, + SampleTower, + move_motor, +) +from hextools.photon_delivery_system import DCLM, Shutter, Slits + +from ._event_loop import gui_qt, get_our_app_name +from ._threading import wait_for_workers_to_quit + +try: # QAction moved from QtWidgets to QtGui in Qt6 + from qtpy.QtWidgets import QAction +except ImportError: + from qtpy.QtGui import QAction + +RunEngineClientT = TypeVar("RunEngineClientT", bound=RunEngineClient | RunEngine) + +PLAN_HISTORY_FILE_ENV = "HEXTOOLS_PLAN_HISTORY_FILE" + +# Names must match those defined in the profile / Queue Server namespace. +EXPECTED_DEVICES: list[tuple[str, type]] = [ + ("fe_shutter", Shutter), + ("photon_shutter", Shutter), + ("a_slits", Slits), + ("f_slits", Slits), + ("storage_ring", NSLS2StorageRing), + ("dclm", DCLM), + ("optics_table", OpticsTable), + ("sample_tower", SampleTower), + ("panda", HDFPanda), + ("kinetix1", KinetixDetector), + ("kinetix2", KinetixDetector), + ("kinetix3", KinetixDetector), + ("kinetix4", KinetixDetector), + ("double_obj_camera", FOV_2_4_mm_Camera), + ("wide_fov_camera", FOV_20_40_mm_Camera), + ("phantom", PhantomDetector), + ("sample_cam", VimbaDetector), + ("f_hutch_cam", VimbaDetector), +] + +def _patch_setchecked_checkstate(): + """Let ``setChecked`` accept ``Qt.CheckState`` enums (bluesky-widgets on PySide6>=6.9).""" + from qtpy.QtWidgets import QAbstractButton + + original = QAbstractButton.setChecked + + def set_checked(self, value): + if not isinstance(value, (bool, int)): + value = getattr(value, "value", value) + return original(self, bool(value)) + + QAbstractButton.setChecked = set_checked + + +_patch_setchecked_checkstate() + + +class QtOrganizeQueueWidgets(QSplitter): + """Vertically stacked monitor-mode queue, history, and console widgets.""" + + def __init__(self, model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = model + + self.setOrientation(Qt.Vertical) + + self._frame_1 = QFrame(self) + self._frame_2 = QFrame(self) + self._frame_3 = QFrame(self) + + self.addWidget(self._frame_1) + self.addWidget(self._frame_2) + self.addWidget(self._frame_3) + + self._running_plan = QtReRunningPlan(model) + self._running_plan.monitor_mode = True + self._plan_queue = QtRePlanQueue(model) + self._plan_queue.monitor_mode = True + self._plan_history = QtRePlanHistory(model) + self._plan_history.monitor_mode = True + self._console_monitor = QtReConsoleMonitor(model) + + vbox = QVBoxLayout() + vbox.addWidget(self._running_plan) + self._frame_1.setLayout(vbox) + + vbox = QVBoxLayout() + vbox.addWidget(self._plan_queue) + vbox.addWidget(self._plan_history) + self._frame_2.setLayout(vbox) + + vbox = QVBoxLayout() + vbox.addWidget(self._console_monitor) + self._frame_3.setLayout(vbox) + + +class QtRunEngineManagerMonitor(QWidget): + """Read-only view of the queue, history, and running plan.""" + + def __init__(self, model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = model + vbox = QVBoxLayout() + hbox = QHBoxLayout() + hbox.addWidget(QtReManagerConnection(model)) + hbox.addWidget(QtReStatusMonitor(model)) + hbox.addWidget(QtReMetadataMonitor(model, RE_METADATA_KEYS)) + hbox.addWidget(QtWeatherWidget()) + hbox.addStretch() + vbox.addLayout(hbox) + + vbox.addWidget(QtOrganizeQueueWidgets(model), stretch=2) + + self.setLayout(vbox) + + +class QtRunEngineManagerEditor(QWidget): + """Full control view with environment, queue, and plan editing.""" + + def __init__(self, model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = model + vbox = QVBoxLayout() + hbox = QHBoxLayout() + hbox.addWidget(QtReEnvironmentControls(model)) + hbox.addWidget(QtReQueueControls(model)) + hbox.addWidget(QtReExecutionControls(model)) + hbox.addWidget(QtReStatusMonitor(model)) + hbox.addStretch() + vbox.addLayout(hbox) + + hbox = QHBoxLayout() + vbox1 = QVBoxLayout() + + # Register plan editor so double-clicking a queue item opens it. + pe = QtRePlanEditor(model) + pq = QtRePlanQueue(model) + pq.registered_item_editors.append(pe.edit_queue_item) + + vbox1.addWidget(pe, stretch=1) + vbox1.addWidget(pq, stretch=1) + hbox.addLayout(vbox1) + + vbox2 = QVBoxLayout() + vbox2.addWidget(QtReRunningPlan(model), stretch=1) + vbox2.addWidget(QtRePlanHistory(model), stretch=2) + hbox.addLayout(vbox2) + vbox.addLayout(hbox) + self.setLayout(vbox) + + +class QtTomographyView(QWidget): + """Tomography controls arranged around the live detector viewers. + + Detector viewers fill the majority of the tab and the tomography plan + selector sits in a side column. The shared RE controls and progress bars + live outside the tab in :class:`QtTabbedTechniqueSelector`. + """ + + def __init__(self, re_client: RunEngineClient | RunEngine, *args, **kwargs): + super().__init__(*args, **kwargs) + self._re_client = re_client + + vbox = QVBoxLayout() + + top = QHBoxLayout() + # Detector viewers take the majority of the screen. + top.addWidget(QtTabbedDetectorsWidget( + re_client, + {f"kinetix{i}": "XF:27ID1-BI{Kinetix-Det:" + str(i) + "}Pva1:Image" for i in range(1, 5)}, + combined={"Dual Cam": ["kinetix1", "kinetix3"]}, + ), stretch=3) + + # Side column: a tabbed selector offering the tomography plans. Each tab + # validates and runs its plan per the active execution mode (in-process + # IPython vs. Queue Server). + plan_tabs = QTabWidget() + plan_tabs.addTab(QtPlanWidget(re_client, tomo_flyscan), "Flyscan") + plan_tabs.addTab(QtPlanWidget(re_client, tomo_1d_step_scan), "1D Step") + plan_tabs.addTab(QtPlanWidget(re_client, tomo_2d_step_scan), "2D Step") + plan_tabs.addTab(QtPlanWidget(re_client, take_radiograph), "Radiography") + plan_tabs.addTab(QtPlanWidget(re_client, tomo_alignment_scan), "Alignment") + + side = QVBoxLayout() + side.addWidget(plan_tabs, stretch=1) + top.addLayout(side, stretch=1) + + vbox.addLayout(top, stretch=1) + + self.setLayout(vbox) + + +class QtEDXDView(QWidget): + """Energy-dispersive X-ray diffraction view: the live GeRM viewer and EDXD plans.""" + + def __init__(self, re_client: RunEngineClient | RunEngine, *args, **kwargs): + super().__init__(*args, **kwargs) + self._re_client = re_client + + top = QHBoxLayout() + top.addWidget( + QtTabbedDetectorsWidget(re_client, {"germ": "XF:27ID1-ES{GeRM-Det:1}MCA:Pva1:Image"}), + stretch=3, + ) + + plan_tabs = QTabWidget() + plan_tabs.addTab(QtPlanWidget(re_client, edxd_scan), "EDXD Scan") + side = QVBoxLayout() + side.addWidget(plan_tabs, stretch=1) + top.addLayout(side, stretch=1) + + vbox = QVBoxLayout() + vbox.addLayout(top, stretch=1) + self.setLayout(vbox) + + +class QtXRDView(QWidget): + """X-ray diffraction view: the live Perkin Elmer area detector viewer.""" + + def __init__(self, re_client: RunEngineClient | RunEngine, *args, **kwargs): + super().__init__(*args, **kwargs) + self._re_client = re_client + + vbox = QVBoxLayout() + vbox.addWidget( + QtTabbedDetectorsWidget(re_client, {"perkin_elmer": "XF:27ID1-ES{PE-Det:1}Pva1:Image"}), + stretch=1, + ) + self.setLayout(vbox) + + +class QtBeamlineView(QWidget): + """Beamline controls arranged around the visible-light camera viewers. + + Mirrors :class:`QtTomographyView`: the visible-light camera viewers fill the + majority of the tab and a side column offers the motor and energy plans. The + shared RE controls and progress bars live outside the tab in + :class:`QtTabbedTechniqueSelector`. + """ + + def __init__(self, re_client: RunEngineClient | RunEngine, *args, **kwargs): + super().__init__(*args, **kwargs) + self._re_client = re_client + + vbox = QVBoxLayout() + + top = QHBoxLayout() + # Visible-light camera viewers take the majority of the screen. + top.addWidget(QtTabbedDetectorsWidget( + re_client, + { + "sample_cam": "XF:27ID1-ES{Sample-Cam:1}Pva1:Image", + "f_hutch_cam": "XF:27IDA-BI{GigE-Cam:5}Pva1:Image", + "diamond_window_cam": "XF:27IDA-BI{FAM:1-Cam:1}Pva1:Image", + "fs_window_cam": "XF:27IDA-BI{FS:1-Cam:1}Pva1:Image", + }, + ), stretch=3) + + # Side column: a tabbed selector offering the beamline plans. Each tab + # validates and runs its plan per the active execution mode (in-process + # IPython vs. Queue Server). + plan_tabs = QTabWidget() + plan_tabs.addTab(QtPlanWidget(re_client, move_motor), "Motors") + plan_tabs.addTab(QtPlanWidget(re_client, change_beam_mode), "Change Beam Mode") + plan_tabs.addTab(QtPlanWidget(re_client, change_energy), "Change Energy") + + side = QVBoxLayout() + side.addWidget(plan_tabs, stretch=1) + top.addLayout(side, stretch=1) + + vbox.addLayout(top, stretch=1) + + self.setLayout(vbox) + + +class QtTabbedTechniqueSelector(QWidget, Generic[RunEngineClientT]): + """Container with shared RE controls on top, the technique tabs in the + middle, and the shared live progress bars along the bottom.""" + + def __init__(self, re_client: RunEngineClientT, *args, **kwargs): + super().__init__(*args, **kwargs) + self._re_client = re_client + + vbox = QVBoxLayout() + + # Shared top row: queue/plan controls (Queue Server only) and RE + # metadata. In-process mode reads metadata from the local RunEngine. + controls = QHBoxLayout() + if isinstance(re_client, RunEngineClient): + controls.addWidget(QtReQueueControls(re_client)) + controls.addWidget(QtReExecutionControls(re_client)) + controls.addWidget(QtReRunningPlan(re_client)) + else: + controls.addWidget(QtReExecutionControlsLocal(local=True)) + controls.addWidget(QtProposalInfo(re_client)) + controls.addWidget(QtWeatherWidget()) + ipython = IPython.get_ipython() + user_ns = ipython.user_ns if ipython is not None else {} + self._plan_monitor = ( + PlanMonitor(re_client, namespace=user_ns, parent=self) + if isinstance(re_client, RunEngine) + else None + ) + if self._plan_monitor is not None: + controls.addWidget(QtPlanStatus(self._plan_monitor)) + controls.addStretch() + if isinstance(re_client, RunEngine) and ipython is not None: + controls.addWidget(QtShutterStatus(re_client, user_ns)) + vbox.addLayout(controls) + + # Technique tabs. + tabs = QTabWidget() + tabs.setObjectName("mainViewerTabs") + tabs.setTabPosition(QTabWidget.TabPosition.West) + self._beamline = QtBeamlineView(self._re_client) + tabs.addTab(self._beamline, "Beamline") + self._tomography = QtTomographyView(self._re_client) + tabs.addTab(self._tomography, "Tomography") + self._edxd = QtEDXDView(self._re_client) + tabs.addTab(self._edxd, "EDXD") + self._xrd = QtXRDView(self._re_client) + tabs.addTab(self._xrd, "XRD") + self._available_devices = QtAvailableDevices(self._re_client, EXPECTED_DEVICES) + tabs.addTab(self._available_devices, "Available Devices") + if self._plan_monitor is not None: + tabs.addTab(QtPlanExecutionView(self._plan_monitor), "Plan Execution") + tabs.addTab(QtPlanLogView(self._plan_monitor), "Log") + tabs.addTab( + QtPlanHistory( + self._plan_monitor, + history_file=os.environ.get(PLAN_HISTORY_FILE_ENV) or None, + ), + "History", + ) + vbox.addWidget(tabs, stretch=1) + + # Shared live per-device progress bars pinned to the bottom. + vbox.addWidget(QtReWaitingHookMonitor(re_client)) + + # Queue status pinned to the bottom of the window (Queue Server only). + if isinstance(re_client, RunEngineClient): + vbox.addWidget(QtRePlanQueue(re_client)) + + self.setLayout(vbox) + + +class _StateSignal(QObject): + changed = Signal() + + +class QtDataAcquisitionWindow: + """Application window that contains the menu bar and viewer. + + Parameters + ---------- + qt_widget : QtViewer + Contained viewer widget. + + Attributes + ---------- + file_menu : qtpy.QtWidgets.QMenu + File menu. + help_menu : qtpy.QtWidgets.QMenu + Help menu. + main_menu : qtpy.QtWidgets.QMainWindow.menuBar + Main menubar. + qt_widget : QtViewer + Contained viewer widget. + view_menu : qtpy.QtWidgets.QMenu + View menu. + window_menu : qtpy.QtWidgets.QMenu + Window menu. + """ + + def __init__(self, re_client: RunEngineClient | RunEngine, *, show: bool = True): + self.qt_widget = QtTabbedTechniqueSelector(re_client) + + self._qt_window = QMainWindow() + self._qt_window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + self._qt_window.setWindowTitle("HEX Data Acquisition") + self._qt_window.setUnifiedTitleAndToolBarOnMac(True) + self._qt_center = QWidget(self._qt_window) + + self._qt_window.setCentralWidget(self._qt_center) + self._qt_center.setLayout(QHBoxLayout()) + self._status_bar = QStatusBar() + self._qt_window.setStatusBar(self._status_bar) + + self._re_client = re_client + self._status_bar.showMessage(self._re_state_text()) + self._help = QLabel("") + self._status_bar.addPermanentWidget(self._help) + if isinstance(re_client, RunEngine): + self._install_state_hook(re_client) + else: + self._re_state_timer = QTimer(self._qt_window) + self._re_state_timer.timeout.connect( + lambda: self._status_bar.showMessage(self._re_state_text()) + ) + self._re_state_timer.start(250) + + layout = self._qt_center.layout() + if layout is not None: + layout.addWidget(self.qt_widget) + else: + raise RuntimeError("Failed to get layout for central widget.") + + # self._add_viewer_dock_widget(self.qt_widget.dockConsole) + # self._add_viewer_dock_widget(self.qt_widget.dockLayerControls) + # self._add_viewer_dock_widget(self.qt_widget.dockLayerList) + + # self.qt_widget.viewer.events.status.connect(self._status_changed) + # self.qt_widget.viewer.events.help.connect(self._help_changed) + # self.qt_widget.viewer.events.title.connect(self._title_changed) + # self.qt_widget.viewer.events.palette.connect(self._update_palette) + + if show: + self.show() + + def _install_state_hook(self, re: RunEngine): + self._state_signal = _StateSignal(self._qt_window) + self._state_signal.changed.connect( + lambda: self._status_bar.showMessage(self._re_state_text()) + ) + previous_hook = re.state_hook + + # Called from the RunEngine's event-loop thread. + def state_hook(new_state, old_state): + self._state_signal.changed.emit() + if previous_hook is not None: + previous_hook(new_state, old_state) + + re.state_hook = state_hook + + def _re_state_text(self) -> str: + if isinstance(self._re_client, RunEngine): + state = self._re_client.state + else: + status = self._re_client.re_manager_status or {} + state = status.get("re_state") or status.get("manager_state") or "unknown" + return f"RunEngine: {str(state).capitalize()}" + + def resize(self, width, height): + """Resize the window. + + Parameters + ---------- + width : int + Width in logical pixels. + height : int + Height in logical pixels. + """ + self._qt_window.resize(width, height) + + def show(self): + """Resize, show, and bring forward the window.""" + window_layout = self._qt_window.layout() + if window_layout is not None: + self._qt_window.resize(window_layout.sizeHint()) + self._qt_window.show() + + # We want to call Window._qt_window.raise_() in every case *except* + # when instantiating a viewer within a gui_qt() context for the + # _first_ time within the Qt app's lifecycle. + # + # `app_name` will be ours iff the application was instantiated in + # gui_qt(). isActiveWindow() will be True if it is the second time a + # _qt_window has been created. See #732 + app = QApplication.instance() + if app is None: + raise RuntimeError("Failed to get QApplication instance.") + app_name = app.applicationName() + if app_name != get_our_app_name() or self._qt_window.isActiveWindow(): + self._qt_window.raise_() # for macOS + self._qt_window.activateWindow() # for Windows + + def _status_changed(self, event): + """Update status bar. + + Parameters + ---------- + event : qtpy.QtCore.QEvent + Event from the Qt context. + """ + self._status_bar.showMessage(event.text) + + def _title_changed(self, event): + """Update window title. + + Parameters + ---------- + event : qtpy.QtCore.QEvent + Event from the Qt context. + """ + self._qt_window.setWindowTitle(event.text) + + def _help_changed(self, event): + """Update help message on status bar. + + Parameters + ---------- + event : qtpy.QtCore.QEvent + Event from the Qt context. + """ + self._help.setText(event.text) + + + def close(self): + """Close the viewer window and cleanup sub-widgets.""" + # on some versions of Darwin, exiting while fullscreen seems to tickle + # some bug deep in NSWindow. This forces the fullscreen keybinding + # test to complete its draw cycle, then pop back out of fullscreen. + if self._qt_window.isFullScreen(): + self._qt_window.showNormal() + for i in range(8): + ttime.sleep(0.1) + QApplication.processEvents() + self.qt_widget.close() + self._qt_window.close() + wait_for_workers_to_quit() + del self._qt_window + + +def launch_local_viewer(): + """Create and return the GUI in in-process (local) mode. + + Intended to be run from within an IPython session that already has the + RunEngine (``RE``) and devices loaded, so the plan widget can execute plans + via ``RE(plan(...))``. + """ + from qtpy.QtWidgets import QApplication + + # IPython's Qt event loop hook runs after startup, so create the + # QApplication now to build widgets safely. + app = QApplication.instance() or QApplication([]) + apply_bnl_theme(app) + re = IPython.get_ipython().user_ns.get("RE", None) + if not isinstance(re, RunEngine): + raise RuntimeError("RE not found in IPython user namespace or is not a RunEngine instance.") + + return QtDataAcquisitionWindow(re_client=re) + + +def main(): + """Launch the HEX Data Acquisition GUI.""" + parser = argparse.ArgumentParser(description="HEX Queue Monitor") + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--queueserver-uri", + default=None, + help="Address of the Bluesky QueueServer http server. If set, connect to" + " the QueueServer via HTTP and submit plans to its queue.", + ) + mode.add_argument( + "--profile", + default="collection", + help="Profile to load when running acquisition in-process. If set (or if" + " no QueueServer is given), start IPython on this profile with the GUI" + " attached, running plans via RE(plan(...)).", + ) + parser.add_argument( + "--mock", + action="store_true", + help="Run devices in mock/simulation mode by setting" + " HEXTOOLS_RUNNING_IN_CI=YES.", + ) + parser.add_argument( + "--history-file", + default=None, + help="JSON file to save plan history to as plans finish, and to load it" + f" from on startup. Can also be set with {PLAN_HISTORY_FILE_ENV}.", + ) + + args = parser.parse_args() + + if args.mock: + os.environ["HEXTOOLS_RUNNING_IN_CI"] = "YES" + if args.history_file: + # An env var, so it reaches the GUI built later inside the IPython session. + os.environ[PLAN_HISTORY_FILE_ENV] = args.history_file + + os.environ["BEAMLINE_ACRONYM"] = "HEX" + + if args.queueserver_uri: + with gui_qt("HEX Queue Monitor"): + apply_bnl_theme() + re_client = RunEngineClient(http_server_uri = args.queueserver_uri) + QtDataAcquisitionWindow(re_client=re_client) + else: + + from IPython import start_ipython + from traitlets.config import Config + + profile_path = Path(__file__).resolve().parent.parent / "profiles" / f"{args.profile}.py" + if not profile_path.exists(): + raise FileNotFoundError(f"Profile not found: {profile_path}") + + # Mirror the environment tweaks from the pixi `start` task. + for var in ("SESSION_MANAGER", "PYTHONPATH", "PYTHONUSERBASE"): + os.environ.pop(var, None) + os.environ["MPLBACKEND"] = "qtagg" + + config = Config() + config.InteractiveShellApp.gui = "qt" + # Run the profile first (defining RE and devices), then attach the GUI. + # exec_lines run in order, before any command-line files. + config.InteractiveShellApp.exec_lines = [ + f"get_ipython().run_line_magic('run', {f'-i {profile_path}'!r})", + "from hextools.gui.__main__ import launch_local_viewer", + "launch_local_viewer()", + ] + config.TerminalIPythonApp.display_banner = False + start_ipython(argv=[], config=config) + +if __name__ == "__main__": + main() diff --git a/src/hextools/gui/_event_loop.py b/src/hextools/gui/_event_loop.py new file mode 100644 index 0000000..fa0b6de --- /dev/null +++ b/src/hextools/gui/_event_loop.py @@ -0,0 +1,54 @@ +"""Vendored from bluesky_widgets.qt._event_loop""" + +from contextlib import contextmanager + +import qtpy +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QApplication + +from ._threading import wait_for_workers_to_quit + + +_our_app_name = None + +def get_our_app_name(): + return _our_app_name + + +@contextmanager +def gui_qt(app_name): + """Start a Qt event loop in which to run the application. + + Parameters + ---------- + app_name: str + + Notes + ----- + This context manager is not needed if running the app within an interactive + IPython session. In this case, use the ``%gui qt`` magic command, or start + IPython with the Qt GUI event loop enabled by default by using + ``ipython --gui=qt``. + """ + app = QApplication.instance() + if not app: + # automatically determine monitor DPI. + # Note: this MUST be set before the QApplication is instantiated + if qtpy.QT5: + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + elif qtpy.QT6: + QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + # if this is the first time the Qt app is being instantiated, we set + # the name, so that we know whether to raise_ in Window.show() + app = QApplication([app_name]) + global _our_app_name + _our_app_name = app_name + else: + app._existed = True + app.aboutToQuit.connect(wait_for_workers_to_quit) + yield app + # if the application already existed before this function was called, + # there's no need to start it again. By avoiding unnecessary calls to + # ``app.exec_``, we avoid blocking. + if app.applicationName() == app_name: + app.exec_() diff --git a/src/hextools/gui/_ipython.py b/src/hextools/gui/_ipython.py new file mode 100644 index 0000000..f258e4a --- /dev/null +++ b/src/hextools/gui/_ipython.py @@ -0,0 +1,31 @@ +"""Run code in the attached IPython shell on behalf of the GUI.""" + +from __future__ import annotations + +import sys + + +def run_in_ipython(code: str) -> BaseException | None: + """Echo ``code`` to the terminal, run it in IPython, and return any error.""" + from IPython.core.getipython import get_ipython + + ipython = get_ipython() + if ipython is None: + return RuntimeError("No IPython shell available for in-process execution.") + # prompt_toolkit's patch_stdout proxy buffers output until the plan finishes; + # write to the real terminal instead so progress renders live. + patched_stdout = sys.stdout + real_stdout = sys.__stdout__ + if real_stdout is not None: + sys.stdout = real_stdout + try: + if real_stdout is not None and real_stdout.isatty(): + # Clear the idle prompt line, then show a dim tag and bold command. + print(f"\r\033[K\033[2m[GUI]\033[0m \033[1m{code}\033[0m") + else: + print(f"[GUI] {code}") + # Trailing semicolon suppresses the Out[n] display of the RE return value. + result = ipython.run_cell(f"{code};", store_history=True) + finally: + sys.stdout = patched_stdout + return result.error_in_exec or result.error_before_exec diff --git a/src/hextools/gui/_theme.py b/src/hextools/gui/_theme.py new file mode 100644 index 0000000..d37ef06 --- /dev/null +++ b/src/hextools/gui/_theme.py @@ -0,0 +1,293 @@ +"""BNL brand theming for the HEX GUI. + +A dark theme built on the Brookhaven National Laboratory brand palette +(https://www.bnl.gov/brandcenter/palette.php). +""" + +from __future__ import annotations + +from pathlib import Path + +# SVG assets referenced from the stylesheet (Qt needs forward-slash URLs). +_ASSET_URL = (Path(__file__).parent / "assets").as_posix() +# Primary palette +TEAL = "#105C78" +CERULEAN = "#00ADDC" +LIME = "#B2D33B" +ORANGE = "#F68B1F" +FUCHSIA = "#B72467" + +# Secondary palette +GOLDENROD = "#FFCD34" +CRIMSON = "#DB3526" +VIOLET = "#51499E" +CORNFLOWER = "#4881C3" +JADE = "#25B574" + +# Neutrals & utility grays +BLACK = "#000000" +DARK_GRAY = "#4C515A" +GRAY = "#58595B" +MID_GRAY = "#858889" +LIGHT_GRAY = "#ACB2AE" +SAND = "#BDBDB0" +CREAM = "#DDDCCB" + +# Derived tints/shades used for surfaces (dark theme). +TEAL_DARK = "#0C4A61" +TEAL_HOVER = "#16708F" +CERULEAN_DARK = "#0090BC" +BACKGROUND = "#1E2227" +SURFACE = "#282D34" +ALT_ROW = "#2F353D" +BORDER = "#3A424B" +BORDER_STRONG = "#4C555F" +TEXT = "#E4E8EB" +TEXT_MUTED = "#9AA5AD" +# Light foreground used on top of accent-colored (teal/cerulean) surfaces. +ON_ACCENT = "#FFFFFF" + +BNL_COLORS = { + "teal": TEAL, + "cerulean": CERULEAN, + "lime": LIME, + "orange": ORANGE, + "fuchsia": FUCHSIA, + "goldenrod": GOLDENROD, + "crimson": CRIMSON, + "violet": VIOLET, + "cornflower": CORNFLOWER, + "jade": JADE, +} + +STYLESHEET = f""" +QWidget {{ + background-color: {BACKGROUND}; + color: {TEXT}; + font-size: 12px; +}} +QMainWindow, QDialog {{ background-color: {BACKGROUND}; }} +QLabel {{ background: transparent; }} +QToolTip {{ + background-color: {TEAL}; + color: {ON_ACCENT}; + border: 1px solid {TEAL_DARK}; + padding: 4px 6px; +}} + +QGroupBox {{ + border: 1px solid {BORDER}; + border-radius: 6px; + margin-top: 8px; + padding: 28px 10px 12px 10px; + background-color: {SURFACE}; +}} +QGroupBox::title {{ + subcontrol-origin: border; + subcontrol-position: top left; + left: 12px; + top: 8px; + padding: 0 4px; + color: {CERULEAN}; + font-weight: 600; +}} + +QPushButton {{ + background-color: {TEAL}; + color: {ON_ACCENT}; + border: none; + border-radius: 4px; + padding: 4px 10px; + min-width: 44px; + min-height: 22px; +}} +QPushButton:hover {{ background-color: {TEAL_HOVER}; }} +QPushButton:pressed {{ background-color: {TEAL_DARK}; }} +QPushButton:checked {{ background-color: {CERULEAN}; }} +QPushButton:disabled {{ + background-color: {BORDER}; + color: {TEXT_MUTED}; +}} +QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 5px; + padding: 3px; +}} +QToolButton:hover {{ background-color: {ALT_ROW}; border-color: {BORDER}; }} +QToolButton:pressed, QToolButton:checked {{ + background-color: {CERULEAN}; color: {ON_ACCENT}; +}} + +QLineEdit, QPlainTextEdit, QTextEdit, QSpinBox, QDoubleSpinBox, QComboBox {{ + background-color: {SURFACE}; + border: 1px solid {BORDER_STRONG}; + border-radius: 5px; + padding: 4px 6px; + selection-background-color: {CERULEAN}; + selection-color: {ON_ACCENT}; +}} +QComboBox {{ combobox-popup: 0; }} +QLineEdit:hover, QComboBox:hover, QSpinBox:hover, QDoubleSpinBox:hover {{ + border-color: {CERULEAN}; +}} +QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus, +QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {{ + border: 2px solid {CERULEAN}; + padding: 3px 5px; +}} +QComboBox::drop-down {{ border: none; width: 20px; }} +QComboBox::down-arrow {{ + image: url({_ASSET_URL}/chevron-down.svg); + width: 12px; + height: 12px; + margin-right: 6px; +}} +QComboBox::down-arrow:on {{ image: url({_ASSET_URL}/chevron-up.svg); }} +QComboBox QAbstractItemView {{ + background-color: {SURFACE}; + border: 1px solid {BORDER_STRONG}; + selection-background-color: {CERULEAN}; + selection-color: {ON_ACCENT}; +}} + +QTabWidget::pane {{ + border: 1px solid {BORDER}; + border-radius: 4px; + background: {SURFACE}; +}} +QTabBar::tab {{ + background: {BACKGROUND}; + color: {TEXT_MUTED}; + padding: 3px 12px; + border: 1px solid {BORDER}; + border-bottom: none; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +}} +QTabBar::tab:last, QTabBar::tab:only-one {{ + border-right: 1px solid {BORDER}; +}} +QTabBar::tab:selected {{ + background: {SURFACE}; + color: {CERULEAN}; + font-weight: 600; + border-top: 3px solid {LIME}; + padding-top: 1px; + margin-bottom: -1px; +}} +QTabBar::tab:hover:!selected {{ color: {CERULEAN}; background: {ALT_ROW}; }} + +/* Main viewer uses a vertical (West) tab bar. Scope with '>' so the + nested plan-editor (North) tab bar does not inherit these rules. */ +QTabWidget#mainViewerTabs::pane {{ left: -1px; }} +QTabWidget#mainViewerTabs > QTabBar::tab {{ + padding: 12px 5px; + border: 1px solid {BORDER}; + border-right: none; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin-right: 0; + margin-bottom: 3px; +}} +QTabWidget#mainViewerTabs > QTabBar::tab:selected {{ + border: 1px solid {BORDER}; + border-right: none; + border-left: 3px solid {LIME}; + margin-bottom: 3px; +}} + +QHeaderView::section {{ + background-color: {TEAL}; + color: {ON_ACCENT}; + padding: 5px 6px; + border: none; + border-right: 1px solid {TEAL_DARK}; + font-weight: 600; +}} +QTableView, QTableWidget, QTreeView, QListView {{ + background-color: {SURFACE}; + alternate-background-color: {ALT_ROW}; + gridline-color: {BORDER}; + border: 1px solid {BORDER}; + border-radius: 4px; + selection-background-color: {CERULEAN}; + selection-color: {ON_ACCENT}; + outline: none; +}} +QTableView::item, QTreeView::item, QListView::item {{ padding: 2px 4px; }} +QTableCornerButton::section {{ background-color: {TEAL}; border: none; }} + +QMenuBar {{ background-color: {TEAL}; color: {ON_ACCENT}; padding: 2px; }} +QMenuBar::item {{ background: transparent; padding: 5px 12px; border-radius: 4px; }} +QMenuBar::item:selected {{ background: {TEAL_HOVER}; }} +QMenu {{ background-color: {SURFACE}; border: 1px solid {BORDER_STRONG}; padding: 4px; }} +QMenu::item {{ padding: 5px 24px 5px 12px; border-radius: 4px; }} +QMenu::item:selected {{ background-color: {CERULEAN}; color: {ON_ACCENT}; }} +QMenu::separator {{ height: 1px; background: {BORDER}; margin: 4px 8px; }} + +QProgressBar {{ + border: 1px solid {BORDER_STRONG}; + border-radius: 5px; + text-align: center; + background: {SURFACE}; + color: {TEXT}; + min-height: 18px; +}} +QProgressBar::chunk {{ background-color: {CERULEAN}; border-radius: 4px; }} + +QScrollBar:vertical {{ background: transparent; width: 12px; margin: 0; }} +QScrollBar::handle:vertical {{ + background: {BORDER_STRONG}; + border-radius: 6px; + min-height: 24px; +}} +QScrollBar::handle:vertical:hover {{ background: {CERULEAN}; }} +QScrollBar:horizontal {{ background: transparent; height: 12px; margin: 0; }} +QScrollBar::handle:horizontal {{ + background: {BORDER_STRONG}; + border-radius: 6px; + min-width: 24px; +}} +QScrollBar::handle:horizontal:hover {{ background: {CERULEAN}; }} +QScrollBar::add-line, QScrollBar::sub-line {{ width: 0; height: 0; }} +QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }} + +QCheckBox, QRadioButton {{ background: transparent; spacing: 6px; }} +QCheckBox::indicator, QRadioButton::indicator {{ + width: 14px; + height: 14px; + background: {SURFACE}; + border: 1px solid {BORDER_STRONG}; +}} +QCheckBox::indicator {{ border-radius: 3px; }} +QRadioButton::indicator {{ border-radius: 8px; }} +QCheckBox::indicator:hover, QRadioButton::indicator:hover {{ border-color: {TEAL}; }} +QCheckBox::indicator:checked {{ + background: {TEAL}; + border-color: {TEAL}; + image: url({_ASSET_URL}/check.svg); +}} +QRadioButton::indicator:checked {{ + background: {TEAL}; + border-color: {TEAL}; + image: url({_ASSET_URL}/radio-dot.svg); +}} +QSplitter::handle {{ background: {BORDER}; }} +QSplitter::handle:horizontal {{ width: 4px; }} +QSplitter::handle:vertical {{ height: 4px; }} +QStatusBar {{ background: {TEAL}; color: {ON_ACCENT}; }} +QStatusBar::item {{ border: none; }} +""" + + +def apply_bnl_theme(app=None): + """Apply the BNL-branded stylesheet to the given (or current) QApplication.""" + if app is None: + from qtpy.QtWidgets import QApplication + + app = QApplication.instance() + if app is not None: + app.setStyleSheet(STYLESHEET) diff --git a/src/hextools/gui/_threading.py b/src/hextools/gui/_threading.py new file mode 100644 index 0000000..4570165 --- /dev/null +++ b/src/hextools/gui/_threading.py @@ -0,0 +1,624 @@ +""" +Vendored from bluesky_widgets.qt.threading +""" + +import inspect +import time +import weakref +from functools import wraps +from typing import Any, Callable, Dict, Optional, Sequence, Set, Type, Union + +import toolz as tz +from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot + + +def as_generator_function(func: Callable) -> Callable: + """Turns a regular function (single return) into a generator function.""" + + @wraps(func) + def genwrapper(*args, **kwargs): + yield + return func(*args, **kwargs) + + return genwrapper + + +class WorkerBaseSignals(QObject): + started = Signal() # emitted when the work is started + finished = Signal() # emitted when the work is finished + returned = Signal(object) # emitted with return value + errored = Signal(object) # emitted with error object on Exception + + +class WorkerBase(QRunnable): + """Base class for creating a Worker that can run in another thread. + + Parameters + ---------- + SignalsClass : type, optional + A QObject subclass that contains signals, by default WorkerBaseSignals + """ + + #: A set of Workers. Add to set using :meth:`WorkerBase.start` + _worker_set: Set["WorkerBase"] = set() + + def __init__(self, *args, SignalsClass: Type[QObject] = WorkerBaseSignals, **kwargs) -> None: + super().__init__() + self._abort_requested = False + self._running = False + self._signals = SignalsClass() + + def __getattr__(self, name): + """Pass through attr requests to signals to simplify connection API. + + The goal is to enable ``worker.signal.connect`` instead of + ``worker.signals.yielded.connect``. Because multiple inheritance of Qt + classes is not well supported in PyQt, we have to use composition here + (signals are provided by QObjects, and QRunnable is not a QObject). So + this passthrough allows us to connect to signals on the ``_signals`` + object. + """ + # the Signal object is actually a class attribute + attr = getattr(self._signals.__class__, name, None) + if isinstance(attr, Signal): + # but what we need to connect to is the instantiated signal + # (which is of type `SignalInstance` in PySide and + # `pyqtBoundSignal` in PyQt) + return getattr(self._signals, name) + + def quit(self) -> None: + """Send a request to abort the worker. + + .. note:: + + It is entirely up to subclasses to honor this method by checking + ``self.abort_requested`` periodically in their ``worker.work`` + method, and exiting if ``True``. + """ + self._abort_requested = True + + @property + def abort_requested(self) -> bool: + """Whether the worker has been requested to stop.""" + return self._abort_requested + + @property + def is_running(self) -> bool: + """Whether the worker has been started""" + return self._running + + @Slot() + def run(self): + """Start the worker. + + The end-user should never need to call this function. + But it cannot be made private or renamed, since it is called by Qt. + + The order of method calls when starting a worker is: + + .. code-block:: none + + calls QThreadPool.globalInstance().start(worker) + | triggered by the QThreadPool.start() method + | | called by worker.run + | | | + V V V + worker.start -> worker.run -> worker.work + + **This** is the function that actually gets called when calling + :func:`QThreadPool.start(worker)`. It simply wraps the :meth:`work` + method, and emits a few signals. Subclasses should NOT override this + method (except with good reason), and instead should implement + :meth:`work`. + """ + self.started.emit() + self._running = True + try: + result = self.work() + self.returned.emit(result) + except Exception as exc: + self.errored.emit(exc) + self.finished.emit() + + def work(self): + """Main method to execute the worker. + + The end-user should never need to call this function. + But subclasses must implement this method (See + :meth:`GeneratorFunction.work` for an example implementation). + Minimally, it should check ``self.abort_requested`` periodically and + exit if True. + + Examples + -------- + .. code-block:: python + + class MyWorker(WorkerBase): + + def work(self): + i = 0 + while True: + if self.abort_requested: + self.aborted.emit() + break + i += 1 + if i > max_iters: + break + time.sleep(0.5) + """ + raise NotImplementedError(f'"{self.__class__.__name__}" failed to define work() method') + + def start(self): + """Start this worker in a thread and add it to the global threadpool. + + The order of method calls when starting a worker is: + + .. code-block:: none + + calls QThreadPool.globalInstance().start(worker) + | triggered by the QThreadPool.start() method + | | called by worker.run + | | | + V V V + worker.start -> worker.run -> worker.work + """ + if self in WorkerBase._worker_set: + raise RuntimeError("This worker is already started!") + + repr(self) + + WorkerBase._worker_set.add(self) + + worker_ref = weakref.ref(self) + + def on_finished(): + w = worker_ref() + if w is not None: + WorkerBase._worker_set.discard(w) + + self.finished.connect(on_finished) + QThreadPool.globalInstance().start(self) + + +class FunctionWorker(WorkerBase): + """QRunnable with signals that wraps a simple long-running function. + + .. note:: + + ``FunctionWorker`` does not provide a way to stop a very long-running + function (e.g. ``time.sleep(10000)``). So whenever possible, it is + better to implement your long running function as a generator that + yields periodically, and use the :class:`GeneratorWorker` instead. + + Parameters + ---------- + func : Callable + A function to call in another thread + *args + will be passed to the function + **kwargs + will be passed to the function + + Raises + ------ + TypeError + If ``func`` is a generator function and not a regular function. + """ + + def __init__(self, func: Callable, *args, **kwargs): + if inspect.isgeneratorfunction(func): + raise TypeError( + f"Generator function {func} cannot be used with FunctionWorker, use GeneratorWorker instead" + ) + super().__init__() + + self._func = func + self._args = args + self._kwargs = kwargs + + def work(self): + return self._func(*self._args, **self._kwargs) + + +class GeneratorWorkerSignals(WorkerBaseSignals): + yielded = Signal(object) # emitted with yielded values (if generator used) + paused = Signal() # emitted when a running job has successfully paused + resumed = Signal() # emitted when a paused job has successfully resumed + aborted = Signal() # emitted when a running job is successfully aborted + + +class GeneratorWorker(WorkerBase): + """QRunnable with signals that wraps a long-running generator. + + Provides a convenient way to run a generator function in another thread, + while allowing 2-way communication between threads, using plain-python + generator syntax in the original function. + + Parameters + ---------- + func : callable + The function being run in another thread. May be a generator function. + SignalsClass : type, optional + A QObject subclass that contains signals, by default + GeneratorWorkerSignals + *args + Will be passed to func on instantiation + **kwargs + Will be passed to func on instantiation + """ + + def __init__( + self, + func: Callable, + *args, + SignalsClass: Type[QObject] = GeneratorWorkerSignals, + **kwargs, + ): + if not inspect.isgeneratorfunction(func): + raise TypeError( + f"Regular function {func} cannot be used with GeneratorWorker, use FunctionWorker instead" + ) + super().__init__(SignalsClass=SignalsClass) + + self._gen = func(*args, **kwargs) + self._incoming_value = None + self._pause_requested = False + self._resume_requested = False + self._paused = False + # polling interval: ONLY relevant if the user paused a running worker + self._pause_interval = 0.01 + + def work(self) -> None: + """Core event loop that calls the original function. + + Enters a continual loop, yielding and returning from the original + function. Checks for various events (quit, pause, resume, etc...). + (To clarify: we are creating a rudimentary event loop here because + there IS NO Qt event loop running in the other thread to hook into) + """ + while True: + if self.abort_requested: + self.aborted.emit() + break + if self._paused: + if self._resume_requested: + self._paused = False + self._resume_requested = False + self.resumed.emit() + else: + time.sleep(self._pause_interval) + continue + elif self._pause_requested: + self._paused = True + self._pause_requested = False + self.paused.emit() + continue + try: + self.yielded.emit(self._gen.send(self._next_value())) + except StopIteration as exc: + return exc.value + + def send(self, value: Any): + """Send a value into the function (if a generator was used).""" + self._incoming_value = value + + def _next_value(self) -> Any: + out = None + if self._incoming_value is not None: + out = self._incoming_value + self._incoming_value = None + return out + + @property + def is_paused(self) -> bool: + """Whether the worker is currently paused.""" + return self._paused + + def toggle_pause(self) -> None: + """Request to pause the worker if playing or resume if paused.""" + if self.is_paused: + self._resume_requested = True + else: + self._pause_requested = True + + def pause(self) -> None: + """Request to pause the worker.""" + if not self.is_paused: + self._pause_requested = True + + def resume(self) -> None: + """Send a request to resume the worker.""" + if self.is_paused: + self._resume_requested = True + + +############################################################################ + +# public API + +# For now, the next three functions simply wrap the QThreadPool API, and allow +# us to track and cleanup all workers that were started with ``start_worker``, +# provided that ``wait_for_workers_to_quit`` is called at shutdown. +# In the future, this could wrap any API, or a pure python threadpool. + + +def set_max_thread_count(num: int): + """Set the maximum number of threads used by the thread pool. + + Note: The thread pool will always use at least 1 thread, even if + maxThreadCount limit is zero or negative. + """ + QThreadPool.globalInstance().setMaxThreadCount(num) + + +def wait_for_workers_to_quit(msecs: int = None): + """Ask all workers to quit, and wait up to `msec` for quit. + + Attempts to clean up all running workers by calling ``worker.quit()`` + method. Any workers in the ``WorkerBase._worker_set`` set will have this + method. + + By default, this function will block indefinitely, until worker threads + finish. If a timeout is provided, a ``RuntimeError`` will be raised if + the workers do not gracefully exit in the time requests, but the threads + will NOT be killed. It is (currently) left to the user to use their OS + to force-quit rogue threads. + + .. important:: + + If the user does not put any yields in their function, and the function + is super long, it will just hang... For instance, there's no graceful + way to kill this thread in python: + + .. code-block:: python + + @thread_worker + def ZZZzzz(): + time.sleep(10000000) + + This is why it's always advisable to use a generator that periodically + yields for long-running computations in another thread. + + See `this stack-overflow post + `_ + for a good discussion on the difficulty of killing a rogue python thread: + + Parameters + ---------- + msecs : int, optional + Waits up to msecs milliseconds for all threads to exit and removes all + threads from the thread pool. If msecs is `None` (the default), the + timeout is ignored (waits for the last thread to exit). + + Raises + ------ + RuntimeError + If a timeout is provided and workers do not quit successfully within + the time allotted. + """ + for worker in WorkerBase._worker_set: + worker.quit() + + msecs = msecs if msecs is not None else -1 + if not QThreadPool.globalInstance().waitForDone(msecs): + raise RuntimeError(f"Workers did not quit gracefully in the time allotted ({msecs} ms)") + + +def active_thread_count() -> int: + """Return the number of active threads in the global ThreadPool.""" + return QThreadPool.globalInstance().activeThreadCount() + + +############################################################################# + +# convenience functions for creating Worker instances + + +def create_worker( + func: Callable, + *args, + _start_thread: Optional[bool] = None, + _connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None, + _worker_class: Optional[Type[WorkerBase]] = None, + _ignore_errors: bool = False, + **kwargs, +) -> WorkerBase: + """Convenience function to start a function in another thread. + + By default, uses :class:`Worker`, but a custom ``WorkerBase`` subclass may + be provided. If so, it must be a subclass of :class:`Worker`, which + defines a standard set of signals and a run method. + + Parameters + ---------- + func : Callable + The function to call in another thread. + _start_thread : bool, optional + Whether to immediaetly start the thread. If False, the returned worker + must be manually started with ``worker.start()``. by default it will be + ``False`` if the ``_connect`` argument is ``None``, otherwise ``True``. + _connect : Dict[str, Union[Callable, Sequence]], optional + A mapping of ``"signal_name"`` -> ``callable`` or list of ``callable``: + callback functions to connect to the various signals offered by the + worker class. by default None + _worker_class : Type[WorkerBase], optional + The :class`WorkerBase` to instantiate, by default + :class:`FunctionWorker` will be used if ``func`` is a regular function, + and :class:`GeneratorWorker` will be used if it is a generator. + _ignore_errors : bool, optional + If ``False`` (the default), errors raised in the other thread will be + reraised in the main thread (makes debugging significantly easier). + *args + will be passed to ``func`` + **kwargs + will be passed to ``func`` + + Returns + ------- + worker : WorkerBase + An instantiated worker. If ``_start_thread`` was ``False``, the worker + will have a `.start()` method that can be used to start the thread. + + Raises + ------ + TypeError + If a worker_class is provided that is not a subclass of WorkerBase. + TypeError + If _connect is provided and is not a dict of ``{str: callable}`` + + Examples + -------- + .. code-block:: python + + def long_function(duration): + import time + time.sleep(duration) + + worker = create_worker(long_function, 10) + + """ + if not _worker_class: + if inspect.isgeneratorfunction(func): + _worker_class = GeneratorWorker + else: + _worker_class = FunctionWorker + + if not (inspect.isclass(_worker_class) and issubclass(_worker_class, WorkerBase)): + raise TypeError(f"Worker {_worker_class} must be a subclass of WorkerBase") + + worker = _worker_class(func, *args, **kwargs) + + if _connect is not None: + if not isinstance(_connect, dict): + raise TypeError("The '_connect' argument must be a dict") + + if _start_thread is None: + _start_thread = True + + for key, val in _connect.items(): + _val = val if isinstance(val, (tuple, list)) else [val] + for v in _val: + if not callable(v): + raise TypeError(f'"_connect[{key!r}]" must be a function or ' "sequence of functions") + getattr(worker, key).connect(v) + + # if the user has not provided a default connection for the "errored" + # signal... and they have not explicitly set ``ignore_errors=True`` + # Then rereaise any errors from the thread. + if not _ignore_errors and not (_connect or {}).get("errored", False): + + def reraise(e): + raise e + + worker.errored.connect(reraise) + + if _start_thread: + worker.start() + return worker + + +@tz.curry +def thread_worker( + function: Callable, + start_thread: Optional[bool] = None, + connect: Optional[Dict[str, Union[Callable, Sequence[Callable]]]] = None, + worker_class: Optional[Type[WorkerBase]] = None, + ignore_errors: bool = False, +) -> Callable: + """Decorator that runs a function in a separate thread when called. + + When called, the decorated function returns a :class:`WorkerBase`. See + :func:`create_worker` for additional keyword arguments that can be used + when calling the function. + + The returned worker will have these signals: + + - *started*: emitted when the work is started + - *finished*: emitted when the work is finished + - *returned*: emitted with return value + - *errored*: emitted with error object on Exception + + It will also have a ``worker.start()`` method that can be used to start + execution of the function in another thread. (useful if you need to connect + callbacks to signals prior to execution) + + If the decorated function is a generator, the returned worker will also + provide these signals: + + - *yielded*: emitted with yielded values + - *paused*: emitted when a running job has successfully paused + - *resumed*: emitted when a paused job has successfully resumed + - *aborted*: emitted when a running job is successfully aborted + + And these methods: + + - *quit*: ask the thread to quit + - *toggle_paused*: toggle the running state of the thread. + - *send*: send a value into the generator. (This requires that your + decorator function uses the ``value = yield`` syntax) + + Parameters + ---------- + function : callable + Function to call in another thread. For communication between threads + may be a generator function. + start_thread : bool, optional + Whether to immediaetly start the thread. If False, the returned worker + must be manually started with ``worker.start()``. by default it will be + ``False`` if the ``_connect`` argument is ``None``, otherwise ``True``. + connect : Dict[str, Union[Callable, Sequence]], optional + A mapping of ``"signal_name"`` -> ``callable`` or list of ``callable``: + callback functions to connect to the various signals offered by the + worker class. by default None + worker_class : Type[WorkerBase], optional + The :class`WorkerBase` to instantiate, by default + :class:`FunctionWorker` will be used if ``func`` is a regular function, + and :class:`GeneratorWorker` will be used if it is a generator. + ignore_errors : bool, optional + If ``False`` (the default), errors raised in the other thread will be + reraised in the main thread (makes debugging significantly easier). + + Returns + ------- + callable + function that creates a worker, puts it in a new thread and returns + the worker instance. + + Examples + -------- + .. code-block:: python + + @thread_worker + def long_function(start, end): + # do work, periodically yielding + i = start + while i <= end: + time.sleep(0.1) + yield i + + # do teardown + return 'anything' + + # call the function to start running in another thread. + worker = long_function() + # connect signals here if desired... or they may be added using the + # `connect` argument in the `@thread_worker` decorator... in which + # case the worker will start immediately when long_function() is called + worker.start() + """ + + @wraps(function) + def worker_function(*args, **kwargs): + # decorator kwargs can be overridden at call time by using the + # underscore-prefixed version of the kwarg. + kwargs["_start_thread"] = kwargs.get("_start_thread", start_thread) + kwargs["_connect"] = kwargs.get("_connect", connect) + kwargs["_worker_class"] = kwargs.get("_worker_class", worker_class) + kwargs["_ignore_errors"] = kwargs.get("_ignore_errors", ignore_errors) + return create_worker( + function, + *args, + **kwargs, + ) + + return worker_function diff --git a/src/hextools/gui/assets/check.svg b/src/hextools/gui/assets/check.svg new file mode 100644 index 0000000..3864b6f --- /dev/null +++ b/src/hextools/gui/assets/check.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/hextools/gui/assets/chevron-down.svg b/src/hextools/gui/assets/chevron-down.svg new file mode 100644 index 0000000..6d765f3 --- /dev/null +++ b/src/hextools/gui/assets/chevron-down.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/hextools/gui/assets/chevron-up.svg b/src/hextools/gui/assets/chevron-up.svg new file mode 100644 index 0000000..7fa75d7 --- /dev/null +++ b/src/hextools/gui/assets/chevron-up.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/hextools/gui/assets/radio-dot.svg b/src/hextools/gui/assets/radio-dot.svg new file mode 100644 index 0000000..eafe85c --- /dev/null +++ b/src/hextools/gui/assets/radio-dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/hextools/gui/available_devices.py b/src/hextools/gui/available_devices.py new file mode 100644 index 0000000..e03ddbc --- /dev/null +++ b/src/hextools/gui/available_devices.py @@ -0,0 +1,197 @@ +"""Qt widget listing beamline devices and their connection status. + +In local (in-process) mode every ``ophyd.Device`` or ``ophyd_async`` ``Device`` +in the IPython namespace is listed, and counts as connected only once it has +actually connected. In Queue Server mode a fixed list of expected devices is +shown, each connected if the server reports it among its allowed devices or plans. +""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from typing import Any + +import IPython +from bluesky import RunEngine +from bluesky_widgets.models.run_engine_client import RunEngineClient +from qtpy.QtCore import QTimer, Signal, Slot +from qtpy.QtWidgets import ( + QGridLayout, + QLabel, + QScrollArea, + QVBoxLayout, + QWidget, +) + +_DEVICE_BASES: list[type] = [] +try: + from ophyd_async.core import Device as _AsyncDevice + + _DEVICE_BASES.append(_AsyncDevice) +except ImportError: # pragma: no cover + _AsyncDevice = None # type: ignore[assignment,misc] +try: + from ophyd import Device as _OphydDevice + + _DEVICE_BASES.append(_OphydDevice) +except ImportError: # pragma: no cover + pass + +_CONNECTED_STYLE = "color: #2e7d32; font-weight: bold;" +_DISCONNECTED_STYLE = "color: #c62828; font-weight: bold;" +_LOCAL_POLL_MS = 2000 + + +def _is_connected(obj: Any) -> bool: + """Return True only if ``obj`` has completed a successful connection.""" + if _AsyncDevice is not None and isinstance(obj, _AsyncDevice): + if getattr(obj, "_mock", None) is not None: + return True + task = getattr(obj, "_connect_task", None) + return bool(task is not None and task.done() and not task.cancelled() and task.exception() is None) + # Classic ophyd objects expose a ``connected`` property. + return bool(getattr(obj, "connected", False)) + + +def _discover_devices(namespace: Mapping[str, Any]) -> list[tuple[str, str, Any]]: + """Return ``(label, type name, device)`` for each distinct device in ``namespace``. + + Aliases of one device share a row, labelled by the alias matching the device's + own ``name`` (else the first alphabetically) with the others in parentheses. + """ + aliases: dict[int, list[str]] = {} + devices: dict[int, Any] = {} + for name, obj in sorted(namespace.items()): + if name.startswith("_") or not isinstance(obj, tuple(_DEVICE_BASES)): + continue + aliases.setdefault(id(obj), []).append(name) + devices[id(obj)] = obj + entries = [] + for key, names in aliases.items(): + obj = devices[key] + primary = next((n for n in names if n == getattr(obj, "name", None)), names[0]) + others = [n for n in names if n != primary] + label = f"{primary} ({', '.join(others)})" if others else primary + entries.append((label, type(obj).__name__, obj)) + return sorted(entries, key=lambda entry: entry[0]) + + +class QtAvailableDevices(QWidget): + """Table of device name, type, and connection status. + + Parameters + ---------- + re_client : RunEngineClient or RunEngine + Queue Server client model, or the in-process RunEngine. + devices : Sequence[tuple[str, type]], optional + ``(name, type)`` pairs of the devices expected to be available. Only used + in Queue Server mode; local mode discovers devices from the namespace. + namespace : Mapping[str, object], optional + Namespace searched in local mode. Defaults to the IPython ``user_ns``. + """ + + _signal_refresh = Signal() + + def __init__( + self, + re_client: RunEngineClient | RunEngine, + devices: Sequence[tuple[str, type]] = (), + *, + namespace: Mapping[str, Any] | None = None, + parent=None, + ): + super().__init__(parent) + self._re_client = re_client + self._devices = list(devices) + self._is_qserver = isinstance(re_client, RunEngineClient) + if namespace is None and not self._is_qserver: + ip = IPython.get_ipython() + namespace = ip.user_ns if ip is not None else {} + self._namespace = namespace or {} + self._rows: list[tuple[str, str]] = [] + self._status_labels: dict[str, QLabel] = {} + + self._scroll = QScrollArea() + self._scroll.setWidgetResizable(True) + + vbox = QVBoxLayout() + vbox.addWidget(self._scroll) + self.setLayout(vbox) + + self._signal_refresh.connect(self._refresh) + if self._is_qserver: + events = self._re_client.events # type: ignore[union-attr] + events.allowed_devices_changed.connect(self._on_model_changed) + events.allowed_plans_changed.connect(self._on_model_changed) + else: + # Polling also picks up devices created or deleted at the IPython prompt. + self._timer = QTimer(self) + self._timer.timeout.connect(self._refresh) + self._timer.start(_LOCAL_POLL_MS) + + self._refresh() + + def _on_model_changed(self, event=None): + # Model events fire from a polling thread; marshal onto the GUI thread. + self._signal_refresh.emit() + + def _qserver_connected(self, name: str, dtype: type) -> bool: + model = self._re_client + allowed_devices = getattr(model, "_allowed_devices", {}) or {} # noqa: SLF001 + allowed_plans = getattr(model, "_allowed_plans", {}) or {} # noqa: SLF001 + info = allowed_devices.get(name) + if isinstance(info, dict): + return info.get("classname") in (None, dtype.__name__) + return name in allowed_plans + + def _entries(self) -> list[tuple[str, str, Callable[[], bool]]]: + """``(label, type name, is-connected check)`` for each row to show.""" + if self._is_qserver: + return [ + (name, dtype.__name__, lambda n=name, t=dtype: self._qserver_connected(n, t)) + for name, dtype in self._devices + ] + return [ + (label, type_name, lambda o=obj: _is_connected(o)) + for label, type_name, obj in _discover_devices(self._namespace) + ] + + def _rebuild(self, rows: list[tuple[str, str]]): + grid = QGridLayout() + for col, header in enumerate(("Name", "Type", "Status")): + grid.addWidget(QLabel(f"{header}"), 0, col) + self._status_labels = {} + for row, (label, type_name) in enumerate(rows, start=1): + grid.addWidget(QLabel(label), row, 0) + grid.addWidget(QLabel(type_name), row, 1) + status = QLabel() + self._status_labels[label] = status + grid.addWidget(status, row, 2) + grid.setColumnStretch(3, 1) + grid.setRowStretch(len(rows) + 1, 1) + content = QWidget() + content.setLayout(grid) + self._scroll.setWidget(content) # deletes the previous content widget + self._rows = rows + + @Slot() + def _refresh(self): + entries = self._entries() + rows = [(label, type_name) for label, type_name, _ in entries] + if rows != self._rows: + self._rebuild(rows) + for label, _, is_connected in entries: + connected = is_connected() + status = self._status_labels[label] + status.setText("Connected" if connected else "Disconnected") + status.setStyleSheet(_CONNECTED_STYLE if connected else _DISCONNECTED_STYLE) + + def closeEvent(self, event): + if self._is_qserver: + events = self._re_client.events # type: ignore[union-attr] + for emitter in (events.allowed_devices_changed, events.allowed_plans_changed): + try: + emitter.disconnect(self._on_model_changed) + except (ValueError, TypeError, RuntimeError): + pass + super().closeEvent(event) diff --git a/src/hextools/gui/device_progress_viewer.py b/src/hextools/gui/device_progress_viewer.py new file mode 100644 index 0000000..e1dbc55 --- /dev/null +++ b/src/hextools/gui/device_progress_viewer.py @@ -0,0 +1,312 @@ +from functools import partial +from typing import Any + +from bluesky import RunEngine +from bluesky_widgets.models.run_engine_client import RunEngineClient +from qtpy.QtWidgets import QVBoxLayout, QWidget, QLabel, QProgressBar, QHBoxLayout +from qtpy.QtCore import Qt, Signal +from ._threading import FunctionWorker + +class _ReDeviceProgressBar(QWidget): + """ + A single-line progress indicator for one RunEngine status object (typically one device). + + Layout: ``name [start] [====== current | elapsed | pct% ======] -> target (ETA)``. + The start position is shown in front of the bar and the target with the estimated time + remaining after it. The current position, elapsed time and percentage are drawn on the bar. + """ + + def __init__(self, name, parent=None): + super().__init__(parent) + + self._lb_name = QLabel(f"{name}") + self._lb_name.setStyleSheet("font-weight: bold;") + self._lb_name.setMinimumWidth(120) + + self._lb_start = QLabel("") + self._lb_start.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + self._lb_start.setMinimumWidth(90) + + self._lb_end = QLabel("") + self._lb_end.setMinimumWidth(170) + + self._progress_bar = QProgressBar() + self._progress_bar.setRange(0, 1000) + self._progress_bar.setValue(0) + self._progress_bar.setTextVisible(True) + self._progress_bar.setMinimumWidth(200) + + # Last known values, used to fill in fields omitted from later updates (e.g. the + # final "done" update, which may not repeat the position/unit information). + self._last = { + "precision": None, + "unit": None, + "initial": None, + "current": None, + "target": None, + "time_elapsed": None, + "time_remaining": None, + } + + hbox = QHBoxLayout() + hbox.setContentsMargins(0, 0, 0, 0) + hbox.addWidget(self._lb_name) + hbox.addWidget(self._lb_start) + hbox.addWidget(self._progress_bar, stretch=1) + hbox.addWidget(self._lb_end) + self.setLayout(hbox) + + def update_progress(self, msg: dict[str, Any]): + # Fall back to the last known value for any field omitted from this update, so the + # display keeps its last values (e.g. on the final update) instead of showing "N/A". + def coalesce(key): + value = msg.get(key) + if value is None: + value = self._last[key] + else: + self._last[key] = value + return value + + precision = coalesce("precision") + unit = coalesce("unit") or "" + initial = coalesce("initial") + current = coalesce("current") + target = coalesce("target") + time_elapsed = coalesce("time_elapsed") + time_remaining = coalesce("time_remaining") + done = bool(msg.get("done")) + + def fmt_pos(value): + if value is None: + return "N/A" + if isinstance(precision, int) and isinstance(value, (int, float)): + text = f"{value:.{precision}f}" + else: + text = f"{value}" + return f"{text} {unit}".strip() if unit else text + + # Progress as fraction complete in [0, 1]. Prefer computing it from positions + # (matches bluesky's own progress bar). ophyd reports ``fraction`` as the fraction + # *remaining*, so convert it when positions are unavailable. + progress = None + if None not in (initial, current, target): + span = abs(target - initial) + if span: + progress = abs(current - initial) / span + if progress is None and msg.get("fraction") is not None: + progress = 1.0 - msg["fraction"] + if done: + progress = 1.0 + if target is not None: + current = target + + if progress is not None: + self._progress_bar.setValue(int(max(0.0, min(1.0, progress)) * 1000)) + + # Estimate the remaining time if the status object did not report it. + if time_remaining is None and time_elapsed is not None and progress: + time_remaining = 0.0 if progress >= 1.0 else time_elapsed * (1.0 - progress) / progress + + elapsed_str = "" if time_elapsed is None else f"{time_elapsed:.1f}s" + remaining_str = "?" if time_remaining is None else f"{time_remaining:.1f}s" + + bar_text = fmt_pos(current) + if elapsed_str: + bar_text += f" | {elapsed_str}" + bar_text += " | %p%" + self._progress_bar.setFormat(bar_text) + + self._lb_start.setText(fmt_pos(initial)) + eta = "done" if done or (progress is not None and progress >= 1.0) else f"ETA {remaining_str}" + self._lb_end.setText(f"\u2192 {fmt_pos(target)} ({eta})") + + +class _WaitingHookDispatcher: + """A RunEngine ``waiting_hook`` that forwards watcher updates to a monitor. + + Installed as ``RE.waiting_hook``. Any previously-installed hook (e.g. a + terminal ``ProgressBarManager``) is preserved in ``previous_hook`` and still + receives every call, so existing behavior keeps working alongside the GUI. + """ + + def __init__(self, monitor: "QtReWaitingHookMonitor", previous_hook=None): + self._monitor = monitor + self._previous_hook = previous_hook + self._names: dict[int, str] = {} # id(status) -> device name + + def __call__(self, status_objects): + # Pass through to any pre-existing hook first. + if self._previous_hook is not None: + try: + self._previous_hook(status_objects) + except Exception: + pass + + if status_objects is None: + self._names.clear() + self._monitor.signal_completed.emit() + return + + for status in status_objects: + if not hasattr(status, "watch"): + continue + try: + status.watch(partial(self._on_watch, status)) + except Exception: + pass + try: + status.add_callback(self._on_finished) + except Exception: + pass + + def _on_watch(self, status, *, name=None, **kwargs): + if name is None: + name = self._names.get(id(status)) + else: + self._names[id(status)] = name + msg = dict(kwargs) + msg["name"] = name + msg["done"] = bool(getattr(status, "done", False)) + self._monitor.signal_progress_update.emit(msg) + + def _on_finished(self, status=None, *args, **kwargs): + name = self._names.get(id(status)) if status is not None else None + if name is None and status is not None: + name = getattr(getattr(status, "device", None), "name", None) + if name is None: + return + self._monitor.signal_progress_update.emit({"name": name, "done": True}) + + +class QtReWaitingHookMonitor(QWidget): + """ + Displays live progress bars for RunEngine ``waiting_hook``/watcher updates. One progress + bar is shown per device (status object), reporting the start position, target, current + position and estimated time remaining. All progress bars are cleared when the wait + completes. + + Accepts either a Queue Server ``RunEngineClient`` (progress is streamed from RE Manager) + or a raw in-process ``RunEngine`` (the widget installs itself as ``RE.waiting_hook``, + chaining to any hook that was already set). + """ + + signal_progress_update = Signal(object) + signal_completed = Signal() + + def __init__(self, re_client: RunEngineClient | RunEngine, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + + self._re_client = re_client + self._hook: _WaitingHookDispatcher | None = None + self._previous_hook = None + self._thread = None + + # Maps device name -> _ReDeviceProgressBar + self._progress_bars = {} + + self._lb_status = QLabel("Waiting for updates\u2026") + + self._bars_layout = QVBoxLayout() + self._bars_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + bars_container = QWidget() + bars_container.setLayout(self._bars_layout) + + vbox = QVBoxLayout() + vbox.addWidget(self._lb_status) + vbox.addWidget(bars_container) + vbox.addStretch() + self.setLayout(vbox) + + self.signal_progress_update.connect(self._handle_progress_msg) + self.signal_completed.connect(self._on_completed) + + if isinstance(self._re_client, RunEngineClient): + self._re_client.start_re_progress_monitoring() + self._start_thread() + else: + self._install_waiting_hook() + + # -- Queue Server (RE Manager) path --------------------------------------- + + def _start_thread(self): + self._thread = FunctionWorker(self._re_client.re_progress_monitoring_thread) + if self._thread.returned is None: + raise RuntimeError("Failed to connect to the returned signal of the thread.") + if self._thread.finished is None: + raise RuntimeError("Failed to connect to the finished signal of the thread.") + self._thread.returned.connect(self._process_new_progress_update) + self._thread.finished.connect(self._finished_receiving_progress_update) + self._thread.start() + + def _finished_receiving_progress_update(self): + self._start_thread() + + def _process_new_progress_update(self, result): + if not result: + return + _, msg = result + self._handle_progress_msg(msg) + + # -- In-process RunEngine path -------------------------------------------- + + def _install_waiting_hook(self): + run_engine = self._re_client + self._previous_hook = getattr(run_engine, "waiting_hook", None) + self._hook = _WaitingHookDispatcher(self, self._previous_hook) + run_engine.waiting_hook = self._hook + + def _uninstall_waiting_hook(self): + run_engine = self._re_client + if isinstance(run_engine, RunEngineClient) or self._hook is None: + return + # Only restore if our hook is still the active one. + if getattr(run_engine, "waiting_hook", None) is self._hook: + run_engine.waiting_hook = self._previous_hook + self._hook = None + + # -- Shared display logic -------------------------------------------------- + + def _handle_progress_msg(self, msg): + if not msg: + return + + if msg.get("completed"): + self._on_completed() + return + + name = msg.get("name") + if name is None: + return + + bar = self._progress_bars.get(name) + if bar is None: + bar = _ReDeviceProgressBar(name) + self._progress_bars[name] = bar + self._bars_layout.addWidget(bar) + + bar.update_progress(msg) + self._lb_status.setText("Watching device progress\u2026") + + def _on_completed(self): + self._clear_progress_bars() + self._lb_status.setText("Waiting for updates\u2026") + + def _clear_progress_bars(self): + for bar in self._progress_bars.values(): + self._bars_layout.removeWidget(bar) + bar.deleteLater() + self._progress_bars = {} + + def closeEvent(self, event): + self._uninstall_waiting_hook() + super().closeEvent(event) + + def __del__(self): + try: + if isinstance(self._re_client, RunEngineClient): + self._re_client.stop_re_progress_monitoring() + else: + self._uninstall_waiting_hook() + except Exception: + pass \ No newline at end of file diff --git a/src/hextools/gui/device_selector_widget.py b/src/hextools/gui/device_selector_widget.py new file mode 100644 index 0000000..1c54e76 --- /dev/null +++ b/src/hextools/gui/device_selector_widget.py @@ -0,0 +1,466 @@ +"""Qt widgets for selecting Queue Server devices by type. + +The widgets discover the devices currently allowed by the Queue Server (via the +``RunEngineClient`` model) and present the subset matching a requested device +type. :class:`QtDeviceSelector` offers a single-selection dropdown; +:class:`QtMultiDeviceSelector` offers a growable set of dropdown rows (add with +``+``, remove with ``-``) for selecting any number of distinct devices. + +Device "type" is expressed with :class:`DeviceType`, which maps onto the +protocol flags reported by the Queue Server (``is_readable``, ``is_movable``, +``is_flyable``). Selection can optionally be narrowed further to specific ophyd +class names. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from enum import Enum + +from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import ( + QComboBox, + QGroupBox, + QHBoxLayout, + QSizePolicy, + QToolButton, + QVBoxLayout, + QWidget, +) + +_NO_SELECTION = "\u2014" # em dash + + +class DeviceType(str, Enum): + """Device categories that can be filtered on, matching Queue Server flags.""" + + ANY = "any" + READABLE = "readable" + MOVABLE = "movable" + FLYABLE = "flyable" + + +_TYPE_FLAG = { + DeviceType.READABLE: "is_readable", + DeviceType.MOVABLE: "is_movable", + DeviceType.FLYABLE: "is_flyable", +} + + +def _matches(info: dict, device_type: DeviceType, classnames: frozenset[str] | None) -> bool: + if classnames is not None and info.get("classname") not in classnames: + return False + if device_type is DeviceType.ANY: + return True + return bool(info.get(_TYPE_FLAG[device_type], False)) + + +def _iter_matching_devices( + devices: dict, + device_type: DeviceType, + classnames: frozenset[str] | None, + *, + include_components: bool, + _prefix: str = "", +) -> Iterable[str]: + """Yield the dotted names of devices (and optionally components) that match.""" + for name, info in devices.items(): + if not isinstance(info, dict): + continue + full_name = f"{_prefix}{name}" + if _matches(info, device_type, classnames): + yield full_name + if include_components and info.get("components"): + yield from _iter_matching_devices( + info["components"], + device_type, + classnames, + include_components=include_components, + _prefix=f"{full_name}.", + ) + + +class DynamicDeviceSelector(QWidget): + """Select any number of distinct devices via growable dropdown rows. + + Each row is a dropdown offering only the devices not already chosen in + another row. The first row cannot be removed; later rows show a ``-`` button + that removes the whole row. A ``+`` button on the last row appends another + row and is hidden once every available device has been selected. + """ + + selection_changed = Signal(object) + + def __init__(self, options: Sequence[str] | None = None, parent=None): + super().__init__(parent) + self._options: list[str] = list(options or []) + self._rows: list[dict] = [] + self._updating = False + + self._rows_layout = QVBoxLayout() + self._rows_layout.setContentsMargins(0, 0, 0, 0) + self._rows_layout.setSpacing(4) + self.setLayout(self._rows_layout) + + self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + + self._add_row() + self._refresh() + + def _make_button(self, text: str) -> QToolButton: + button = QToolButton() + button.setText(text) + button.setFixedWidth(24) + return button + + def _add_row(self, select: str | None = None) -> dict: + combo = QComboBox() + plus = self._make_button("+") + minus = self._make_button("\u2212") + + container = QWidget() + hbox = QHBoxLayout(container) + hbox.setContentsMargins(0, 0, 0, 0) + hbox.setSpacing(4) + hbox.addWidget(combo, 1) + hbox.addWidget(plus) + hbox.addWidget(minus) + + row = {"widget": container, "combo": combo, "plus": plus, "minus": minus} + self._rows.append(row) + self._rows_layout.addWidget(container) + + self._populate_combo(row, select) + combo.currentIndexChanged.connect(self._on_combo_changed) + plus.clicked.connect(self._on_add_clicked) + minus.clicked.connect(lambda *_, r=row: self._remove_row(r)) + return row + + def _remove_row(self, row: dict): + if len(self._rows) <= 1: + return + self._rows.remove(row) + row["widget"].setParent(None) + row["widget"].deleteLater() + self._refresh() + + def _selected_except(self, exclude: dict | None) -> set[str]: + chosen = set() + for row in self._rows: + if row is exclude: + continue + value = row["combo"].currentData() + if value is not None: + chosen.add(value) + return chosen + + def _remaining(self) -> list[str]: + chosen = self._selected_except(None) + return [name for name in self._options if name not in chosen] + + def _populate_combo(self, row: dict, select: str | None = None): + combo = row["combo"] + current = select if select is not None else combo.currentData() + others = self._selected_except(row) + + combo.blockSignals(True) + combo.clear() + for name in self._options: + if name not in others: + combo.addItem(name, userData=name) + index = combo.findData(current) + if index < 0 and combo.count(): + index = 0 + if index >= 0: + combo.setCurrentIndex(index) + combo.blockSignals(False) + + def _refresh(self): + if self._updating: + return + self._updating = True + try: + for row in self._rows: + self._populate_combo(row) + has_more = bool(self._remaining()) + last = len(self._rows) - 1 + for i, row in enumerate(self._rows): + row["plus"].setVisible(i == last and has_more) + row["minus"].setVisible(i > 0) + finally: + self._updating = False + self.selection_changed.emit(self.selected_devices()) + + def _on_combo_changed(self, *_): + self._refresh() + + def _on_add_clicked(self, *_): + remaining = self._remaining() + if not remaining: + return + self._add_row(select=remaining[0]) + self._refresh() + + def selected_devices(self) -> list[str]: + """Devices chosen across the rows, in row order (no duplicates).""" + result: list[str] = [] + for row in self._rows: + value = row["combo"].currentData() + if value is not None and value not in result: + result.append(value) + return result + + def set_available(self, options: Sequence[str]): + """Update the available devices, dropping rows that no longer apply.""" + self._options = list(options) + kept = [] + for row in self._rows: + value = row["combo"].currentData() + if value is None or value in self._options: + kept.append(row) + else: + row["widget"].setParent(None) + row["widget"].deleteLater() + self._rows = kept + if not self._rows: + self._add_row() + self._refresh() + + def set_selected_devices(self, names: Iterable[str]): + """Replace the rows so that exactly ``names`` (if available) are chosen.""" + wanted = [name for name in names if name in self._options] + for row in self._rows: + row["widget"].setParent(None) + row["widget"].deleteLater() + self._rows = [] + for name in wanted: + self._add_row(select=name) + if not self._rows: + self._add_row() + self._refresh() + + +class _DeviceSelectorBase(QWidget): + """Shared discovery/refresh logic for the device selector widgets.""" + + signal_devices_changed = Signal() + + def __init__( + self, + model, + device_type: DeviceType | str = DeviceType.ANY, + parent=None, + *, + classnames: Sequence[str] | None = None, + include_components: bool = False, + ): + super().__init__(parent) + self.model = model + self._device_type = DeviceType(device_type) + self._classnames = frozenset(classnames) if classnames is not None else None + self._include_components = include_components + self._device_names: list[str] = [] + + self.signal_devices_changed.connect(self._refresh) + self.model.events.allowed_devices_changed.connect(self._on_allowed_devices_changed) + + self._refresh() + + def _on_allowed_devices_changed(self, event=None): + # Fired from the model's polling thread; marshal onto the GUI thread. + self.signal_devices_changed.emit() + + def _available_devices(self) -> list[str]: + allowed = getattr(self.model, "_allowed_devices", {}) or {} # noqa: SLF001 + names = _iter_matching_devices( + allowed, + self._device_type, + self._classnames, + include_components=self._include_components, + ) + return sorted(names) + + @Slot() + def _refresh(self): + names = self._available_devices() + if names == self._device_names: + return + self._device_names = names + self._populate(names) + + def _populate(self, names: list[str]): + raise NotImplementedError + + @property + def device_names(self) -> list[str]: + """Names of the devices currently offered for selection.""" + return list(self._device_names) + + def closeEvent(self, event): + try: + self.model.events.allowed_devices_changed.disconnect(self._on_allowed_devices_changed) + except (ValueError, TypeError, RuntimeError): + pass + super().closeEvent(event) + + +class QtDeviceSelector(_DeviceSelectorBase): + """Dropdown for selecting a single device of a given type. + + Parameters + ---------- + model : bluesky_widgets.models.run_engine_client.RunEngineClient + The Queue Server client model (e.g. ``viewer.run_engine``). + device_type : DeviceType or str, optional + Which category of devices to offer. Default: :attr:`DeviceType.ANY`. + parent : QWidget, optional + Parent widget. + classnames : Sequence[str], optional + If given, only devices whose ophyd class name is in this set are offered. + include_components : bool, optional + If True, matching sub-components are offered using dotted names. + title : str, optional + Group box title. Default: derived from ``device_type``. + allow_empty : bool, optional + If True (default), include a placeholder entry representing no selection. + """ + + signal_selection_changed = Signal(object) + + def __init__( + self, + model, + device_type: DeviceType | str = DeviceType.ANY, + parent=None, + *, + classnames: Sequence[str] | None = None, + include_components: bool = False, + title: str | None = None, + allow_empty: bool = True, + ): + self._allow_empty = allow_empty + self._combo = QComboBox() + super().__init__( + model, + device_type, + parent, + classnames=classnames, + include_components=include_components, + ) + + if title is None: + title = f"{self._device_type.value.capitalize()} device" + + self._combo.currentIndexChanged.connect(self._on_index_changed) + + group_box = QGroupBox(title) + inner = QVBoxLayout() + inner.setContentsMargins(8, 6, 8, 6) + inner.addWidget(self._combo) + group_box.setLayout(inner) + + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + def _populate(self, names: list[str]): + previous = self.selected_device + self._combo.blockSignals(True) + self._combo.clear() + if self._allow_empty: + self._combo.addItem(_NO_SELECTION, userData=None) + for name in names: + self._combo.addItem(name, userData=name) + if previous is not None: + index = self._combo.findData(previous) + if index >= 0: + self._combo.setCurrentIndex(index) + self._combo.blockSignals(False) + self._on_index_changed() + + def _on_index_changed(self, *_): + self.signal_selection_changed.emit(self.selected_device) + + @property + def selected_device(self) -> str | None: + """The currently selected device name, or None.""" + return self._combo.currentData() + + def set_selected_device(self, name: str | None): + """Select ``name`` if it is available; otherwise clear the selection.""" + index = self._combo.findData(name) if name is not None else -1 + if index < 0 and self._allow_empty: + index = 0 + if index >= 0: + self._combo.setCurrentIndex(index) + + +class QtMultiDeviceSelector(_DeviceSelectorBase): + """Growable dropdown rows for selecting any number of devices of a type. + + Parameters + ---------- + model : bluesky_widgets.models.run_engine_client.RunEngineClient + The Queue Server client model (e.g. ``viewer.run_engine``). + device_type : DeviceType or str, optional + Which category of devices to offer. Default: :attr:`DeviceType.ANY`. + parent : QWidget, optional + Parent widget. + classnames : Sequence[str], optional + If given, only devices whose ophyd class name is in this set are offered. + include_components : bool, optional + If True, matching sub-components are offered using dotted names. + title : str, optional + Group box title. Default: derived from ``device_type``. + """ + + signal_selection_changed = Signal(object) + + def __init__( + self, + model, + device_type: DeviceType | str = DeviceType.ANY, + parent=None, + *, + classnames: Sequence[str] | None = None, + include_components: bool = False, + title: str | None = None, + ): + self._selector = DynamicDeviceSelector() + super().__init__( + model, + device_type, + parent, + classnames=classnames, + include_components=include_components, + ) + + if title is None: + title = f"{self._device_type.value.capitalize()} devices" + + self._selector.selection_changed.connect(self._on_selection_changed) + + group_box = QGroupBox(title) + inner = QVBoxLayout() + inner.setContentsMargins(8, 6, 8, 6) + inner.addWidget(self._selector) + group_box.setLayout(inner) + + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + def _populate(self, names: list[str]): + self._selector.set_available(names) + + def _on_selection_changed(self, *_): + self.signal_selection_changed.emit(self.selected_devices) + + @property + def selected_devices(self) -> list[str]: + """Names of the currently selected devices, in row order.""" + return self._selector.selected_devices() + + def set_selected_devices(self, names: Iterable[str]): + """Show one row per device in ``names`` that is available.""" + self._selector.set_selected_devices(names) diff --git a/src/hextools/gui/device_sources.py b/src/hextools/gui/device_sources.py new file mode 100644 index 0000000..a326c76 --- /dev/null +++ b/src/hextools/gui/device_sources.py @@ -0,0 +1,298 @@ +"""Pluggable sources of device names for plan-input widgets. + +A :class:`DeviceSource` answers two questions for a device-typed plan argument: +which device names may be offered, and whether a chosen name is a valid device. +Two implementations are provided: + +* :class:`NamespaceDeviceSource` validates against an in-process namespace + (typically the IPython ``user_ns``), using real ``isinstance`` checks. This + is exact but only works when the GUI runs in the same process as the devices. +* :class:`QueueServerDeviceSource` validates against the devices the Queue + Server currently allows, matching on the reported ophyd class name. This works + out-of-process but relies on class-name matching. + +Both feed candidate names into the selector widgets and can be used to validate +selections before a plan is submitted. Device annotations are passed around as a +tuple of Python classes (e.g. ``(KinetixDetector, PhantomDetector)``); pass +``None`` to mean "any device". +""" + +from __future__ import annotations + + +from collections import deque +from collections.abc import Callable, Iterable, Iterator, Mapping +from abc import ABC, abstractmethod +from typing import Any + +import IPython + +_DEVICE_BASES: tuple[type, ...] +_SIGNAL_BASES: list[type] = [] +try: # ophyd_async devices (used throughout hextools). + from ophyd_async.core import Device as _AsyncDevice + from ophyd_async.core import DeviceVector as _DeviceVector + from ophyd_async.core import Signal as _AsyncSignal + + _BASES: list[type] = [_AsyncDevice] + _SIGNAL_BASES.append(_AsyncSignal) +except Exception: # pragma: no cover - optional dependency layout + _AsyncDevice = _DeviceVector = None # type: ignore[assignment,misc] + _BASES = [] +try: + from ophyd_async.core import DeviceMap as _DeviceMap +except Exception: # pragma: no cover - older ophyd_async + _DeviceMap = None # type: ignore[assignment,misc] +try: # classic ophyd devices, if present. + from ophyd.ophydobj import OphydObject as _OphydObject + from ophyd.signal import Signal as _OphydSignal + + _BASES.append(_OphydObject) + _SIGNAL_BASES.append(_OphydSignal) +except Exception: # pragma: no cover - optional dependency layout + pass +_DEVICE_BASES = tuple(_BASES) +_SIGNAL_TYPES = tuple(_SIGNAL_BASES) + + +def _iter_children(name: str, obj: Any) -> Iterator[tuple[str, Any]]: + """Yield ``(path, child)`` for each direct child device of ``obj``. + + Paths are valid Python expressions relative to the namespace, so they can be + rendered directly into ``RE(plan(...))`` calls. + """ + if _AsyncDevice is not None and isinstance(obj, _AsyncDevice): + for key, child in obj.children(): + if _DeviceVector is not None and isinstance(obj, _DeviceVector) and key.isdigit(): + yield f"{name}[{key}]", child + elif _DeviceMap is not None and isinstance(obj, _DeviceMap) and key in obj: + yield f"{name}[{key!r}]", child + else: + yield f"{name}.{key}", child + return + for key in getattr(obj, "component_names", ()) or (): + try: + child = getattr(obj, key) + except Exception: + continue + if _looks_like_device(child): + yield f"{name}.{key}", child + + +def _walk_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]: + """Map path -> device for all devices in ``namespace`` and their children. + + Each device appears once: breadth-first order means a top-level name wins + over any dotted path to the same object. Among top-level aliases, the one + matching the device's ``name`` is preferred, then alphabetical order. + """ + queue: deque[tuple[str, Any]] = deque( + sorted( + ( + (name, obj) + for name, obj in namespace.items() + if not name.startswith("_") and _looks_like_device(obj) + ), + key=lambda kv: (kv[0] != getattr(kv[1], "name", None), kv[0]), + ) + ) + seen: set[int] = set() + found: dict[str, Any] = {} + while queue: + path, obj = queue.popleft() + if id(obj) in seen: + continue + seen.add(id(obj)) + found[path] = obj + queue.extend(_iter_children(path, obj)) + return found + + +def _looks_like_device(obj: Any) -> bool: + """Check if an object is an instance of an ophyd(-async) device + + Parameters + ---------- + obj : Any + The object to check. + + Returns + ------- + bool + True if the object is an instance of an ophyd(-async) device, False otherwise. + """ + + if _DEVICE_BASES and isinstance(obj, _DEVICE_BASES): + return True + return False + + +class DeviceSource(ABC): + """Abstract source of device names for a device-typed plan argument. + + Subclasses implement :meth:`list_devices` and :meth:`is_valid_device`. The + ``types`` argument, when given, is a tuple of acceptable device classes taken + from the plan's type hints; ``None`` means any device is acceptable. + """ + + @abstractmethod + def list_devices(self, types: tuple[type, ...] | None = None) -> list[str]: + """Return the sorted names of devices matching ``types``.""" + ... + + @abstractmethod + def is_valid_device( + self, name: str, types: tuple[type, ...] | None = None + ) -> bool: + """Return True if ``name`` is a known device matching ``types``.""" + ... + + def subscribe(self, callback: Callable[..., None]) -> None: + """Register ``callback`` to be notified when the device set changes.""" + ... + + def unsubscribe(self, callback: Callable[..., None]) -> None: + """Remove a previously registered ``callback``.""" + ... + + +class NamespaceDeviceSource(DeviceSource): + """Validate device inputs against an in-process (IPython) namespace. + + Parameters + ---------- + namespace : Mapping[str, object], optional + Name-to-object mapping to search. Defaults to the current IPython + ``user_ns`` when available, otherwise an empty mapping. + """ + + def __init__(self, namespace: Mapping[str, Any]): + self._namespace = namespace + + def list_devices(self, types: tuple[type, ...] | None = None) -> list[str]: + """Get a list of device_names matching the given types. + + Parameters + ---------- + types : tuple[type, ...] | None, optional + The acceptable device types. If None, any device type is acceptable. + + Returns + ------- + list[str] + The sorted list of device names matching the given types. + """ + + names = [ + name + for name, obj in _walk_namespace(self._namespace).items() + if self._matches(obj, types) + ] + return sorted(names) + + def is_valid_device( + self, name: str, types: tuple[type, ...] | None = None + ) -> bool: + """Check if the given name corresponds to a valid device of the specified types. + + Parameters + ---------- + name : str + The name of the device to check. + types : tuple[type, ...] | None, optional + The acceptable device types. If None, any device type is acceptable. + + Returns + ------- + bool + True if the device is valid and matches the specified types, False otherwise. + """ + + obj = _walk_namespace(self._namespace).get(name) + if obj is None: + return False + return self._matches(obj, types) + + @staticmethod + def _matches(obj: Any, types: tuple[type, ...] | None) -> bool: + """Check if the object matches the given types or looks like a device. + + Parameters + ---------- + obj : Any + The object to check. + types : tuple[type, ...] | None + The acceptable types. If None, any object that looks like a device is acceptable. + + Returns + ------- + bool + True if the object matches the given types or looks like a device, False otherwise. + """ + + if types: + return isinstance(obj, types) + # Untyped inputs would otherwise list every signal of every device. + return _looks_like_device(obj) and not isinstance(obj, _SIGNAL_TYPES) + + +class QueueServerDeviceSource(DeviceSource): + """Validate device inputs against the Queue Server's allowed devices. + + Parameters + ---------- + model : bluesky_widgets.models.run_engine_client.RunEngineClient + The Queue Server client model (e.g. ``viewer.run_engine``). + """ + + def __init__(self, model): + self.model = model + + def _allowed(self) -> dict: + return getattr(self.model, "_allowed_devices", {}) or {} # noqa: SLF001 + + @staticmethod + def _wanted_classnames(types: tuple[type, ...] | None) -> set[str] | None: + if not types: + return None + return {t.__name__ for t in types} + + def _walk(self) -> dict[str, dict]: + """Map dotted path -> info for allowed devices and their components.""" + found: dict[str, dict] = {} + queue: deque[tuple[str, Any]] = deque(sorted(self._allowed().items())) + while queue: + path, info = queue.popleft() + if not isinstance(info, dict) or path in found: + continue + found[path] = info + for key, child in (info.get("components") or {}).items(): + queue.append((f"{path}.{key}", child)) + return found + + def list_devices(self, types: tuple[type, ...] | None = None) -> list[str]: + wanted = self._wanted_classnames(types) + names = [ + name + for name, info in self._walk().items() + if wanted is None or info.get("classname") in wanted + ] + return sorted(names) + + def is_valid_device( + self, name: str, types: tuple[type, ...] | None = None + ) -> bool: + info = self._walk().get(name) + if not isinstance(info, dict): + return False + wanted = self._wanted_classnames(types) + return wanted is None or info.get("classname") in wanted + + def subscribe(self, callback: Callable[..., None]) -> None: + self.model.events.allowed_devices_changed.connect(callback) + + def unsubscribe(self, callback: Callable[..., None]) -> None: + try: + self.model.events.allowed_devices_changed.disconnect(callback) + except (ValueError, TypeError, RuntimeError): + pass diff --git a/src/hextools/gui/live_re_md_viewer.py b/src/hextools/gui/live_re_md_viewer.py new file mode 100644 index 0000000..e3fc3f9 --- /dev/null +++ b/src/hextools/gui/live_re_md_viewer.py @@ -0,0 +1,170 @@ +"""Qt widgets that display live Run Engine metadata. + +Two sources of RE metadata are supported, sharing a common base class that owns +the UI and polling loop: + +* :class:`QtReMetadataMonitor` polls the Queue Server ``re_metadata`` API. Use + this when driving the beamline through the Queue Server. +* :class:`QtReMetadataMonitorLocal` reads ``RE.md`` from a RunEngine obtained + from the IPython namespace. Use this when running acquisition in-process. + +Display is best effort: a missing key, a closed environment, an absent +RunEngine, or a failed request renders as a placeholder rather than raising. +""" + +from __future__ import annotations + +from collections.abc import Mapping, MutableMapping, Sequence +from typing import Any + +from bluesky import RunEngine +from bluesky_widgets.qt.threading import FunctionWorker +from qtpy.QtCore import Qt, QTimer, Signal, Slot +from qtpy.QtWidgets import QFormLayout, QGroupBox, QLabel, QVBoxLayout, QWidget +from bluesky_widgets.models.run_engine_client import RunEngineClient + +RunEngineMetadata = MutableMapping[str, Any] + +_PLACEHOLDER = "\u2014" # em dash + + +class QtReMetadataMonitor(QWidget): + """Poll a source of RE metadata and display selected keys (best effort). + + Subclasses implement :meth:`_fetch_metadata` to return the current metadata + dict. The base class owns the label grid, the polling timer, and the + off-thread refresh worker. + + Parameters + ---------- + keys : Sequence[str] + RE metadata keys to display. The order is preserved. + parent : QWidget, optional + Parent widget. + poll_period : float, optional + Interval between metadata requests, in seconds. Default: 1.0. + title : str, optional + Group box title. Default: ``"RE Metadata"``. + """ + + signal_metadata_updated = Signal(object) + + def __init__( + self, + re: RunEngineClient | RunEngine, + keys: Sequence[str] | dict[str, str], + parent: QWidget | None = None, + *, + poll_period: float = 1.0, + title: str = "RE Metadata", + ): + super().__init__(parent) + self._re = re + self._keys = list(keys) if isinstance(keys, Sequence) else list(keys.values()) + self._key_labels = list(keys.keys()) if isinstance(keys, dict) else keys + self._worker = None + + self._labels: dict[str, QLabel] = {} + form = QFormLayout() + form.setContentsMargins(8, 6, 8, 6) + form.setHorizontalSpacing(12) + form.setVerticalSpacing(6) + for i, key in enumerate(self._keys): + label = QLabel(_PLACEHOLDER) + label.setTextInteractionFlags(Qt.TextSelectableByMouse) + self._labels[key] = label + form.addRow(f"{self._key_labels[i]}:", label) + + group_box = QGroupBox(title) + group_box.setLayout(form) + + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + self.signal_metadata_updated.connect(self._slot_metadata_updated) + + self._timer = QTimer(self) + self._timer.setInterval(int(poll_period * 1000)) + self._timer.timeout.connect(self._start_worker) + self._timer.start() + + # Kick off an immediate refresh instead of waiting for the first tick. + self._start_worker() + + def _fetch_metadata(self) -> RunEngineMetadata: + """Return the current RE metadata dict, or an empty dict on failure.""" + if isinstance(self._re, RunEngineClient): + try: + response = self._re._client.re_metadata() # noqa: SLF001 + except Exception: + return {} + if not response.get("success", False): + return {} + return response.get("re_metadata") or {} + elif isinstance(self._re, RunEngine): + return self._re.md + + raise RuntimeError("RE Metadata Viewer widget must be initialized with a RunEngineClient or RunEngine instance.") + + def _start_worker(self): + # Skip if a request is still in flight to avoid overlapping polls. + if self._worker is not None: + return + self._worker = FunctionWorker(self._fetch_metadata) + self._worker.returned.connect(self._on_worker_returned) + self._worker.start() + + def _on_worker_returned(self, metadata): + self._worker = None + self.signal_metadata_updated.emit(metadata) + + @Slot(object) + def _slot_metadata_updated(self, metadata: dict): + + def get_nested_value(metadata: dict, key: str): + keys = key.split("/") + value = metadata + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + return None + return value + + for key, label in self._labels.items(): + value = get_nested_value(metadata, key) + if value is not None: + label.setText(str(value)) + else: + label.setText(_PLACEHOLDER) + + def closeEvent(self, event): + self._timer.stop() + super().closeEvent(event) + + +class QtProposalInfo(QtReMetadataMonitor): + """Widget to display proposal information from the RunEngine metadata.""" + + def __init__( + self, + re: RunEngineClient | RunEngine, + parent: QWidget | None = None, + *, + poll_period: float = 1.0, + ): + + super().__init__( + re, + { + "Proposal ID": "data_session", + "Cycle": "cycle", + "Proposal Title": "proposal/title", + "Proposal Type": "proposal/type", + "PI Name": "proposal/pi_name" + }, + poll_period=poll_period, + parent=parent, + title="Proposal Info" + ) \ No newline at end of file diff --git a/src/hextools/gui/misc/__init__.py b/src/hextools/gui/misc/__init__.py new file mode 100644 index 0000000..b8f8584 --- /dev/null +++ b/src/hextools/gui/misc/__init__.py @@ -0,0 +1,2 @@ +from .weather import QtWeatherWidget +from .tabbed_detectors_widget import QtTabbedDetectorsWidget \ No newline at end of file diff --git a/src/hextools/gui/misc/tabbed_detectors_widget.py b/src/hextools/gui/misc/tabbed_detectors_widget.py new file mode 100644 index 0000000..d8e10ae --- /dev/null +++ b/src/hextools/gui/misc/tabbed_detectors_widget.py @@ -0,0 +1,115 @@ +from bluesky_widgets.models.run_engine_client import RunEngineClient +from IPython.core.getipython import get_ipython +from ntnda_qt_viewer import NTNDAViewerWidget +from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import QHBoxLayout, QTabWidget, QWidget + +from hextools.gui.device_sources import ( + DeviceSource, + NamespaceDeviceSource, + QueueServerDeviceSource, +) + + +class QtTabbedDetectorsWidget(QTabWidget): + """Tabbed NTNDArray live viewers, one tab per available detector. + + Parameters + ---------- + re_client : RunEngineClient or RunEngine + Queue Server client, or the in-process RunEngine. Determines where + device availability is checked. + detectors : dict[str, str] + Device name -> NTNDArray PV prefix. A detector's tab is only shown + while a device with that name is available. + combined : dict[str, list[str]], optional + Tab label -> detector names (keys of ``detectors``) shown side by side + in one extra tab, after the individual ones. Shown only while all of + its detectors are available. + show_rois : bool, optional + Whether to show the viewers' ROI controls. + show_profile_lines : bool, optional + Whether to show the viewers' crosshair profile lines. + """ + + _signal_devices_changed = Signal() + + def __init__( + self, + re_client, + detectors: dict[str, str], + *args, + combined: dict[str, list[str]] | None = None, + show_rois: bool = False, + show_profile_lines: bool = False, + **kwargs, + ): + super().__init__(*args, **kwargs) + self._detectors = dict(detectors) + self._combined = dict(combined or {}) + self._show_rois = show_rois + self._show_profile_lines = show_profile_lines + self._viewers: dict[str, NTNDAViewerWidget] = {} + self._combined_tabs: dict[str, QWidget] = {} + self._source = self._make_source(re_client) + + self._signal_devices_changed.connect(self._refresh) + self._source.subscribe(self._on_devices_changed) + self._refresh() + + @staticmethod + def _make_source(re_client) -> DeviceSource: + if isinstance(re_client, RunEngineClient): + return QueueServerDeviceSource(re_client) + ipython = get_ipython() + return NamespaceDeviceSource(ipython.user_ns if ipython is not None else {}) + + def _on_devices_changed(self, event=None): + # May fire from the Queue Server polling thread; marshal onto the GUI thread. + self._signal_devices_changed.emit() + + @Slot() + def _refresh(self): + available = {name: self._source.is_valid_device(name) for name in self._detectors} + for name, prefix in self._detectors.items(): + viewer = self._viewers.get(name) + if viewer is None: + if not available[name]: + continue + # Created lazily so unavailable detectors open no PV connections. + viewer = self._make_viewer(prefix) + self._viewers[name] = viewer + self.insertTab(self._tab_position(name), viewer, name) + self.setTabVisible(self.indexOf(viewer), available[name]) + + for label, names in self._combined.items(): + all_available = all(available.get(name, False) for name in names) + tab = self._combined_tabs.get(label) + if tab is None: + if not all_available: + continue + # Separate viewer instances: a widget can only live in one tab. + tab = QWidget() + row = QHBoxLayout(tab) + row.setContentsMargins(0, 0, 0, 0) + for name in names: + row.addWidget(self._make_viewer(self._detectors[name]), stretch=1) + self._combined_tabs[label] = tab + self.addTab(tab, label) + self.setTabVisible(self.indexOf(tab), all_available) + + def _make_viewer(self, prefix: str) -> NTNDAViewerWidget: + viewer = NTNDAViewerWidget(prefix=prefix) + viewer._roi_controls_widget.setVisible(self._show_rois) # noqa: SLF001 + # The viewer's flag defaults off but its lines start visible; sync them. + viewer._on_profile_lines_toggled(self._show_profile_lines) # noqa: SLF001 + return viewer + + def _tab_position(self, name: str) -> int: + """Index that keeps tabs in the order the detectors were given.""" + order = list(self._detectors) + return sum(1 for other in order[: order.index(name)] if other in self._viewers) + + def closeEvent(self, event): + self._source.unsubscribe(self._on_devices_changed) + super().closeEvent(event) diff --git a/src/hextools/gui/misc/weather.py b/src/hextools/gui/misc/weather.py new file mode 100644 index 0000000..aaa3f21 --- /dev/null +++ b/src/hextools/gui/misc/weather.py @@ -0,0 +1,198 @@ +"""A small Qt widget showing the date, time, and current weather for Upton, NY. + +The clock (date and time) updates every second from the local machine, using the +``America/New_York`` timezone so it matches the site. Weather (temperature and a +short condition) is fetched periodically from the free Open-Meteo API, which +needs no API key. Display is best effort: a failed request leaves the previous +reading in place and shows a placeholder until the next successful fetch. +""" + +from __future__ import annotations + +import json +import urllib.parse +import urllib.request +from datetime import datetime +from zoneinfo import ZoneInfo + +from bluesky_widgets.qt.threading import FunctionWorker +from qtpy.QtCore import Qt, QTimer, Signal, Slot +from qtpy.QtWidgets import QFormLayout, QGroupBox, QLabel, QVBoxLayout, QWidget + +_PLACEHOLDER = "\u2014" # em dash + +# Brookhaven National Laboratory, Upton, NY. +_LATITUDE = 40.8677 +_LONGITUDE = -72.8814 +_TIMEZONE = ZoneInfo("America/New_York") +_API_URL = "https://api.open-meteo.com/v1/forecast" + +# WMO weather interpretation codes -> (description, emoji). +_WMO_CODES: dict[int, tuple[str, str]] = { + 0: ("Clear sky", "\u2600\ufe0f"), + 1: ("Mainly clear", "\U0001f324\ufe0f"), + 2: ("Partly cloudy", "\u26c5"), + 3: ("Overcast", "\u2601\ufe0f"), + 45: ("Fog", "\U0001f32b\ufe0f"), + 48: ("Rime fog", "\U0001f32b\ufe0f"), + 51: ("Light drizzle", "\U0001f327\ufe0f"), + 53: ("Drizzle", "\U0001f327\ufe0f"), + 55: ("Dense drizzle", "\U0001f327\ufe0f"), + 56: ("Freezing drizzle", "\U0001f327\ufe0f"), + 57: ("Freezing drizzle", "\U0001f327\ufe0f"), + 61: ("Light rain", "\U0001f327\ufe0f"), + 63: ("Rain", "\U0001f327\ufe0f"), + 65: ("Heavy rain", "\U0001f327\ufe0f"), + 66: ("Freezing rain", "\U0001f327\ufe0f"), + 67: ("Freezing rain", "\U0001f327\ufe0f"), + 71: ("Light snow", "\U0001f328\ufe0f"), + 73: ("Snow", "\U0001f328\ufe0f"), + 75: ("Heavy snow", "\U0001f328\ufe0f"), + 77: ("Snow grains", "\U0001f328\ufe0f"), + 80: ("Rain showers", "\U0001f326\ufe0f"), + 81: ("Rain showers", "\U0001f326\ufe0f"), + 82: ("Violent rain showers", "\u26c8\ufe0f"), + 85: ("Snow showers", "\U0001f328\ufe0f"), + 86: ("Heavy snow showers", "\U0001f328\ufe0f"), + 95: ("Thunderstorm", "\u26c8\ufe0f"), + 96: ("Thunderstorm, hail", "\u26c8\ufe0f"), + 99: ("Thunderstorm, hail", "\u26c8\ufe0f"), +} + + +def _describe(code: int) -> str: + description, emoji = _WMO_CODES.get(code, ("Unknown", "")) + return f"{emoji} {description}".strip() + + +class QtWeatherWidget(QWidget): + """Display the current date, time, temperature, and weather for Upton, NY. + + Parameters + ---------- + parent : QWidget, optional + Parent widget. + weather_period : float, optional + Interval between weather requests, in seconds. Default: 600 (10 minutes). + title : str, optional + Group box title. Default: ``"Upton, NY"``. + """ + + signal_weather_updated = Signal(object) + + def __init__( + self, + parent=None, + *, + weather_period: float = 600.0, + title: str = "Upton, NY", + ): + super().__init__(parent) + self._worker = None + + self._date_label = QLabel(_PLACEHOLDER) + self._time_label = QLabel(_PLACEHOLDER) + self._temp_label = QLabel(_PLACEHOLDER) + self._condition_label = QLabel(_PLACEHOLDER) + for label in ( + self._date_label, + self._time_label, + self._temp_label, + self._condition_label, + ): + label.setTextInteractionFlags(Qt.TextSelectableByMouse) + + form = QFormLayout() + form.setContentsMargins(8, 6, 8, 6) + form.setHorizontalSpacing(12) + form.setVerticalSpacing(6) + form.addRow("Date:", self._date_label) + form.addRow("Time:", self._time_label) + form.addRow("Temperature:", self._temp_label) + form.addRow("Condition:", self._condition_label) + + group_box = QGroupBox(title) + group_box.setLayout(form) + + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + self.signal_weather_updated.connect(self._slot_weather_updated) + + # Clock ticks once a second; weather refreshes on a slower cadence. + self._clock_timer = QTimer(self) + self._clock_timer.setInterval(1000) + self._clock_timer.timeout.connect(self._update_clock) + self._clock_timer.start() + self._update_clock() + + self._weather_timer = QTimer(self) + self._weather_timer.setInterval(int(weather_period * 1000)) + self._weather_timer.timeout.connect(self._start_worker) + self._weather_timer.start() + self._start_worker() + + def _update_clock(self): + now = datetime.now(_TIMEZONE) + self._date_label.setText(now.strftime("%A, %B %d, %Y")) + self._time_label.setText(now.strftime("%I:%M:%S %p %Z")) + + def _start_worker(self): + # Skip if a request is still in flight to avoid overlapping polls. + if self._worker is not None: + return + self._worker = FunctionWorker(self._request_weather) + self._worker.returned.connect(self._on_worker_returned) + self._worker.start() + + def _request_weather(self) -> dict: + """Return the current-weather dict, or an empty dict on any failure.""" + query = urllib.parse.urlencode( + { + "latitude": _LATITUDE, + "longitude": _LONGITUDE, + "current": "temperature_2m,weather_code", + "temperature_unit": "fahrenheit", + "timezone": "America/New_York", + } + ) + try: + with urllib.request.urlopen(f"{_API_URL}?{query}", timeout=10) as response: + payload = json.load(response) + except Exception: + return {} + return payload.get("current") or {} + + def _on_worker_returned(self, current): + self._worker = None + self.signal_weather_updated.emit(current) + + @Slot(object) + def _slot_weather_updated(self, current: dict): + temperature = current.get("temperature_2m") + if temperature is not None: + self._temp_label.setText(f"{temperature:g} \u00b0F") + else: + self._temp_label.setText(_PLACEHOLDER) + + code = current.get("weather_code") + if code is not None: + self._condition_label.setText(_describe(int(code))) + else: + self._condition_label.setText(_PLACEHOLDER) + + def closeEvent(self, event): + self._clock_timer.stop() + self._weather_timer.stop() + super().closeEvent(event) + + +if __name__ == "__main__": + from qtpy.QtWidgets import QApplication + + app = QApplication([]) + widget = QtWeatherWidget() + widget.resize(280, 160) + widget.show() + app.exec_() diff --git a/src/hextools/gui/plan_status.py b/src/hextools/gui/plan_status.py new file mode 100644 index 0000000..765d02b --- /dev/null +++ b/src/hextools/gui/plan_status.py @@ -0,0 +1,759 @@ +"""Widgets showing the running plan, its natural-language narration, and history.""" + +from __future__ import annotations + +import enum +import importlib +import inspect +import json +import logging +import os +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Any + +from bluesky import RunEngine +from bluesky.callbacks.best_effort import BestEffortCallback, hinted_fields +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg +from matplotlib.figure import Figure +from qtpy.QtCore import QObject, Qt, QTimer, Signal, Slot +from qtpy.QtGui import QFont, QFontDatabase +from qtpy.QtWidgets import ( + QAbstractItemView, + QFormLayout, + QGroupBox, + QHBoxLayout, + QHeaderView, + QLabel, + QListView, + QListWidget, + QMessageBox, + QPlainTextEdit, + QPushButton, + QSizePolicy, + QSplitter, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QVBoxLayout, + QWidget, +) + +from hextools.gui._ipython import run_in_ipython +from hextools.gui.device_sources import _walk_namespace +from hextools.utils.msg_hooks import MsgHookNarrator, nl_msg_hook + +_NO_PLAN = "\u2014" # em dash +_NOT_APPLICABLE = "N/A" +_EXIT_STATUS_LABELS = {"success": "Success", "fail": "Failed", "abort": "Aborted"} + +logger = logging.getLogger(__name__) + + +class _Unrenderable(Exception): + """A plan argument has no expression that would recreate it in the namespace.""" + + +def _render(value: Any, names: Mapping[int, str]) -> str: + """Return a Python expression for ``value`` that evaluates in the namespace.""" + if id(value) in names: + return names[id(value)] + if isinstance(value, enum.Enum): + if isinstance(value, str): + return repr(value.value) + raise _Unrenderable + if value is None or isinstance(value, (bool, int, float, complex, str, bytes)): + return repr(value) + if hasattr(value, "dtype") and hasattr(value, "item") and getattr(value, "ndim", None) == 0: + return repr(value.item()) # numpy scalar + if isinstance(value, list): + return "[" + ", ".join(_render(v, names) for v in value) + "]" + if isinstance(value, tuple): + inner = ", ".join(_render(v, names) for v in value) + return f"({inner},)" if len(value) == 1 else f"({inner})" + if isinstance(value, dict): + return "{" + ", ".join(f"{_render(k, names)}: {_render(v, names)}" for k, v in value.items()) + "}" + raise _Unrenderable + + +def _display(value: Any, names: Mapping[int, str]) -> str: + """Like :func:`_render`, but always returns something readable.""" + try: + return _render(value, names) + except _Unrenderable: + name = getattr(value, "name", None) + return name if isinstance(name, str) else repr(value) + + +@dataclass +class PlanRecord: + """A plan run through the RunEngine, as captured just before it started.""" + + name: str + arguments: str + rerunnable: bool + func: Callable | None = None + scan_ids: list = field(default_factory=list) + uids: list[str] = field(default_factory=list) + exit_status: str | None = None + start_time: float | None = None + stop_time: float | None = None + + @property + def call(self) -> str: + return f"{self.name}({self.arguments})" + + def to_json(self) -> dict[str, Any]: + func = None + if self.func is not None: + func = f"{self.func.__module__}:{self.func.__qualname__}" + return { + "name": self.name, + "arguments": self.arguments, + "rerunnable": self.rerunnable, + "function": func, + "scan_ids": self.scan_ids, + "uids": self.uids, + "exit_status": self.exit_status, + "start_time": self.start_time, + "stop_time": self.stop_time, + } + + @classmethod + def from_json(cls, data: dict[str, Any]) -> PlanRecord: + func = _import_function(data.get("function")) + return cls( + name=data["name"], + arguments=data["arguments"], + rerunnable=bool(data.get("rerunnable")) and func is not None, + func=func, + scan_ids=list(data.get("scan_ids") or []), + uids=list(data.get("uids") or []), + exit_status=data.get("exit_status"), + start_time=data.get("start_time"), + stop_time=data.get("stop_time"), + ) + + +def _import_function(reference: str | None) -> Callable | None: + """Resolve a ``module:qualname`` reference, or return None if it no longer exists.""" + if not reference: + return None + module_name, _, qualname = reference.partition(":") + try: + obj: Any = importlib.import_module(module_name) + for part in qualname.split("."): + obj = getattr(obj, part) + except (ImportError, AttributeError): + return None + return obj if callable(obj) else None + + +def _record_plan(plan: Any, namespace: Mapping[str, Any]) -> PlanRecord: + """Capture a plan's name and arguments before its first message is sent.""" + gen = getattr(plan, "_iter", plan) # unwrap bluesky's @plan ``Plan`` object + if not inspect.isgenerator(gen): + name = getattr(plan, "__name__", None) or type(plan).__name__ + return PlanRecord(name=name, arguments="...", rerunnable=False) + + code = gen.gi_code + # Before the first send, the generator's locals are exactly its arguments. + args = inspect.getgeneratorlocals(gen) + names = {id(obj): path for path, obj in _walk_namespace(namespace).items()} + + varnames = code.co_varnames + n_pos, n_kwonly = code.co_argcount, code.co_kwonlyargcount + positional = varnames[:n_pos] + kwonly = varnames[n_pos : n_pos + n_kwonly] + index = n_pos + n_kwonly + varargs = varkw = None + if code.co_flags & inspect.CO_VARARGS: + varargs = varnames[index] + index += 1 + if code.co_flags & inspect.CO_VARKEYWORDS: + varkw = varnames[index] + + # (keyword or None for positional, value) in call order. + items: list[tuple[str | None, Any]] = [] + extra_positional = args.get(varargs, ()) if varargs else () + for i, param in enumerate(positional): + as_positional = i < code.co_posonlyargcount or bool(extra_positional) + items.append((None if as_positional else param, args[param])) + items.extend((None, v) for v in extra_positional) + items.extend((param, args[param]) for param in kwonly) + if varkw: + items.extend(args.get(varkw, {}).items()) + + rerunnable = True + parts = [] + for key, value in items: + try: + text = _render(value, names) + except _Unrenderable: + rerunnable = False + text = _display(value, names) + parts.append(text if key is None else f"{key}={text}") + + func = gen.gi_frame.f_globals.get(code.co_name) if gen.gi_frame is not None else None + # Only re-run via a module-level function whose (unwrapped) code is this generator's. + if not callable(func) or getattr(inspect.unwrap(func), "__code__", None) is not code: + func = None + return PlanRecord( + name=code.co_name, + arguments=", ".join(parts), + rerunnable=rerunnable and func is not None, + func=func, + ) + + +def _monospace_font() -> QFont: + return QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + + +class PlanMonitor(QObject): + """Bridges RunEngine activity to Qt signals on the GUI thread. + + Attaches ``narrator`` as the RunEngine ``msg_hook`` and chains onto any + existing ``msg_hook`` and ``state_hook`` rather than replacing them. + """ + + line = Signal(float, str) + plan_started = Signal(str, str) + plan_finished = Signal(object) + run_started = Signal(object, str) + document = Signal(str, object) + + def __init__( + self, + re: RunEngine, + narrator: MsgHookNarrator = nl_msg_hook, + namespace: Mapping[str, Any] | None = None, + parent=None, + ): + super().__init__(parent) + self._re = re + self._narrator = narrator + self._namespace = namespace if namespace is not None else {} + self._current: PlanRecord | None = None + narrator.add_listener(self._on_line) + + previous_msg_hook = re.msg_hook + if previous_msg_hook is not narrator: + + def msg_hook(msg): + if previous_msg_hook is not None: + previous_msg_hook(msg) + narrator(msg) + + re.msg_hook = msg_hook + + previous_state_hook = re.state_hook + + def state_hook(new_state, old_state): + self._on_state(str(new_state), str(old_state)) + if previous_state_hook is not None: + previous_state_hook(new_state, old_state) + + re.state_hook = state_hook + self._start_token = re.subscribe(self._on_start, "start") + self._document_token = re.subscribe(self._on_document) + + @property + def namespace(self) -> Mapping[str, Any]: + return self._namespace + + @property + def run_engine(self) -> RunEngine: + return self._re + + # These run on the RunEngine thread; Qt queues the signals onto the GUI thread. + def _on_line(self, text: str): + self.line.emit(datetime.now().timestamp(), text) + + def _on_state(self, new_state: str, old_state: str): + if new_state == "running" and old_state == "idle": + plan = getattr(self._re, "_plan", None) # noqa: SLF001 + self._current = _record_plan(plan, self._namespace) + self._current.start_time = time.time() + self.plan_started.emit(self._current.name, self._current.call) + elif new_state == "idle": + self._narrator.flush() + record, self._current = self._current, None + if record is not None: + record.stop_time = time.time() + record.exit_status = getattr(self._re, "_exit_status", None) # noqa: SLF001 + self.plan_finished.emit(record) + + def _on_start(self, name, doc): + scan_id, uid = doc.get("scan_id"), doc.get("uid", "") + if self._current is not None: + self._current.scan_ids.append(scan_id) + self._current.uids.append(uid) + self.run_started.emit(scan_id, uid) + + def _on_document(self, name, doc): + self.document.emit(name, doc) + + +class QtPlanStatus(QWidget): + """Compact summary: plan name, scan id, and latest narration.""" + + def __init__(self, monitor: PlanMonitor, parent=None): + super().__init__(parent) + self._plan = QLabel(_NO_PLAN) + self._scan_id = QLabel(_NO_PLAN) + self._last = QLabel(_NO_PLAN) + self._last.setWordWrap(True) + self._last.setMinimumWidth(260) + # Wrap to the available width rather than widening the window. + self._last.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) + + form = QFormLayout() + form.setContentsMargins(8, 6, 8, 6) + form.addRow("Plan:", self._plan) + form.addRow("Scan ID:", self._scan_id) + form.addRow(QLabel("Last step:")) + form.addRow(self._last) + + group_box = QGroupBox("Plan Status") + group_box.setLayout(form) + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + monitor.plan_started.connect(self._on_plan_started) + monitor.run_started.connect(self._on_run_started) + monitor.line.connect(self._on_line) + + @Slot(str, str) + def _on_plan_started(self, name: str, call: str): + self._plan.setText(name) + self._scan_id.setText(_NO_PLAN) + self._last.setText(_NO_PLAN) + + @Slot(object, str) + def _on_run_started(self, scan_id, uid: str): + self._scan_id.setText(_NO_PLAN if scan_id is None else str(scan_id)) + + @Slot(float, str) + def _on_line(self, timestamp: float, text: str): + self._last.setText(text) + + +class QtLiveTable(QWidget): + """Table equivalent of the BestEffortCallback's LiveTable. + + Shows the same columns (seq_num, time, then the hinted dimension and data + fields of the primary stream) with the same number formatting. + """ + + _DEFAULT_PRECISION = 3 + + def __init__(self, parent=None): + super().__init__(parent) + self._start_doc: dict | None = None + self._all_dim_fields: list[str] = [] + self._dim_fields: list[str] = [] + self._dim_stream = "primary" + self._cleanup_motor_heuristic = False + self._descriptor_uid: str | None = None + self._fields: list[str] = [] + self._data_keys: dict = {} + + self._table = QTableWidget(0, 0) + self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self._table.verticalHeader().setVisible(False) + self._table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self._table.setFont(_monospace_font()) + self._footer = QLabel("") + self._footer.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + + vbox = QVBoxLayout(self) + vbox.setContentsMargins(0, 0, 0, 0) + vbox.addWidget(self._table, stretch=1) + vbox.addWidget(self._footer) + + @Slot(str, object) + def on_document(self, name: str, doc: dict): + handler = getattr(self, f"_on_{name}", None) + if handler is not None: + handler(doc) + + def _on_start(self, doc: dict): + # Mirrors BestEffortCallback.start's choice of dimensions. + self._start_doc = doc + motors = doc.get("motors") + guess = [([m], "primary") for m in motors] if motors else [(["time"], "primary")] + dimensions = doc.get("hints", {}).get("dimensions") + self._cleanup_motor_heuristic = dimensions is None + if dimensions is None or len({d[1] for d in dimensions}) != 1: + self._cleanup_motor_heuristic = True + dimensions = guess + self._dim_fields = [fields[0] if fields else "time" for fields, _ in dimensions] + self._all_dim_fields = [field for fields, _ in dimensions for field in fields] + self._dim_stream = dimensions[0][1] + self._descriptor_uid = None + self._table.setRowCount(0) + self._table.setColumnCount(0) + self._footer.setText("") + + def _on_descriptor(self, doc: dict): + if doc.get("name", "primary") != self._dim_stream: + return + if self._cleanup_motor_heuristic: + # The start doc only named objects; resolve them to fields now. + self._cleanup_motor_heuristic = False + fixed = [] + for obj_name in self._dim_fields: + if obj_name == "time": + fixed.append("time") + continue + hint = doc.get("hints", {}).get(obj_name, {}) + fixed.extend(hint.get("fields", doc.get("object_keys", {}).get(obj_name, []))) + self._dim_fields = fixed + columns = [c for c in hinted_fields(doc) if c not in self._all_dim_fields] + data_keys = doc.get("data_keys", {}) + # Like LiveTable: only fields this stream has, in a dtype it can display. + self._fields = [ + f + for f in list(self._all_dim_fields) + columns + if f in data_keys and data_keys[f].get("dtype") in ("number", "integer", "string", "boolean") + ] + self._data_keys = data_keys + self._descriptor_uid = doc["uid"] + headers = ["seq_num", "time", *self._fields] + self._table.setRowCount(0) + self._table.setColumnCount(len(headers)) + self._table.setHorizontalHeaderLabels(headers) + + def _on_event(self, doc: dict): + if doc.get("descriptor") != self._descriptor_uid: + return + data = doc.get("data", {}) + filled = doc.get("filled", {}) + cells = [str(doc.get("seq_num", "")), str(datetime.fromtimestamp(doc["time"]).time())[:10]] + for field in self._fields: + if field in data and filled.get(field, True): + cells.append(self._format(field, data[field])) + else: + cells.append("") + scrollbar = self._table.verticalScrollBar() + at_bottom = scrollbar.value() == scrollbar.maximum() + row = self._table.rowCount() + self._table.insertRow(row) + for column, text in enumerate(cells): + item = QTableWidgetItem(text) + item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + self._table.setItem(row, column, item) + if at_bottom: + self._table.scrollToBottom() + + def _on_stop(self, doc: dict): + start = self._start_doc + if start is None or doc.get("run_start") != start.get("uid"): + return + self._footer.setText( + f"{start.get('plan_type', '')} {start.get('plan_name', '')} " + f"['{start.get('uid', '')[:8]}'] (scan num: {start.get('scan_id', '')})" + ) + + def _format(self, field: str, value: Any) -> str: + if isinstance(value, enum.Enum): + value = value.value + if isinstance(value, bool): + return str(value) + dtype = self._data_keys.get(field, {}).get("dtype") + try: + if dtype == "number": + precision = self._data_keys[field].get("precision", self._DEFAULT_PRECISION) + try: + precision = int(precision) + except (TypeError, ValueError): + precision = self._DEFAULT_PRECISION + return f"{value:.{precision}f}" + if dtype == "integer": + return f"{value:d}" + except (TypeError, ValueError): + pass + return str(value) + + +class QtPlanProgressView(QWidget): + """Live table of the running plan, with the BestEffortCallback's plots beside it. + + Any ``BestEffortCallback`` in the session namespace is redirected to draw its + figures here instead of in separate windows. The plot side stays hidden + until a figure is created. + """ + + def __init__(self, monitor: PlanMonitor, parent=None): + super().__init__(parent) + self._table = QtLiveTable() + + self._plots = QWidget() + self._plots_layout = QVBoxLayout(self._plots) + self._plots_layout.setContentsMargins(0, 0, 0, 0) + self._plots.hide() + + self._splitter = QSplitter(Qt.Orientation.Horizontal) + self._splitter.addWidget(self._table) + self._splitter.addWidget(self._plots) + + vbox = QVBoxLayout(self) + vbox.setContentsMargins(0, 0, 0, 0) + vbox.addWidget(self._splitter) + + monitor.document.connect(self._table.on_document) + monitor.plan_started.connect(self._clear_plots) + for obj in monitor.namespace.values(): + if isinstance(obj, BestEffortCallback): + obj._fig_factory = self._make_figure # noqa: SLF001 + + def _make_figure(self, name: str) -> Figure: + # The BEC is a QtAwareCallback, so this is called on the GUI thread. + figure = Figure(layout="constrained") + figure.set_label(name) + canvas = FigureCanvasQTAgg(figure) + self._plots_layout.addWidget(canvas, stretch=1) + if not self._plots.isVisible(): + self._plots.show() + half = max(1, self._splitter.width() // 2) + self._splitter.setSizes([half, half]) + return figure + + @Slot() + def _clear_plots(self, *_): + while self._plots_layout.count(): + item = self._plots_layout.takeAt(0) + if item is not None and item.widget() is not None: + item.widget().deleteLater() + self._plots.hide() + + +class QtPlanExecutionView(QWidget): + """The running plan's full call, above its narration since it started.""" + + def __init__(self, monitor: PlanMonitor, parent=None): + super().__init__(parent) + self._call = QLabel("No plan has run yet.") + self._call.setFont(_monospace_font()) + self._call.setWordWrap(True) + self._call.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + call_box = QGroupBox("Current Plan") + call_layout = QVBoxLayout() + call_layout.addWidget(self._call) + call_box.setLayout(call_layout) + + self._messages = QListWidget() + self._messages.setWordWrap(True) + # Wrapping only happens once horizontal scrolling is ruled out. + self._messages.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self._messages.setResizeMode(QListView.ResizeMode.Adjust) + + progress_tabs = QTabWidget() + progress_tabs.addTab(QtPlanProgressView(monitor), "Progress") + progress_tabs.addTab(self._messages, "Debug") + + vbox = QVBoxLayout() + vbox.addWidget(call_box) + vbox.addWidget(progress_tabs, stretch=1) + self.setLayout(vbox) + + monitor.plan_started.connect(self._on_plan_started) + monitor.line.connect(self._on_line) + + @Slot(str, str) + def _on_plan_started(self, name: str, call: str): + self._call.setText(call) + self._messages.clear() + + @Slot(float, str) + def _on_line(self, timestamp: float, text: str): + scrollbar = self._messages.verticalScrollBar() + at_bottom = scrollbar.value() == scrollbar.maximum() + self._messages.addItem(f"[{datetime.fromtimestamp(timestamp):%H:%M:%S %H:%M:%S}] {text}") + # Only follow new lines if the user hasn't scrolled up to read history. + if at_bottom: + self._messages.scrollToBottom() + + +class QtPlanLogView(QWidget): + """Every narration line received this session.""" + + _MAX_LINES = 20000 + + def __init__(self, monitor: PlanMonitor, parent=None): + super().__init__(parent) + self._log = QPlainTextEdit() + self._log.setReadOnly(True) + self._log.setFont(_monospace_font()) + self._log.setMaximumBlockCount(self._MAX_LINES) + vbox = QVBoxLayout() + vbox.addWidget(self._log) + self.setLayout(vbox) + + monitor.plan_started.connect(self._on_plan_started) + monitor.line.connect(self._on_line) + + @Slot(str, str) + def _on_plan_started(self, name: str, call: str): + self._append(datetime.now().timestamp(), f"=== {call} ===") + + @Slot(float, str) + def _on_line(self, timestamp: float, text: str): + self._append(timestamp, text) + + def _append(self, timestamp: float, text: str): + stamp = datetime.fromtimestamp(timestamp) + self._log.appendPlainText(f"[{stamp:%Y-%m-%d %H:%M:%S}] {text}") + + +class QtPlanHistory(QWidget): + """Table of completed plans, newest first, with re-run of a selected plan. + + Parameters + ---------- + monitor : PlanMonitor + Source of finished plans. + history_file : str or Path, optional + JSON file the history is saved to after every plan. If it already + exists, its plans are loaded into the table on startup. + """ + + _COLUMNS = ("Start", "Stop", "Plan", "Arguments", "Status", "Scan ID", "UID") + _ARGUMENTS_COLUMN = 3 + + def __init__(self, monitor: PlanMonitor, history_file: str | Path | None = None, parent=None): + super().__init__(parent) + self._monitor = monitor + self._records: list[PlanRecord] = [] # same order as table rows + self._history_file = Path(history_file).expanduser() if history_file else None + + self._table = QTableWidget(0, len(self._COLUMNS)) + self._table.setHorizontalHeaderLabels(self._COLUMNS) + self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self._table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self._table.verticalHeader().setVisible(False) + header = self._table.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(self._ARGUMENTS_COLUMN, QHeaderView.ResizeMode.Stretch) + self._table.itemSelectionChanged.connect(self._update_button) + + self._rerun_button = QPushButton("Re-run Selected Plan") + self._rerun_button.setEnabled(False) + self._rerun_button.clicked.connect(self._on_rerun_clicked) + buttons = QHBoxLayout() + buttons.addStretch() + buttons.addWidget(self._rerun_button) + + vbox = QVBoxLayout() + vbox.addWidget(self._table, stretch=1) + vbox.addLayout(buttons) + self.setLayout(vbox) + + self._load() + monitor.plan_finished.connect(self._on_plan_finished) + + def _load(self): + if self._history_file is None or not self._history_file.exists(): + return + try: + entries = json.loads(self._history_file.read_text()) + records = [PlanRecord.from_json(entry) for entry in entries] + except (OSError, ValueError, KeyError, TypeError) as ex: + # Don't overwrite a file we couldn't read; it may hold history worth recovering. + logger.warning("Not saving plan history: failed to load %s: %s", self._history_file, ex) + self._history_file = None + return + for record in records: # stored oldest first + self._add_row(record) + + def _save(self): + if self._history_file is None: + return + entries = [record.to_json() for record in reversed(self._records)] + tmp = self._history_file.with_name(self._history_file.name + ".tmp") + try: + self._history_file.parent.mkdir(parents=True, exist_ok=True) + tmp.write_text(json.dumps(entries, indent=2, default=str)) + os.replace(tmp, self._history_file) # atomic, so a crash can't truncate the file + except OSError as ex: + logger.warning("Failed to save plan history to %s: %s", self._history_file, ex) + + @staticmethod + def _fmt_time(timestamp: float | None) -> str: + if timestamp is None: + return _NOT_APPLICABLE + return f"{datetime.fromtimestamp(timestamp):%Y-%m-%d %H:%M:%S}" + + @Slot(object) + def _on_plan_finished(self, record: PlanRecord): + self._add_row(record) + self._save() + + def _add_row(self, record: PlanRecord): + scan_ids = [str(s) for s in record.scan_ids if s is not None] + cells = ( + self._fmt_time(record.start_time), + self._fmt_time(record.stop_time), + record.name, + record.arguments, + _EXIT_STATUS_LABELS.get(record.exit_status or "", record.exit_status or _NOT_APPLICABLE), + ", ".join(scan_ids) or _NOT_APPLICABLE, + ", ".join(record.uids) or _NOT_APPLICABLE, + ) + self._table.insertRow(0) + for column, text in enumerate(cells): + item = QTableWidgetItem(text) + item.setToolTip(text) + self._table.setItem(0, column, item) + self._records.insert(0, record) + self._update_button() + + def _selected_record(self) -> PlanRecord | None: + rows = self._table.selectionModel().selectedRows() + return self._records[rows[0].row()] if rows else None + + @Slot() + def _update_button(self): + record = self._selected_record() + self._rerun_button.setEnabled(record is not None and record.rerunnable) + if record is not None and not record.rerunnable: + self._rerun_button.setToolTip( + "This plan can't be re-run: its function or some of its arguments " + "can't be found in the session namespace." + ) + else: + self._rerun_button.setToolTip("") + + def _on_rerun_clicked(self): + record = self._selected_record() + if record is None or not record.rerunnable: + return + if self._monitor.run_engine.state != "idle": + QMessageBox.warning(self, "RunEngine busy", "Wait for the current plan to finish.") + return + namespace = self._monitor.namespace + existing = namespace.get(record.name) + if existing is None: + namespace[record.name] = record.func # type: ignore[index] + elif existing is not record.func: + QMessageBox.warning( + self, + "Name conflict", + f"'{record.name}' in the session refers to a different object than the " + "plan that originally ran, so it won't be re-run.", + ) + return + code = f"RE({record.call})" + # Defer so the click handler returns before IPython executes the cell. + QTimer.singleShot(0, lambda: self._execute(code)) + + def _execute(self, code: str): + error = run_in_ipython(code) + if error is not None: + QMessageBox.critical(self, "Re-run failed", f"{type(error).__name__}: {error}") diff --git a/src/hextools/gui/plan_widget.py b/src/hextools/gui/plan_widget.py new file mode 100644 index 0000000..5b9877a --- /dev/null +++ b/src/hextools/gui/plan_widget.py @@ -0,0 +1,575 @@ +"""A Qt widget that builds a plan-submission form from a plan's type hints. + +:class:`QtPlanWidget` takes any bluesky plan function and inspects its signature +to generate input fields: + +* scalar parameters (``int``, ``float``, ``bool``, ``str`` and their optional + variants) get a matching editor; +* a parameter typed as a *list* of devices (e.g. ``list[KinetixDetector | + PhantomDetector]``) gets a growable device selector; +* a *single* device parameter that is required gets a device dropdown, while a + single device parameter that defaults to ``None`` is treated as auto-resolved + by the plan at run time and is omitted from the form. + +Device fields are populated and validated through a pluggable +:class:`~hextools.gui.device_sources.DeviceSource`, so the same widget can check +inputs against an in-process IPython namespace or the Queue Server's allowed +devices. Depending on :class:`ExecutionMode`, the submit button either adds a +:class:`~bluesky_queueserver_api.BPlan` to the queue or runs the plan in this +process by sending ``RE(plan(...))`` to the IPython shell. +""" + +from __future__ import annotations + +import collections.abc +import html +import inspect +import types +import typing +from enum import Enum + +import docstring_parser +import IPython +from bluesky import RunEngine +from bluesky_queueserver_api import BPlan +from bluesky_widgets.qt.threading import FunctionWorker +from bluesky_widgets.models.run_engine_client import RunEngineClient +from qtpy.QtCore import QEvent, QSize, Qt, QTimer, Signal, Slot +from qtpy.QtGui import QDoubleValidator, QIntValidator +from qtpy.QtWidgets import ( + QCheckBox, + QComboBox, + QFormLayout, + QFrame, + QGroupBox, + QLabel, + QLineEdit, + QPushButton, + QMessageBox, + QScrollArea, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +from hextools.gui._ipython import run_in_ipython +from hextools.gui.device_selector_widget import DynamicDeviceSelector +from hextools.gui.device_sources import ( + DeviceSource, + NamespaceDeviceSource, + QueueServerDeviceSource, +) + + +_NO_SELECTION = "\u2014" # em dash placeholder for an unset single device + + +def _param_tooltips(plan) -> dict[str, str]: + """Map parameter name -> rich-text tooltip from the plan's numpydoc docstring.""" + doc = inspect.getdoc(plan) + if not doc: + return {} + try: + parsed = docstring_parser.parse(doc, style=docstring_parser.DocstringStyle.NUMPYDOC) + except docstring_parser.ParseError: + return {} + tooltips = {} + for param in parsed.params: + if not param.description: + continue + # Keep paragraph breaks but let single newlines reflow. + paragraphs = [" ".join(p.split()) for p in param.description.split("\n\n")] + body = "

".join(html.escape(p) for p in paragraphs) + tooltips[param.arg_name] = f"

{body}

" + return tooltips + + +def _unwrap_optional(annotation): + """Return ``(base_type, is_optional)`` for ``T`` or ``Optional[T]``.""" + origin = typing.get_origin(annotation) + if origin is typing.Union or isinstance(annotation, types.UnionType): + args = list(typing.get_args(annotation)) + non_none = [a for a in args if a is not type(None)] + is_optional = len(non_none) != len(args) + if len(non_none) == 1: + return non_none[0], is_optional + return annotation, is_optional + return annotation, False + + +def _scalar_type(annotation): + """Return int/float/bool/str for a scalar annotation, else None.""" + base, _ = _unwrap_optional(annotation) + if base is bool: + return bool + if base in (int, float, str): + return base + return None + + +def _enum_type(annotation): + """Return the Enum subclass for an enum annotation, else None.""" + base, _ = _unwrap_optional(annotation) + if isinstance(base, type) and issubclass(base, Enum): + return base + return None + + +def _is_list_annotation(annotation) -> bool: + base, _ = _unwrap_optional(annotation) + origin = typing.get_origin(base) + return origin in (list, tuple, set, frozenset) or origin in ( + collections.abc.Sequence, + collections.abc.Iterable, + ) + + +def _device_types(annotation) -> tuple[type, ...] | None: + """Return the concrete device classes named by ``annotation``, or None.""" + base, _ = _unwrap_optional(annotation) + origin = typing.get_origin(base) + if origin is typing.Union or isinstance(base, types.UnionType): + members = [ + a + for a in typing.get_args(base) + if a is not type(None) and isinstance(a, type) + ] + return tuple(members) or None + if isinstance(base, type): + return (base,) + return None + + +def _element_device_types(annotation) -> tuple[type, ...] | None: + """Return the device classes of a list annotation's element type.""" + base, _ = _unwrap_optional(annotation) + args = typing.get_args(base) + if not args: + return None + return _device_types(args[0]) + + +class _VerticalScrollArea(QScrollArea): + """Scrolls vertically only, and is always wide enough for its content plus the scrollbar.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWidgetResizable(True) + self.setFrameShape(QFrame.Shape.NoFrame) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + def setWidget(self, widget: QWidget): + super().setWidget(widget) + widget.installEventFilter(self) + + def eventFilter(self, obj, event): + # Content width changes (e.g. devices populated, rows added) must re-size the area. + if obj is self.widget() and event.type() == QEvent.Type.LayoutRequest: + self.updateGeometry() + return super().eventFilter(obj, event) + + def _content_width(self) -> int: + widget = self.widget() + if widget is None: + return 0 + return ( + widget.minimumSizeHint().width() + + self.verticalScrollBar().sizeHint().width() + + 2 * self.frameWidth() + ) + + def minimumSizeHint(self) -> QSize: + hint = super().minimumSizeHint() + return QSize(max(hint.width(), self._content_width()), hint.height()) + + def sizeHint(self) -> QSize: + hint = super().sizeHint() + return QSize(max(hint.width(), self._content_width()), hint.height()) + + +class QtPlanWidget(QWidget): + """Build and run an arbitrary plan, either in-process or via the Queue Server. + + Parameters + ---------- + model : bluesky_widgets.models.run_engine_client.RunEngineClient or None + The Queue Server client model (e.g. ``viewer.run_engine``). Required for + :attr:`ExecutionMode.QUEUESERVER`; may be ``None`` when running + in-process. + plan : Callable + The plan function whose signature drives the generated form. Its + ``__name__`` is used both as the Queue Server plan name and, for + in-process execution, as the callable looked up in the IPython + namespace. + parent : QWidget, optional + Parent widget. + execution_mode : ExecutionMode or str, optional + Whether the submit button adds the plan to the Queue Server + (:attr:`ExecutionMode.QUEUESERVER`, the default) or runs it in this + process via ``RE(plan(...))`` (:attr:`ExecutionMode.IN_PROCESS`). + device_source : DeviceSource, optional + Source used to populate and validate device fields. Defaults to a + :class:`~hextools.gui.device_sources.QueueServerDeviceSource` wrapping + ``model`` in queueserver mode, or a + :class:`~hextools.gui.device_sources.NamespaceDeviceSource` in + in-process mode. + namespace : Mapping[str, object], optional + Namespace used for in-process validation/execution. Defaults to the + active IPython ``user_ns``. Ignored when ``device_source`` is given. + re_name : str, optional + Name of the ``RunEngine`` object in the namespace for in-process + execution. Default ``"RE"``. + title : str, optional + Group box title. Default: derived from the plan name. + """ + + signal_devices_changed = Signal() + + def __init__( + self, + re_client, + plan, + parent=None, + *, + title: str | None = None, + ): + super().__init__(parent) + self._re_client = re_client + self._plan = plan + if isinstance(re_client, RunEngineClient): + self._device_source = QueueServerDeviceSource(re_client) + else: + ipython = IPython.get_ipython() + if ipython is None: + raise RuntimeError("Failed to detect ipython instance, required for plan argument validation") + self._device_source = NamespaceDeviceSource(ipython.user_ns) + self._worker = None + self._scalar_fields: list[tuple[str, type, QWidget, bool]] = [] + self._enum_fields: list[tuple[str, QComboBox, bool]] = [] + self._device_fields: list[dict] = [] + self._param_tooltips = _param_tooltips(plan) + + if title is None: + title = plan.__name__.replace("_", " ").title() + + outer = QVBoxLayout() + group_box = QGroupBox(title) + vbox = QVBoxLayout() + + # Inputs scroll instead of growing the window; the button and status stay visible. + fields = QWidget() + fields_layout = QVBoxLayout(fields) + fields_layout.setContentsMargins(0, 0, 0, 0) + self._build_fields(fields_layout) + fields_layout.addStretch() + scroll = _VerticalScrollArea() + scroll.setWidget(fields) + vbox.addWidget(scroll, stretch=1) + + button_label = ( + "Run Plan" + if not isinstance(re_client, RunEngineClient) + else "Submit to Queue" + ) + self._submit_button = QPushButton(button_label) + self._submit_button.clicked.connect(self._on_submit_clicked) + vbox.addWidget(self._submit_button) + + self._status_label = QLabel("") + self._status_label.setWordWrap(True) + self._status_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + vbox.addWidget(self._status_label) + + group_box.setLayout(vbox) + outer.addWidget(group_box) + self.setLayout(outer) + + self.signal_devices_changed.connect(self._refresh_devices) + self._device_source.subscribe(self._on_devices_changed) + self._refresh_devices() + + # -- Form construction ----------------------------------------------------- + + def _add_row(self, form: QFormLayout, name: str, widget: QWidget): + label = QLabel(f"{name.replace('_', ' ').title()}:") + tooltip = self._param_tooltips.get(name) + if tooltip: + label.setToolTip(tooltip) + widget.setToolTip(tooltip) + form.addRow(label, widget) + + def _build_fields(self, vbox: QVBoxLayout): + form = QFormLayout() + form.setContentsMargins(8, 6, 8, 6) + form.setHorizontalSpacing(12) + form.setVerticalSpacing(6) + + parameters = inspect.signature(self._plan).parameters + for name, param in parameters.items(): + if param.kind in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + continue + annotation = param.annotation + required = param.default is inspect.Parameter.empty + + kind = _scalar_type(annotation) + if kind is not None: + self._add_scalar_field(form, name, kind, param, required) + continue + + enum_cls = _enum_type(annotation) + if enum_cls is not None: + self._add_enum_field(form, name, enum_cls, param, required) + continue + + if _is_list_annotation(annotation): + types_ = _element_device_types(annotation) + self._add_device_list_field(vbox, name, types_, required) + continue + + # Single device parameter. + if required: + types_ = _device_types(annotation) + self._add_single_device_field(form, name, types_) + # Optional single device (default None): resolved by the plan. + + if form.rowCount(): + params_box = QGroupBox("Parameters") + params_box.setLayout(form) + vbox.addWidget(params_box) + + def _add_scalar_field(self, form, name, kind, param, required): + if kind is bool: + widget = QCheckBox() + widget.setChecked(bool(param.default) if not required else False) + else: + widget = QLineEdit() + if kind is int: + widget.setValidator(QIntValidator(widget)) + elif kind is float: + validator = QDoubleValidator(widget) + validator.setNotation(QDoubleValidator.StandardNotation) + widget.setValidator(validator) + if not required and param.default is not None: + widget.setText(str(param.default)) + widget.setPlaceholderText("required" if required else "optional") + self._scalar_fields.append((name, kind, widget, required)) + self._add_row(form, name, widget) + + def _add_enum_field(self, form, name, enum_cls, param, required): + combo = QComboBox() + for member in enum_cls: + combo.addItem(str(member.value), member.value) + if not required and param.default is not None: + index = combo.findData(getattr(param.default, "value", param.default)) + if index >= 0: + combo.setCurrentIndex(index) + self._enum_fields.append((name, combo, required)) + self._add_row(form, name, combo) + + def _add_device_list_field(self, vbox, name, types_, required): + selector = DynamicDeviceSelector() + box = QGroupBox(name.replace("_", " ").title()) + if tooltip := self._param_tooltips.get(name): + box.setToolTip(tooltip) + layout = QVBoxLayout() + layout.setContentsMargins(8, 6, 8, 6) + layout.addWidget(selector) + box.setLayout(layout) + box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + vbox.addWidget(box) + self._device_fields.append( + { + "name": name, + "types": types_, + "kind": "list", + "widget": selector, + "required": required, + } + ) + + def _add_single_device_field(self, form, name, types_): + combo = QComboBox() + self._add_row(form, name, combo) + self._device_fields.append( + { + "name": name, + "types": types_, + "kind": "single", + "widget": combo, + "required": True, + } + ) + + # -- Device availability --------------------------------------------------- + + def _on_devices_changed(self, event=None): + # May be fired from a background thread; marshal onto the GUI thread. + self.signal_devices_changed.emit() + + @Slot() + def _refresh_devices(self): + for field in self._device_fields: + options = self._device_source.list_devices(field["types"]) + if field["kind"] == "list": + field["widget"].set_available(options) + else: + combo = field["widget"] + current = combo.currentData() + combo.blockSignals(True) + combo.clear() + combo.addItem(_NO_SELECTION, None) + for name in options: + combo.addItem(name, name) + index = combo.findData(current) + combo.setCurrentIndex(index if index >= 0 else 0) + combo.blockSignals(False) + + # -- Submission ------------------------------------------------------------ + + def _collect_inputs(self) -> tuple[dict, set[str]]: + """Gather plan kwargs and the set of device-valued keys. + + Device kwargs hold device *names* (strings, or a list of strings); + scalar kwargs hold Python values. Raises ValueError on bad input. + """ + kwargs: dict = {} + device_keys: set[str] = set() + + for field in self._device_fields: + name, types_, kind = field["name"], field["types"], field["kind"] + if kind == "list": + selected = field["widget"].selected_devices() + if field["required"] and not selected: + raise ValueError(f"Select at least one device for '{name}'") + for dev in selected: + self._validate_device(name, dev, types_) + if selected: + kwargs[name] = selected + device_keys.add(name) + else: + dev = field["widget"].currentData() + if dev is None: + if field["required"]: + raise ValueError(f"Select a device for '{name}'") + continue + self._validate_device(name, dev, types_) + kwargs[name] = dev + device_keys.add(name) + + for name, kind, widget, required in self._scalar_fields: + if kind is bool: + kwargs[name] = widget.isChecked() + continue + text = widget.text().strip() + if not text: + if required: + raise ValueError(f"'{name}' is required") + continue + try: + kwargs[name] = ( + int(text) + if kind is int + else float(text) + if kind is float + else text + ) + except ValueError as ex: + raise ValueError(f"'{name}' is not a valid {kind.__name__}") from ex + + for name, combo, required in self._enum_fields: + value = combo.currentData() + if value is None: + if required: + raise ValueError(f"'{name}' is required") + continue + kwargs[name] = value + + return kwargs, device_keys + + def _validate_device(self, param_name, device_name, types_): + if not self._device_source.is_valid_device(device_name, types_): + raise ValueError( + f"'{device_name}' is not a valid device for '{param_name}'" + ) + + def _format_plan_call(self, kwargs: dict, device_keys: set[str]) -> str: + """Render ``RE(plan(...))`` for in-process execution in the namespace.""" + parts = [] + for name, value in kwargs.items(): + if name in device_keys: + rendered = ( + "[" + ", ".join(value) + "]" + if isinstance(value, list) + else value + ) + else: + rendered = repr(value) + parts.append(f"{name}={rendered}") + return f"RE({self._plan.__name__}({', '.join(parts)}))" + + def _on_submit_clicked(self): + if self._worker is not None: + return + try: + kwargs, device_keys = self._collect_inputs() + except ValueError as ex: + self._set_status(str(ex), error=True) + return + + if isinstance(self._re_client, RunEngine): + self._run_in_process(kwargs, device_keys) + else: + self._submit_to_queue(kwargs) + + def _submit_to_queue(self, kwargs: dict): + item = BPlan(self._plan.__name__, **kwargs) + self._submit_button.setEnabled(False) + self._set_status("Submitting\u2026") + self._worker = FunctionWorker(self._submit_plan, item) + if self._worker.returned is None or self._worker.errored is None: + raise RuntimeError("Failed to create worker for submitting plan.") + self._worker.returned.connect(self._on_submit_returned) + self._worker.errored.connect(self._on_submit_errored) + self._worker.start() + + def _submit_plan(self, item): + self._re_client.queue_item_add(item=item) + + def _run_in_process(self, kwargs: dict, device_keys: set[str]): + code = self._format_plan_call(kwargs, device_keys) + self._set_status("Running\u2026") + # Defer so the click handler returns before IPython executes the cell. + QTimer.singleShot(0, lambda: self._execute_cell(code)) + + def _execute_cell(self, code: str): + error = run_in_ipython(code) + if error is not None: + self._set_status(f"Plan failed: {error}", error=True) + QMessageBox.critical( + self, + "Plan execution failed", + f"{type(error).__name__}: {error}", + ) + return + self._set_status("Plan complete.") + + def _on_submit_returned(self, _result=None): + self._worker = None + self._submit_button.setEnabled(True) + self._set_status("Plan added to the queue.") + + def _on_submit_errored(self, exc): + self._worker = None + self._submit_button.setEnabled(True) + self._set_status(f"Failed to add plan: {exc}", error=True) + + def _set_status(self, message: str, *, error: bool = False): + self._status_label.setText(message) + self._status_label.setStyleSheet("color: #DB3526;" if error else "") + + def closeEvent(self, event): + self._device_source.unsubscribe(self._on_devices_changed) + super().closeEvent(event) diff --git a/src/hextools/gui/re_execution_controls.py b/src/hextools/gui/re_execution_controls.py new file mode 100644 index 0000000..a7a4101 --- /dev/null +++ b/src/hextools/gui/re_execution_controls.py @@ -0,0 +1,177 @@ +"""Plan execution controls with an optional local-RunEngine mode. + +:class:`QtReExecutionControls` extends the bluesky-widgets execution-controls +widget (Pause/Resume/Stop/Abort/Halt). In the default (Queue Server) mode it +behaves exactly like the upstream widget, driven by ``model.events``. When +``local`` is True it drives an in-process RunEngine directly: the buttons call +``RE.request_pause``/``resume``/``stop``/``abort``/``halt``, button enablement +tracks ``RE.state`` via a poll timer, and the Queue-Server-only ``Ctrl-C`` +button is hidden. +""" + +from __future__ import annotations + +from collections.abc import Mapping + +from bluesky_widgets.qt.run_engine_client import ( + QtReExecutionControls as _QtReExecutionControls, +) +from bluesky_widgets.qt.threading import FunctionWorker +from qtpy.QtCore import QTimer + + +class _NullSignal: + def connect(self, *args, **kwargs): + pass + + +class _NullEvents: + status_changed = _NullSignal() + + +class _NullModel: + """Stand-in model so the base widget's status subscription is a no-op.""" + + events = _NullEvents() + + +class QtReExecutionControls(_QtReExecutionControls): + """bluesky-widgets plan execution controls with an optional local-RE mode. + + Parameters + ---------- + model : object, optional + The Queue Server client model. Ignored when ``local`` is True. + parent : QWidget, optional + Parent widget. + local : bool, optional + Drive an in-process RunEngine instead of the Queue Server. + re : object, optional + A RunEngine instance to control (local mode only). If omitted, resolved + by name from ``namespace``. + re_name : str, optional + Name of the RunEngine in the namespace. Default ``"RE"``. + namespace : Mapping[str, object], optional + Namespace to resolve ``re_name`` from. Defaults to the IPython + ``user_ns``. + poll_period : float, optional + Interval between ``RE.state`` reads, in seconds. Default: 0.5. + """ + + def __init__( + self, + model=None, + parent=None, + *, + local: bool = False, + re=None, + re_name: str = "RE", + namespace: Mapping[str, object] | None = None, + poll_period: float = 0.5, + ): + self._local = local + self._re = re + self._re_name = re_name + self._namespace = namespace + self._timer = None + self._resume_worker = None + + super().__init__(_NullModel() if local else model, parent) + + if not local: + return + + # Kernel interrupt is a Queue Server concept; hide it in local mode. + self._pb_kernel_interrupt.setVisible(False) + + self._timer = QTimer(self) + self._timer.setInterval(int(poll_period * 1000)) + self._timer.timeout.connect(self._poll_local_state) + self._timer.start() + self._poll_local_state() + + def _resolve_re(self): + if self._re is not None: + return self._re + namespace = self._namespace + if namespace is None: + namespace = _ipython_namespace() + return namespace.get(self._re_name) + + def _poll_local_state(self): + run_engine = self._resolve_re() + state = getattr(run_engine, "state", None) if run_engine is not None else None + running = state == "running" + paused = state == "paused" + self._pb_plan_pause_deferred.setEnabled(running) + self._pb_plan_pause_immediate.setEnabled(running) + self._pb_plan_resume.setEnabled(paused) + self._pb_plan_stop.setEnabled(paused) + self._pb_plan_abort.setEnabled(paused) + self._pb_plan_halt.setEnabled(paused) + + def _call_re(self, method, *args, **kwargs): + run_engine = self._resolve_re() + if run_engine is None: + return + try: + getattr(run_engine, method)(*args, **kwargs) + except Exception as ex: # noqa: BLE001 - mirror upstream best-effort behavior + print(f"Exception: {ex}") + + def _pb_plan_pause_deferred_clicked(self): + if not self._local: + return super()._pb_plan_pause_deferred_clicked() + self._call_re("request_pause", True) + + def _pb_plan_pause_immediate_clicked(self): + if not self._local: + return super()._pb_plan_pause_immediate_clicked() + self._call_re("request_pause", False) + + def _pb_plan_resume_clicked(self): + if not self._local: + return super()._pb_plan_resume_clicked() + # resume() blocks until the plan pauses or completes; run it off-thread. + run_engine = self._resolve_re() + if run_engine is None or self._resume_worker is not None: + return + self._resume_worker = FunctionWorker(run_engine.resume) + self._resume_worker.finished.connect(self._on_resume_finished) + self._resume_worker.start() + + def _on_resume_finished(self): + self._resume_worker = None + + def _pb_plan_stop_clicked(self): + if not self._local: + return super()._pb_plan_stop_clicked() + self._call_re("stop") + + def _pb_plan_abort_clicked(self): + if not self._local: + return super()._pb_plan_abort_clicked() + self._call_re("abort") + + def _pb_plan_halt_clicked(self): + if not self._local: + return super()._pb_plan_halt_clicked() + self._call_re("halt") + + def closeEvent(self, event): + if self._timer is not None: + self._timer.stop() + super().closeEvent(event) + + +def _ipython_namespace() -> Mapping[str, object]: + """Return the active IPython ``user_ns``, or an empty mapping if none.""" + try: + from IPython.core.getipython import get_ipython + + ipython = get_ipython() + except Exception: # pragma: no cover - IPython always present in this env + return {} + if ipython is None: + return {} + return ipython.user_ns diff --git a/src/hextools/gui/shutter_status.py b/src/hextools/gui/shutter_status.py new file mode 100644 index 0000000..dd0295a --- /dev/null +++ b/src/hextools/gui/shutter_status.py @@ -0,0 +1,153 @@ +"""Clickable open/closed indicators for the beamline shutters.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from bluesky import RunEngine +from qtpy.QtCore import QTimer, Qt, Signal, Slot +from qtpy.QtWidgets import ( + QGridLayout, + QGroupBox, + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, +) + +from hextools.gui._ipython import run_in_ipython +from hextools.photon_delivery_system.shutter import ( + Shutter, + ensure_shutter_closed, + ensure_shutter_open, +) + +# (top, bottom) gradient stops. +_OPEN_COLORS = ("#7bb08a", "#4a7a58") +_CLOSED_COLORS = ("#c47d7d", "#8f4b4b") +_UNKNOWN_COLORS = ("#a5a5a5", "#707070") +_BUTTON_SIZE = 80 + + +class _ShutterButton(QPushButton): + """Square colored button reflecting, and toggling, one shutter.""" + + _signal_state = Signal(bool) + + def __init__(self, var_name: str, shutter: Shutter, loop: asyncio.AbstractEventLoop, parent=None): + super().__init__(parent) + self.var_name = var_name + self.shutter = shutter + self._loop = loop + self.is_open: bool | None = None + self.setFixedSize(_BUTTON_SIZE, _BUTTON_SIZE) + self.setCursor(Qt.CursorShape.PointingHandCursor) + self._signal_state.connect(self._set_state) + self._render() + # ophyd-async subscriptions must be made on the bluesky event loop, and + # callbacks arrive on that thread; marshal back to Qt via signal. + loop.call_soon_threadsafe(shutter.subscribe_reading, self._on_reading) + + def _on_reading(self, readings: Mapping[str, Any]): + for reading in readings.values(): + self._signal_state.emit(bool(reading["value"])) + + @Slot(bool) + def _set_state(self, is_open: bool): + self.is_open = is_open + self._render() + + def _render(self): + if self.is_open is None: + (top, bottom), state = _UNKNOWN_COLORS, "unknown" + elif self.is_open: + (top, bottom), state = _OPEN_COLORS, "open" + else: + (top, bottom), state = _CLOSED_COLORS, "closed" + action = "close" if self.is_open else "open" + self.setToolTip(f"{self.shutter.name} is {state}. Click to {action}.") + # The app theme's QPushButton min-width/min-height override setFixedSize + # when the stylesheet is polished, so pin the size in the stylesheet too. + inner = _BUTTON_SIZE - 2 # minus the 1px border on each side + self.setStyleSheet( + "QPushButton { border-radius: 6px; border: 1px solid rgba(0, 0, 0, 60); padding: 0;" + f" min-width: {inner}px; max-width: {inner}px;" + f" min-height: {inner}px; max-height: {inner}px;" + f" background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {top}, stop:1 {bottom}); }}" + ) + + def unsubscribe(self): + def _clear(): + try: + self.shutter.clear_sub(self._on_reading) + except KeyError: + pass + + self._loop.call_soon_threadsafe(_clear) + + +class QtShutterStatus(QWidget): + """Shutter indicators; click one to toggle it (local mode only). + + Parameters + ---------- + re : RunEngine + The in-process RunEngine. + namespace : Mapping[str, object] + Namespace searched for :class:`Shutter` objects, typically IPython's + ``user_ns``. Aliases of the same shutter are shown once. + """ + + def __init__(self, re: RunEngine, namespace: Mapping[str, Any], parent=None): + super().__init__(parent) + self._re = re + self._namespace = namespace + + shutters: dict[int, tuple[str, Shutter]] = {} + for name, obj in sorted(namespace.items()): + if not name.startswith("_") and isinstance(obj, Shutter): + shutters.setdefault(id(obj), (name, obj)) + + self._buttons: list[_ShutterButton] = [] + grid = QGridLayout() + grid.setContentsMargins(8, 6, 8, 6) + for row, (name, shutter) in enumerate(shutters.values()): + button = _ShutterButton(name, shutter, re.loop) + button.clicked.connect(lambda *_, b=button: self._toggle(b)) + label = QLabel(name) + label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + grid.addWidget(label, row, 0) + grid.addWidget(button, row, 1) + self._buttons.append(button) + + group_box = QGroupBox("Shutters") + group_box.setLayout(grid) + vbox = QVBoxLayout() + vbox.addWidget(group_box) + self.setLayout(vbox) + + def _toggle(self, button: _ShutterButton): + if self._re.state != "idle": + QMessageBox.warning( + self, "RunEngine busy", "Cannot actuate a shutter while the RunEngine is busy." + ) + return + plan = ensure_shutter_closed if button.is_open else ensure_shutter_open + # Make the plan resolvable when the command is echoed and run in IPython. + self._namespace.setdefault(plan.__name__, plan) # type: ignore[attr-defined] + code = f"RE({plan.__name__}({button.var_name}, allow_actuation=True))" + # Defer so the click handler returns before IPython executes the cell. + QTimer.singleShot(0, lambda: self._execute_cell(code)) + + def _execute_cell(self, code: str): + error = run_in_ipython(code) + if error is not None: + QMessageBox.critical(self, "Shutter actuation failed", f"{type(error).__name__}: {error}") + + def closeEvent(self, event): + for button in self._buttons: + button.unsubscribe() + super().closeEvent(event) diff --git a/src/hextools/machine.py b/src/hextools/machine.py index 7b8e28c..1c6e3b1 100644 --- a/src/hextools/machine.py +++ b/src/hextools/machine.py @@ -1,13 +1,11 @@ """NSLS-II accelerator and storage ring devices for HEX beamline.""" -from ophyd_async.core import SignalR, StandardReadable, StrictEnum, SignalDatatypeT +from ophyd_async.core import StandardReadable, DeviceMock, set_mock_value, default_mock_class +from ophyd_async.core import StandardReadable, StrictEnum from ophyd_async.core import StandardReadableFormat as Format from ophyd_async.epics.core import EpicsDevice, epics_signal_r -from typing import TypeVar -from bluesky.suspenders import SuspendFloor - class NSLS2OpsMode(StrictEnum): OPERATIONS = "Operations" SETUP = "Setup" @@ -21,6 +19,18 @@ class NSLS2OpsMode(StrictEnum): +class MockStorageRing(DeviceMock["NSLS2StorageRing"]): + """Mock storage ring device. + + Sets the beam current to 450 mA, to not trip a suspender in mock mode. + """ + + async def connect(self, device: "NSLS2StorageRing"): + set_mock_value(device.beam_current, 450) + set_mock_value(device.operating_mode, NSLS2OpsMode.OPERATIONS) + + +@default_mock_class(MockStorageRing) class NSLS2StorageRing(StandardReadable, EpicsDevice): """NSLS-II storage ring device.""" diff --git a/src/hextools/motors.py b/src/hextools/motors.py index f8e43ee..d64cd1b 100644 --- a/src/hextools/motors.py +++ b/src/hextools/motors.py @@ -26,6 +26,7 @@ ) from ophyd_async.epics.motor import Motor as AsyncEpicsMotor +import bluesky.plan_stubs as bps def get_encoder_value_from_pos( current_position: float, encoder_resolution: float, encoder_pos_at_zero: int @@ -105,6 +106,10 @@ def _on_setpoint_write(value): callback_on_mock_put(device.user_setpoint, _on_setpoint_write) +# Make every Motor respect velocity/acceleration in mock mode by default. +default_mock_class(VelocityRespectingMotorMock)(AsyncEpicsMotor) + + @default_mock_class(VelocityRespectingMotorMock) class RotationMotor(AsyncEpicsMotor): """A motor that can be used for rotation scans. @@ -280,3 +285,24 @@ def __init__(self, prefix: str, name: str = "collimator"): self.y_fine = AsyncEpicsMotor(prefix + "CMT:1-Ax:Yf}Mtr", name="y_fine") # TODO: Consider renaming PV to follow format of above. self.slits_pitch = AsyncEpicsMotor(prefix + "Slt:CMT-Ax:Pitch}Mtr", name="slits_pitch") + + +def move_motor(motor: AsyncEpicsMotor, value: float, timeout: float | None = None): + """Move the specified motor to the given position. + + Parameters + ---------- + motor : AsyncEpicsMotor + The motor to move. + value : float + The target position for the motor. + timeout : float, optional + Maximum time to wait for the motor to reach the target position. Defaults to None. + + Raises + ------ + RuntimeError + If the motor fails to reach the target position within the specified timeout. + """ + + yield from bps.mv(motor, value, timeout=timeout) diff --git a/src/hextools/photon_delivery_system/__init__.py b/src/hextools/photon_delivery_system/__init__.py index 23127e1..517838c 100644 --- a/src/hextools/photon_delivery_system/__init__.py +++ b/src/hextools/photon_delivery_system/__init__.py @@ -1,6 +1,6 @@ """Photon delivery system (PDS) devices and plans.""" -from .dclm import DCLM, change_energy +from .dclm import DCLM, change_energy, change_beam_mode from .filters import Filter, FilterPosition, load_filters from .shutter import Shutter, ShutterStatus, ensure_shutter_closed, ensure_shutter_open, ensure_shutter_state from .slits import Slits @@ -17,4 +17,5 @@ "ensure_shutter_state", "DCLM", "change_energy", + "change_beam_mode", ] diff --git a/src/hextools/photon_delivery_system/dclm.py b/src/hextools/photon_delivery_system/dclm.py index 47cf576..44fa058 100644 --- a/src/hextools/photon_delivery_system/dclm.py +++ b/src/hextools/photon_delivery_system/dclm.py @@ -1,6 +1,7 @@ """Double crystal Laue monochromator (DCLM) device and energy change plan.""" import asyncio +from collections.abc import AsyncIterator from typing import Final import numpy as np @@ -10,9 +11,10 @@ from bluesky.preprocessors import finalize_wrapper, subs_decorator from ophyd_async.core import ( AsyncMovable, - AsyncStatus, StandardReadable, StrictEnum, + WatchableAsyncStatus, + WatcherUpdate, derived_signal_r, ) from ophyd_async.core import StandardReadableFormat as Format @@ -20,7 +22,7 @@ from ophyd_async.epics.core import EpicsDevice from ophyd_async.epics.motor import Motor as AsyncEpicsMotor -from ..utils import ensure_available, get_obj_from_ipython_ns +from ..utils import ensure_available, forward_watcher_updates from .shutter import Shutter, ensure_shutter_closed @@ -130,17 +132,40 @@ def _get_energy(self, pitch_angle: float) -> float: bragg_angle = np.deg2rad(self.bragg_angle_offset - pitch_angle) return float(self.bragg_factor / np.sin(bragg_angle)) - @AsyncStatus.wrap + @WatchableAsyncStatus.wrap async def set(self, value: BeamMode): if value == BeamMode.WHITE: xtal1_target, bs_target = self.xtal1_out, self.beam_stop_out else: xtal1_target, bs_target = self.xtal1_in, self.beam_stop_in - coros = ( + statuses = [ self.xtal1_vertical_trans.set(xtal1_target), self.cooled_beam_stop.set(bs_target), - ) - await asyncio.gather(*coros) + ] + async for update in forward_watcher_updates(statuses, self.name, combine=False): + yield update + + + + + +def change_beam_mode( + mode: BeamMode, + dclm: DCLM | None = None, +): + """Bluesky plan to change the beam mode of the DCLM. + + Parameters + ---------- + mode : BeamMode + The target beam mode to set. + dclm : DCLM, optional + The DCLM instance to control. If None, the function will attempt to + retrieve it from the IPython namespace. + """ + dclm = ensure_available(DCLM, dclm=dclm) + yield from bps.mv(dclm, mode) + def change_energy( @@ -171,6 +196,8 @@ def change_energy( fs_window_cam : AreaDetector, optional Fluorescence screen camera for auto-tuning. If None, motors are moved without feedback. + auto_tune : bool, optional + Whether to perform auto-tuning using the fluorescence screen camera. coarse_angle_range : float Half-width of the coarse pitch scan in degrees. coarse_num_steps : int diff --git a/src/hextools/profiles/collection.py b/src/hextools/profiles/collection.py index bfbb543..644aada 100644 --- a/src/hextools/profiles/collection.py +++ b/src/hextools/profiles/collection.py @@ -41,7 +41,8 @@ OpticsTable, SampleTower, FOV_20_40_mm_Camera, - Collimator, + move_motor, + CollimatorTable, EDXDTable, ) from hextools.photon_delivery_system import ( @@ -50,6 +51,8 @@ Shutter, Slits, load_filters, + change_energy, + change_beam_mode, ) from hextools.utils import ( ProposalIDPrompt, @@ -120,7 +123,7 @@ # Define our global default path provider for the beamline path_provider = NSLS2PathProvider(RE.md) -with auto_init_devices(timeout=2.0, verbose=False): +with auto_init_devices(timeout=2.0): # Shutters (Front-end and photon) fe_shutter = Shutter("XF:27IDA-PPS{Sh:FE}", name="front-end-shutter") photon_shutter = Shutter("XF:27IDA-PPS{L1-S1}", name="photon-shutter") @@ -227,24 +230,13 @@ name="germ", ) + # A few simulated motors, useful for testing sim_x = SimMotor(name="sim_x", instant=False) sim_y = SimMotor(name="sim_y", instant=False) sim_z = SimMotor(name="sim_z", instant=False) -# Install a suspender to pause the RunEngine if the beam current drops below 100 mA -# and resume when it rises above 390 mA, unless the ring is in maintenance/shutdown. -try: - _ops_mode = call_in_bluesky_event_loop(storage_ring.operating_mode.get_value()) -except Exception as e: - print(f"Could not read storage ring operating mode ({e}); beam suspender not installed.") -else: - if _ops_mode in (NSLS2OpsMode.MAINTENANCE, NSLS2OpsMode.SHUTDOWN): - print(f"Storage ring in {_ops_mode.value} mode; beam suspender not installed.") - else: - RE.install_suspender( - SuspendFloor(storage_ring.beam_current, 100, resume_thresh=390) - ) +RE.install_suspender(SuspendFloor(storage_ring.beam_current, 100, resume_thresh=390)) # Configure baseline supplemental data to include in the metadata of every run. sd = bpp.SupplementalData( diff --git a/src/hextools/tomography/alignment.py b/src/hextools/tomography/alignment.py index 8894578..868fdd8 100644 --- a/src/hextools/tomography/alignment.py +++ b/src/hextools/tomography/alignment.py @@ -17,6 +17,7 @@ from hextools.detectors.phantom import PhantomDetector from hextools.motors import RotationMotor from hextools.photon_delivery_system import Shutter +from hextools.utils import ensure_available Image = np.ndarray[tuple[int, int], np.dtype[np.uint16] | np.dtype[np.uint8]] BinaryImage = np.ndarray[tuple[int, int], np.dtype[np.bool_]] @@ -368,14 +369,14 @@ def check_alignment( def tomo_alignment_scan( dets: list[KinetixDetector | PhantomDetector], - rotation_stage: RotationMotor, - front_end_shutter: Shutter, - photon_shutter: Shutter, exposure_time: float, num_projections: int = 37, init_angle: float = 0.0, stop_angle: float = 360.0, base_x_offset: float = 0.0, + fe_shutter: Shutter | None = None, + photon_shutter: Shutter | None = None, + rot_motor: RotationMotor | None = None, sample_stage_x: AsyncEpicsMotor | None = None, ): """Tomography alignment scan. @@ -403,8 +404,13 @@ def tomo_alignment_scan( sample_stage_x : AsyncEpicsMotor | None, optional The sample stage X motor, by default None. """ + + fe_shutter = ensure_available(Shutter, fe_shutter=fe_shutter) + photon_shutter = ensure_available(Shutter, photon_shutter=photon_shutter) + rot_motor = ensure_available(RotationMotor, rot_motor=rot_motor) + # Check the shutter statuses - fe_shutter_open = yield from bps.rd(front_end_shutter.status) + fe_shutter_open = yield from bps.rd(fe_shutter.status) photon_shutter_open = yield from bps.rd(photon_shutter.status) # FE shutter must already be open. If not, raise an error. @@ -417,9 +423,9 @@ def tomo_alignment_scan( yield from bps.mv(photon_shutter, True) # Set the rotation stage to the maximum velocity before starting the scan - max_velocity = yield from bps.rd(rotation_stage.max_velocity) - yield from bps.mv(rotation_stage.velocity, max_velocity) - yield from bps.mv(rotation_stage, init_angle) + max_velocity = yield from bps.rd(rot_motor.max_velocity) + yield from bps.mv(rot_motor.velocity, max_velocity) + yield from bps.mv(rot_motor, init_angle) for det in dets: yield from bps.mv(det.driver.acquire_time, exposure_time) @@ -442,5 +448,5 @@ def tomo_alignment_scan( if flat_uid is not None: _md["flat_uid"] = flat_uid yield from bp.scan( - dets, rotation_stage, init_angle, stop_angle, num_projections, md=_md + dets, rot_motor, init_angle, stop_angle, num_projections, md=_md ) diff --git a/src/hextools/tomography/flyscans.py b/src/hextools/tomography/flyscans.py index 83a3f5d..577778c 100644 --- a/src/hextools/tomography/flyscans.py +++ b/src/hextools/tomography/flyscans.py @@ -7,7 +7,7 @@ from bluesky.protocols import Collectable, Flyable, HasName, Movable, Readable from bluesky.utils import CustomPlanMetadata, MsgGenerator, plan from nslsii import detectors -from ophyd_async.core import AsyncMovable, DetectorTrigger, StandardFlyable, TriggerInfo +from ophyd_async.core import StandardMovable, DetectorTrigger, StandardFlyable, TriggerInfo from ophyd_async.epics.adkinetix import KinetixDetector, KinetixTriggerMode from ophyd_async.fastcs.panda import HDFPanda from ophyd_async.epics.motor import Motor as AsyncEpicsMotor @@ -393,8 +393,6 @@ def _per_step( actual_start = start if not alternate_flyscan_dir or iteration_counter % 2 == 0 else stop actual_stop = stop if not alternate_flyscan_dir or iteration_counter % 2 == 0 else start - print(f"Starting flyscan from {actual_start} to {actual_stop}") - # Then, run a tomo flyscan yield from _tomo_fly_stub( detectors=[ @@ -440,7 +438,7 @@ def _cleanup(): def tomo_1d_step_scan( detectors: list[KinetixDetector | PhantomDetector], - step_motor: AsyncMovable[float], + step_motor: StandardMovable[float], step_start: float, step_stop: float, step_num: int, @@ -466,7 +464,7 @@ def tomo_1d_step_scan( ---------- detectors : list[KinetixDetector | PhantomDetector] The list of detectors to use during the scan. - step_motor : AsyncMovable[float] + step_motor : StandardMovable[float] The motor that will be moved in steps during the scan. step_start : float The starting position for the step motor. @@ -533,11 +531,11 @@ def tomo_1d_step_scan( def tomo_2d_step_scan( detectors: list[KinetixDetector | PhantomDetector], - outer_motor: AsyncMovable[float], + outer_motor: StandardMovable[float], outer_start: float, outer_stop: float, outer_num: int, - inner_motor: AsyncMovable[float], + inner_motor: StandardMovable[float], inner_start: float, inner_stop: float, inner_num: int, @@ -567,7 +565,7 @@ def tomo_2d_step_scan( ---------- detectors : list[KinetixDetector | PhantomDetector] The list of detectors to use for the scan. - outer_motor : AsyncMovable[float] + outer_motor : StandardMovable[float] The motor controlling the outer axis of the 2-D grid. outer_start : float The starting position of the outer motor. @@ -575,7 +573,7 @@ def tomo_2d_step_scan( The stopping position of the outer motor. outer_num : int The number of steps for the outer motor. - inner_motor : AsyncMovable[float] + inner_motor : StandardMovable[float] The motor controlling the inner axis of the 2-D grid. inner_start : float The starting position of the inner motor. diff --git a/src/hextools/utils.py b/src/hextools/utils/__init__.py similarity index 79% rename from src/hextools/utils.py rename to src/hextools/utils/__init__.py index 45115bb..f27e939 100644 --- a/src/hextools/utils.py +++ b/src/hextools/utils/__init__.py @@ -4,7 +4,7 @@ import os from collections.abc import MutableMapping from datetime import datetime -from typing import Any, OrderedDict, TypeVar +from typing import Any, AsyncIterator, OrderedDict, TypeVar from bluesky.run_engine import RunEngine from IPython.core.getipython import get_ipython @@ -17,12 +17,16 @@ DeviceProcessor, DeviceVector, NotConnectedError, + WatchableAsyncStatus, + Watcher, + WatcherUpdate, wait_for_connection, ) from pygments.token import Token from redis_json_dict.redis_json_dict import RedisJSONDict from rich import print as rprint from rich.console import Console +from .msg_hooks import nl_msg_hook NSVarT = TypeVar("NSVarT") @@ -279,3 +283,59 @@ def print_device_tree(device: Device, indent: int = 0) -> None: x = [] _make_tree_body(x, device) print("\n".join(x)) + + +# TODO: remove vendored copy once merged upstream +async def forward_watcher_updates( + statuses: list[WatchableAsyncStatus], name: str, combine: bool = True +) -> AsyncIterator[WatcherUpdate]: + """Forward watcher updates from several watchable moves. + + If ``combine`` is True, yield the mean fractional progress (0 to 1) of all + moves, since one status drives one progress bar and the children's positions + have different units and ranges. Otherwise, yield each child's update + unchanged, in the order they arrive. + + :param statuses: List of WatchableAsyncStatus instances to forward updates from. + :param name: Name to assign to the combined WatcherUpdate. + :param combine: If True, combine the progress of all statuses into a single update. + """ + updates: asyncio.Queue[tuple[int, dict[str, Any]]] = asyncio.Queue() + progress = [0.0] * len(statuses) + + def _make_watcher(i: int) -> Watcher[Any]: + # Watchers are only ever called with keywords, see _update_watcher. + def watcher(*_: Any, **kw: Any) -> None: + updates.put_nowait((i, kw)) + + return watcher + + for i, status in enumerate(statuses): + status.watch(_make_watcher(i)) + + all_done = asyncio.ensure_future(asyncio.gather(*statuses)) + # Keep going after all_done until the queue is drained, else final updates are lost + while not all_done.done() or not updates.empty(): + if updates.empty(): + getter = asyncio.ensure_future(updates.get()) + await asyncio.wait({getter, all_done}, return_when=asyncio.FIRST_COMPLETED) + if not getter.done(): + getter.cancel() + continue + i, kw = getter.result() + else: + i, kw = updates.get_nowait() + if not combine: + yield WatcherUpdate(**kw) + continue + span = abs(kw["target"] - kw["initial"]) + progress[i] = ( + 1.0 if span == 0 else min(abs(kw["current"] - kw["initial"]) / span, 1.0) + ) + mean = sum(progress) / len(progress) + yield WatcherUpdate( + current=mean, initial=0.0, target=1.0, name=name, fraction=1.0 - mean + ) + # Propagate any child failure. + await all_done + diff --git a/src/hextools/utils/msg_hooks.py b/src/hextools/utils/msg_hooks.py new file mode 100644 index 0000000..f2481ac --- /dev/null +++ b/src/hextools/utils/msg_hooks.py @@ -0,0 +1,769 @@ +from typing import Any, Callable +from datetime import datetime +import re as _re +import uuid as _uuid +import time as ttime +from typing import Dict, Iterable, Optional +from bluesky.utils import msg_to_json_safe_dict + +# ---------------------------------------------------------------------------------- +# Small formatting helpers +# ---------------------------------------------------------------------------------- + +def _fmt_timestamp(t: float | None) -> str: + """Format an epoch timestamp as a human-readable local wall-clock time with ms.""" + if t is None: + return "--:--:--" + try: + dt = datetime.fromtimestamp(t) + except (OverflowError, OSError, ValueError): + return str(t) + return f"{dt:%H:%M:%S}.{dt.microsecond // 1000:03d}" + +def _fmt_value(value: Any) -> str: + """Format an argument value for prose (compact, readable).""" + if value is None: + return "nothing" + if isinstance(value, bool): + return "on" if value else "off" + if isinstance(value, float): + # Trim noisy trailing zeros without losing meaningful precision. + text = f"{value:.6g}" + return text + if isinstance(value, str): + return f"'{value}'" if value else "''" + if isinstance(value, (list, tuple)): + inner = ", ".join(_fmt_value(v) for v in value) + return f"[{inner}]" + if isinstance(value, dict): + inner = ", ".join(f"{k}={_fmt_value(v)}" for k, v in value.items()) + return f"{{{inner}}}" + return str(value) + + +def _fmt_obj(obj: str | None) -> str: + """Format a device/object name.""" + if not obj: + return "the device" + return f"'{obj}'" + + +def _fmt_kwargs(kwargs: dict[str, Any], *, skip: Iterable[str] = ()) -> str: + """Format kwargs into a trailing clause, skipping bookkeeping keys like 'group'.""" + skip = set(skip) + items = {k: v for k, v in (kwargs or {}).items() if k not in skip} + if not items: + return "" + parts = [f"{k}={_fmt_value(v)}" for k, v in items.items()] + return " with " + ", ".join(parts) + + +def _join_names(items: Iterable[str]) -> str: + """Join pre-formatted phrases into an English list with an Oxford comma.""" + items = list(items) + if not items: + return "" + if len(items) == 1: + return items[0] + if len(items) == 2: + return f"{items[0]} and {items[1]}" + return ", ".join(items[:-1]) + f", and {items[-1]}" + + +class GroupLabeler: + """ + Maps bluesky group ids to friendly labels for narration. Auto-generated ids (random + UUIDs, as ``mv``/``abs_set``/``stage_all`` create by default) are named after the + *command* that opened the group -- e.g. a ``stage_all`` group becomes ``stage`` and a + ``kickoff_all`` group becomes ``kickoff``. If more than one auto group for the same + command is open at once, later ones are disambiguated with a number (``stage 2``). + Group names the user chose explicitly (e.g. ``abs_set(motor, 1, group="align")``) are + recognized as non-random and preserved verbatim. + + ``label`` returns the bare token (``stage``, ``stage 2``, ``align``); callers add the + surrounding wording (e.g. ``f"group '{label}'"``). + """ + + # bluesky auto group ids are ``str(uuid4())`` or its 32-char hex form. + _HEX32 = _re.compile(r"\A[0-9a-fA-F]{32}\Z") + + def __init__(self) -> None: + self._labels: dict[str, str] = {} + self._active: dict[str, int] = {} # base command -> count of currently-open groups + self._auto: set = set() # group ids that were auto-generated + + @classmethod + def _looks_autogenerated(cls, group: str) -> bool: + try: + _uuid.UUID(group) + return True + except (ValueError, AttributeError, TypeError): + return bool(cls._HEX32.match(group)) if isinstance(group, str) else False + + def label(self, group: str | None, command: str | None = None) -> str | None: + if group is None: + return None + if group not in self._labels: + if self._looks_autogenerated(group): + base = command or "group" + active = self._active.get(base, 0) + 1 + self._active[base] = active + self._labels[group] = base if active == 1 else f"{base} {active}" + self._auto.add(group) + else: + # Preserve a user-chosen group name verbatim. + self._labels[group] = group + return self._labels[group] + + def release(self, group: str | None) -> None: + """Mark an auto group as closed so its command name is free to reuse without a number.""" + if group is None or group not in self._auto: + return # unknown or user-named group: nothing to release + label = self._labels.get(group) + if not label: + return + base = label.rsplit(" ", 1)[0] if label[-1:].isdigit() else label + if self._active.get(base): + self._active[base] -= 1 + + +# ---------------------------------------------------------------------------------- +# Per-command describers +# ---------------------------------------------------------------------------------- + +def _group_clause(msg: dict[str, Any], groups: "GroupLabeler") -> str: + """Trailing ', as part of group X' clause, or '' when the message carries no group.""" + label = groups.label((msg.get("kwargs") or {}).get("group"), msg.get("command")) + return f", as part of group '{label}'" if label else "" + + +def _fmt_prepare_value(value: Any) -> str: + """ + Describe a ``prepare`` value (usually a dataclass) as a compact parameter phrase. + + The value has already been serialized, so a dataclass typically arrives either as a + dict of its fields or as its ``repr`` string (e.g. ``TriggerInfo(number=1801, ...)``). + """ + if value is None: + return "" + if isinstance(value, dict): + return ", ".join(f"{k}={_fmt_value(v)}" for k, v in value.items()) + if isinstance(value, (list, tuple)): + return _fmt_value(value) + return str(value).strip() + + +def _describe(msg: dict[str, Any], groups: "GroupLabeler") -> str: + """Return the sentence for a single serialized message.""" + command = msg.get("command") + obj = _fmt_obj(msg.get("obj")) + args = msg.get("args") or [] + kwargs = msg.get("kwargs") or {} + + match command: + case "set": + target = _fmt_value(args[0]) if args else "a new value" + return f"Setting {obj} to {target}{_group_clause(msg, groups)}." + case "trigger": + return f"Triggering {obj}{_group_clause(msg, groups)}." + case "read": + return f"Reading the current value of {obj}." + case "wait": + group = groups.label(kwargs.get("group")) + if group: + return f"Waiting for group '{group}' to finish." + return "Waiting for pending operations to complete." + case "sleep": + if args: + return f"Pausing for {_fmt_value(args[0])} seconds." + return "Pausing briefly." + case "checkpoint": + return "Marking a checkpoint (a safe point to pause or resume)." + case "clear_checkpoint": + return "Clearing the current checkpoint (pausing here is no longer safe)." + case "create": + name = kwargs.get("name") + stream = f" for stream '{name}'" if name else "" + return f"Beginning a new data reading{stream}." + case "save": + return "Saving the collected readings as a data point." + case "drop": + return "Discarding the current in-progress data point." + case "open_run": + plan_name = kwargs.get("plan_name") + scan_id = kwargs.get("scan_id") + bits = [] + if plan_name: + bits.append(f"plan '{plan_name}'") + if scan_id is not None: + bits.append(f"scan id {scan_id}") + detail = (" (" + ", ".join(bits) + ")") if bits else "" + return f"Starting a new run{detail}." + case "close_run": + exit_status = kwargs.get("exit_status") + if exit_status: + return f"Closing the run ({exit_status})." + return "Closing the current run." + case "stage": + return f"Staging {obj} for data collection{_group_clause(msg, groups)}." + case "unstage": + return f"Unstaging {obj}{_group_clause(msg, groups)}." + case "monitor": + return f"Starting to monitor {obj} for asynchronous updates{_group_clause(msg, groups)}." + case "unmonitor": + return f"Stopping monitoring of {obj}{_group_clause(msg, groups)}." + case "configure": + tail = _fmt_kwargs(kwargs, skip=("group",)) + return f"Configuring {obj}{tail}{_group_clause(msg, groups)}." + case "subscribe": + return "Attaching a callback to receive documents." + case "unsubscribe": + return "Detaching a previously attached callback." + case "kickoff": + return f"Kicking off flyscan with {obj}{_group_clause(msg, groups)}." + case "complete": + return f"Waiting flyscan with {obj} to complete{_group_clause(msg, groups)}." + case "collect": + return f"Collecting buffered data from {obj}{_group_clause(msg, groups)}." + case "declare_stream": + return "Declaring the structure of a data stream." + case "stop": + return f"Stopping {obj}." + case "locate": + return f"Locating the current position of {obj}." + case "prepare": + params = _fmt_prepare_value(args[0]) if args else "" + with_clause = f" with {params}" if params else "" + return f"Preparing {obj} for the next step{with_clause}{_group_clause(msg, groups)}." + case "pause": + return "Pausing the plan." + case "resume": + return "Resuming the plan." + case "null": + return "Doing nothing (no-op)." + case "wait_for": + return "Waiting for one or more external tasks to complete." + case _: + # Unknown/rare commands: state it plainly using whatever we have. + where = f" on {obj}" if msg.get("obj") else "" + pretty = str(command).replace("_", " ") if command else "an operation" + return f"Performing '{pretty}'{where}." + + +def describe_message( + msg: dict[str, Any], + *, + groups: GroupLabeler | None = None, + show_timestamps: bool = False, +) -> str: + """ + Convert a single serialized RunEngine message into a natural-language sentence. + + Parameters + ---------- + msg : dict + A serialized message (see module docstring for the shape). + groups : GroupLabeler, optional + Shared labeler so group ids are named consistently across a stream. A fresh + one is created if not supplied. + show_timestamps : bool, optional + If True, prefix the sentence with the message's human-readable timestamp, + e.g. ``[14:30:45.123] Setting 'motor' to 1.``. + + Returns + ------- + str + A human-readable, best-effort description of the message. + """ + sentence = _describe(msg, groups or GroupLabeler()) + + if show_timestamps: + return f"[{_fmt_timestamp(msg.get('time'))}] {sentence}" + return sentence + + +# ---------------------------------------------------------------------------------- +# Stream-level narration (collapses common multi-message patterns) +# ---------------------------------------------------------------------------------- + +# Commands whose messages each target a single object and naturally arrive back-to-back +# (one per device). Consecutive messages of the same command are merged into one line, +# but only when they share the same group (see StreamNarrator). Each entry returns the +# sentence *core* (no trailing period and no group clause); the group clause -- and, for +# ``prepare``, the shared value -- are appended by StreamNarrator._flush_batch. +_BATCH_PHRASES = { + "read": lambda names: ( + f"Reading the current value of {names[0]}" + if len(names) == 1 + else f"Reading the current values of {_join_names(names)}" + ), + "stage": lambda names: f"Staging {_join_names(names)} for data collection", + "unstage": lambda names: f"Unstaging {_join_names(names)}", + "monitor": lambda names: f"Starting to monitor {_join_names(names)} for asynchronous updates", + "unmonitor": lambda names: f"Stopping monitoring of {_join_names(names)}", + "kickoff": lambda names: ( + f"Kicking off the flyer {names[0]} (starting a fly-scan)" + if len(names) == 1 + else f"Kicking off the flyers {_join_names(names)} (starting a fly-scan)" + ), + "complete": lambda names: ( + f"Waiting for the flyer {names[0]} to finish flying" + if len(names) == 1 + else f"Waiting for the flyers {_join_names(names)} to finish flying" + ), + "collect": lambda names: f"Collecting buffered data from {_join_names(names)}", + "prepare": lambda names: f"Preparing {_join_names(names)} for the next step", +} + + +def narrate_stream( + messages: Iterable[dict[str, Any]], + *, + show_timestamps: bool = False, + window: float = 60.0, +) -> list[str]: + """ + Convert a whole sequence of serialized messages into a list of natural-language + lines, collapsing common multi-message patterns into single sentences: + + * one or more ``set`` messages sharing a group, followed by a ``wait`` on that group + (what ``mv``/``mvr`` emit) -> a single "Moving ..." sentence; + * one or more ``trigger`` messages sharing a group, followed by a ``wait`` (what + triggering a list of detectors emits) -> a single "Triggering ..." sentence; + * a run of consecutive same-command messages that target one device each -- ``read``, + ``stage``, ``unstage``, ``monitor``, ``unmonitor`` -> a single combined sentence. + + Messages are only joined while they fall within ``window`` seconds of the start of + the run (see :class:`StreamNarrator`). This is a convenience wrapper over + :class:`StreamNarrator` for the case where the full sequence is already available. + + Parameters + ---------- + messages : iterable of dict + Serialized messages, in the order they were processed. + show_timestamps : bool, optional + If True, prefix each line with the human-readable timestamp of the message that + produced it (for a collapsed line, the time of the first message in the group). + window : float, optional + Maximum span, in seconds, over which same-kind messages are joined. + + Returns + ------- + list of str + Human-readable narration lines. + """ + narrator = StreamNarrator(show_timestamps=show_timestamps, window=window) + lines: list[str] = [] + for msg in messages: + lines.extend(narrator.push(msg)) + lines.extend(narrator.flush()) + return lines + + +class StreamNarrator: + """ + Incremental narrator for a *live* message stream consumed one message at a time. + + Messages of the same collapsible kind are joined into a single line, but only while + they fall within ``window`` seconds of the start of the current run. A pending line + is flushed (returned to the caller) as soon as: + + * a message of a different kind arrives, or + * the time window elapses -- either because an incoming message is more than + ``window`` seconds after the run started (handled in :meth:`push`), or because + :meth:`due` is called after a period of inactivity. + + All methods return a list of finalized narration lines (possibly empty). Typical + use with a blocking consumer:: + + narrator = StreamNarrator(show_timestamps=True) + while True: + msg = queue.get(timeout=5) # or however messages arrive + if msg is not None: + for line in narrator.push(msg): + print(line) + for line in narrator.due(): # flush stale lines while idle + print(line) + # at shutdown: + for line in narrator.flush(): + print(line) + + Parameters + ---------- + show_timestamps : bool, optional + Prefix each line with the human-readable timestamp of the message that produced + it (for a collapsed line, the time of the first message in the run). + window : float, optional + Maximum span, in seconds, over which same-kind messages are joined. Defaults to + 60 seconds. + """ + + def __init__(self, *, show_timestamps: bool = False, window: float = 60.0) -> None: + self._show_timestamps = show_timestamps + self._window = float(window) + self._groups = GroupLabeler() + # At most one of {batch, groups} is ever open (starting one flushes the other). + self._batch_cmd: str | None = None + self._batch_msgs: list[dict[str, Any]] = [] + self._batch_group: str | None = None # shared group of the open batch (or None) + self._batch_value: Any = None # shared `prepare` value of the open batch + # Pending `set`/`trigger` messages awaiting a matching `wait`, keyed by group id. + self._pending_group: dict[str, list[dict[str, Any]]] = {} + self._group_order: list[str] = [] # arrival order of open groups + + # -- formatting ------------------------------------------------------------ + + def _line(self, msg: dict[str, Any], text: str | None = None) -> str: + text = text if text is not None else describe_message(msg, groups=self._groups) + if self._show_timestamps: + text = f"[{_fmt_timestamp(msg.get('time'))}] {text}" + return text + + @staticmethod + def _span_exceeded(msgs: list[dict[str, Any]], t: float | None, window: float) -> bool: + """True if ``t`` is more than ``window`` seconds after the run's first message.""" + if not msgs or t is None: + return False + start = msgs[0].get("time") + return start is not None and (t - start) > window + + # -- flush helpers (each returns a list of finalized lines) ---------------- + + def _flush_batch(self) -> list[str]: + out: list[str] = [] + if self._batch_msgs: + # Unique object names, preserving first-seen order. + names: list[str] = [] + seen = set() + for m in self._batch_msgs: + name = _fmt_obj(m.get("obj")) + if name not in seen: + seen.add(name) + names.append(name) + core = _BATCH_PHRASES[self._batch_cmd](names) + if self._batch_cmd == "prepare": + params = _fmt_prepare_value(self._batch_value) + if params: + core += f" with {params}" + if self._batch_group is not None: + label = self._groups.label(self._batch_group, self._batch_cmd) + core += f", as part of group '{label}'" + out.append(self._line(self._batch_msgs[0], core + ".")) + self._batch_cmd, self._batch_msgs = None, [] + self._batch_group, self._batch_value = None, None + return out + + def _flush_group(self, group_id: str) -> list[str]: + # A group that never saw its `wait`: describe its messages plainly. + out = [self._line(m) for m in self._pending_group.pop(group_id, [])] + if group_id in self._group_order: + self._group_order.remove(group_id) + return out + + def _flush_all_groups(self) -> list[str]: + out: list[str] = [] + for gid in list(self._group_order): + out.extend(self._flush_group(gid)) + return out + + def _complete_group(self, msgs: list[dict[str, Any]]) -> list[str]: + # A group closed by its `wait`: collapse into a single "Moving"/"Triggering" line. + pronoun = "it" if len(msgs) == 1 else "them" + group_id = (msgs[0].get("kwargs") or {}).get("group") + label = self._groups.label(group_id, msgs[0].get("command")) + clause = f", as part of group '{label}'" if label else "" + if all(m.get("command") == "set" for m in msgs): + moves = [] + for m in msgs: + args = m.get("args") or [] + target = _fmt_value(args[0]) if args else "a new value" + moves.append(f"{_fmt_obj(m.get('obj'))} to {target}") + return [self._line(msgs[0], f"Moving {_join_names(moves)} and waiting for {pronoun} to arrive{clause}.")] + if all(m.get("command") == "trigger" for m in msgs): + names = [_fmt_obj(m.get("obj")) for m in msgs] + return [self._line(msgs[0], f"Triggering {_join_names(names)} and waiting for {pronoun} to complete{clause}.")] + # Mixed group: describe each action, then note the wait. + out = [self._line(m) for m in msgs] + out.append(self._line(msgs[-1], f"Waiting for group '{label}' to finish." if label else "Waiting for the group to complete.")) + return out + + # -- public API ------------------------------------------------------------ + + def push(self, msg: dict[str, Any]) -> list[str]: + """Consume one serialized message; return any finalized narration lines.""" + command = msg.get("command") + kwargs = msg.get("kwargs") or {} + group_id = kwargs.get("group") + t = msg.get("time") + out: list[str] = [] + + if command in _BATCH_PHRASES: + out.extend(self._flush_all_groups()) # a batch ends any pending group + value = (msg.get("args") or [None])[0] if command == "prepare" else None + # Join only same-command messages that share the same group (and, for + # `prepare`, the same value) and fall within the time window. + joinable = ( + self._batch_cmd == command + and self._batch_group == group_id + and (command != "prepare" or self._batch_value == value) + and not self._span_exceeded(self._batch_msgs, t, self._window) + ) + if joinable: + self._batch_msgs.append(msg) + else: + out.extend(self._flush_batch()) + self._batch_cmd, self._batch_msgs = command, [msg] + self._batch_group, self._batch_value = group_id, value + return out + + if command in ("set", "trigger") and group_id is not None: + out.extend(self._flush_batch()) + existing = self._pending_group.get(group_id) + if existing and self._span_exceeded(existing, t, self._window): + out.extend(self._flush_group(group_id)) + if group_id not in self._pending_group: + self._pending_group[group_id] = [] + self._group_order.append(group_id) + self._pending_group[group_id].append(msg) + return out + + if command == "wait" and group_id is not None and group_id in self._pending_group: + out.extend(self._flush_batch()) + msgs = self._pending_group.pop(group_id) + self._group_order.remove(group_id) + out.extend(self._complete_group(msgs)) + self._groups.release(group_id) + return out + + # Any other message: flush pending buffers first so ordering stays truthful. + out.extend(self._flush_batch()) + out.extend(self._flush_all_groups()) + out.append(self._line(msg)) + if command == "wait" and group_id is not None: + self._groups.release(group_id) + return out + + def due(self, now: float | None = None) -> list[str]: + """ + Flush any open batch/group that has been idle (no new message) for longer than + ``window`` seconds, using ``now`` (defaults to the current wall-clock time) as + the reference. Call periodically while waiting for the next message so a run of + similar messages does not stay buffered indefinitely on a quiet stream. + """ + if now is None: + now = ttime.time() + out: list[str] = [] + + if self._batch_msgs: + last = self._batch_msgs[-1].get("time") + if last is not None and (now - last) > self._window: + out.extend(self._flush_batch()) + + for gid in list(self._group_order): + msgs = self._pending_group.get(gid, []) + last = msgs[-1].get("time") if msgs else None + if last is not None and (now - last) > self._window: + out.extend(self._flush_group(gid)) + + return out + + def flush(self) -> list[str]: + """Flush all pending buffers (call at end of stream).""" + out = self._flush_batch() + out.extend(self._flush_all_groups()) + return out + + +# ---------------------------------------------------------------------------------- +# RunEngine msg_hook +# ---------------------------------------------------------------------------------- + +class MsgHookNarrator: + """ + A bluesky ``RunEngine`` ``msg_hook`` that narrates a plan's activity in plain English. + + Attach an instance to a RunEngine and it will, for every ``Msg`` the RunEngine + processes, serialize the message, feed it to a :class:`StreamNarrator`, and emit any + finalized narration lines (printed by default):: + + RE.msg_hook = MsgHookNarrator() + + By default the hook consumes *raw* ``Msg`` objects and serializes them itself. If you + are feeding it messages that are already serialized (dicts of the documented shape), + pass ``serialize=False``. + + Parameters + ---------- + emit : callable, optional + Called with each finalized narration line (a ``str``). Defaults to :func:`print`. + show_timestamps : bool, optional + Prefix each line with the message's human-readable timestamp. + window : float, optional + Maximum span, in seconds, over which same-kind messages are joined (see + :class:`StreamNarrator`). + serialize : bool, optional + If True (default), incoming values are treated as raw ``Msg`` objects and + serialized via :func:`serialize_msg`. If False, they are assumed to already be + serialized dicts. + """ + + def __init__( + self, + *, + emit: Callable[[str], Any] | None = None, + show_timestamps: bool = False, + window: float = 60.0, + serialize: bool = True, + ) -> None: + self._emit = emit if emit is not None else print + self._serialize = serialize + self._narrator = StreamNarrator(show_timestamps=show_timestamps, window=window) + self._listeners: list[Callable[[str], Any]] = [] + + def add_listener(self, callback: Callable[[str], Any]) -> None: + """Also send each finalized line to ``callback`` (called on the RunEngine thread).""" + self._listeners.append(callback) + + def remove_listener(self, callback: Callable[[str], Any]) -> None: + if callback in self._listeners: + self._listeners.remove(callback) + + def _send(self, line: str) -> None: + self._emit(line) + for listener in list(self._listeners): + try: + listener(line) + except Exception: + # A broken listener must not fail the running plan. + pass + + def __call__(self, msg: Any) -> None: + """Consume one message (raw ``Msg`` by default); emit any finalized lines.""" + record = msg_to_json_safe_dict(msg) if self._serialize else msg + for line in self._narrator.push(record): + self._send(line) + + def due(self, now: float | None = None) -> None: + """Emit any lines whose buffered run has been idle longer than ``window`` seconds.""" + for line in self._narrator.due(now): + self._send(line) + + def flush(self) -> None: + """Emit all remaining buffered lines (call when the plan/stream ends).""" + for line in self._narrator.flush(): + self._send(line) + + +#: Ready-to-use natural-language ``msg_hook`` instance: ``RE.msg_hook = nl_msg_hook``. +nl_msg_hook = MsgHookNarrator() + + +# ---------------------------------------------------------------------------------- +# Demo +# ---------------------------------------------------------------------------------- + +# if __name__ == "__main__": +# # Example stream resembling reading several detectors, with a mix of an +# # auto-generated group (UUID) and a user-named group ("align"). +# example_stream = [ +# {"time": 0.0, "command": "open_run", "obj": None, "args": [], +# "kwargs": {"plan_name": "scan", "scan_id": 3}, "run": None}, +# {"time": 0.0, "command": "stage", "obj": "det1", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "set", "obj": "motor", "args": [0], +# "kwargs": {"group": "align"}, "run": None}, +# {"time": 0.0, "command": "wait", "obj": None, "args": [], +# "kwargs": {"group": "align"}, "run": None}, +# {"time": 0.0, "command": "create", "obj": None, "args": [], "kwargs": {"name": "primary"}, "run": None}, +# {"time": 0.0, "command": "trigger", "obj": "det1", "args": [], +# "kwargs": {"group": "trigger"}, "run": None}, +# {"time": 0.0, "command": "trigger", "obj": "det2", "args": [], +# "kwargs": {"group": "trigger"}, "run": None}, +# {"time": 0.0, "command": "wait", "obj": None, "args": [], +# "kwargs": {"group": "trigger"}, "run": None}, +# {"time": 0.0, "command": "read", "obj": "det1", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "read", "obj": "det2", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "read", "obj": "motor", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "save", "obj": None, "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "set", "obj": "motor", "args": [1], +# "kwargs": {"group": "a1b2c3d4-e5f6-4718-a93a-4b5c6d7e8f90"}, "run": None}, +# {"time": 0.0, "command": "wait", "obj": None, "args": [], +# "kwargs": {"group": "a1b2c3d4-e5f6-4718-a93a-4b5c6d7e8f90"}, "run": None}, +# {"time": 0.0, "command": "unstage", "obj": "det1", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "unstage", "obj": "det2", "args": [], "kwargs": {}, "run": None}, +# {"time": 0.0, "command": "close_run", "obj": None, "args": [], +# "kwargs": {"exit_status": "success"}, "run": None}, +# ] + +# # Assign increasing wall-clock timestamps so the human-readable times are meaningful. +# _base = _time.time() +# for _i, _m in enumerate(example_stream): +# _m["time"] = _base + _i * 0.5 + +# print("=== describe_message (one line per raw message) ===") +# _groups = GroupLabeler() +# for m in example_stream: +# print(" -", describe_message(m, groups=_groups)) + +# print("\n=== narrate_stream (collapsed / friendlier) ===") +# for line in narrate_stream(example_stream): +# print(" -", line) + +# print("\n=== narrate_stream with timestamps ===") +# for line in narrate_stream(example_stream, show_timestamps=True): +# print(" -", line) + +# # --- Live streaming demo: consume one message at a time with a short window. --- +# # Three reads arrive close together, then a 4th read arrives after a long gap, so +# # the window splits it into a separate line even though it is the same command. +# print("\n=== StreamNarrator (live, window=10s) ===") +# _t0 = _time.time() +# live_stream = [ +# {"time": _t0 + 0.0, "command": "read", "obj": "det1", "args": [], "kwargs": {}, "run": None}, +# {"time": _t0 + 1.0, "command": "read", "obj": "det2", "args": [], "kwargs": {}, "run": None}, +# {"time": _t0 + 2.0, "command": "read", "obj": "det3", "args": [], "kwargs": {}, "run": None}, +# # ... long pause (> window) ... +# {"time": _t0 + 45.0, "command": "read", "obj": "det4", "args": [], "kwargs": {}, "run": None}, +# {"time": _t0 + 46.0, "command": "save", "obj": None, "args": [], "kwargs": {}, "run": None}, +# ] +# narrator = StreamNarrator(show_timestamps=True, window=10.0) +# for _m in live_stream: +# for _line in narrator.push(_m): +# print(" -", _line) +# for _line in narrator.flush(): +# print(" -", _line) + +# # --- msg_hook demo: feed *raw* Msg objects; the hook serializes them itself. --- +# # Simulates `RE.msg_hook = MsgHookNarrator()` running over a tiny plan. Devices are +# # serialized via repr(), so their type shows up in the narration. +# from collections import namedtuple + +# _Msg = namedtuple("Msg", ["command", "obj", "args", "kwargs", "run"]) + +# class _DemoMotor: +# def __init__(self, name): +# self.name = name + +# def __repr__(self): +# return f"EpicsMotor(name={self.name!r})" + +# def _raw(command, obj=None, args=(), kwargs=None, run=None): +# return _Msg(command, obj, args, kwargs or {}, run) + +# _motor = _DemoMotor("theta") +# raw_plan = [ +# _raw("open_run", kwargs={"plan_name": "rel_scan", "scan_id": 7}), +# _raw("stage", _motor), +# _raw("set", _motor, args=(90,), kwargs={"group": "move-1"}), +# _raw("wait", kwargs={"group": "move-1"}), +# _raw("trigger", _DemoMotor("det"), kwargs={"group": "trig"}), +# _raw("wait", kwargs={"group": "trig"}), +# _raw("unstage", _motor), +# _raw("close_run", kwargs={"exit_status": "success"}), +# ] + +# print("\n=== MsgHookNarrator (raw Msg -> serialized -> narration) ===") +# hook = MsgHookNarrator(emit=lambda s: print(" -", s)) +# for _m in raw_plan: # this is exactly what RE.msg_hook would receive +# hook(_m) +# hook.flush() + diff --git a/tests/tomography/test_alignment.py b/tests/tomography/test_alignment.py index 97ba2e3..34b739f 100644 --- a/tests/tomography/test_alignment.py +++ b/tests/tomography/test_alignment.py @@ -407,7 +407,15 @@ async def test_tomo_alignment_scan_fails_if_fe_shutter_closed( ) with pytest.raises(ValueError, match="Front-end shutter is closed"): - RE(tomo_alignment_scan([], rotation_motor, fe_shutter, photon_shutter, 0.1)) + RE( + tomo_alignment_scan( + [], + 0.1, + fe_shutter=fe_shutter, + photon_shutter=photon_shutter, + rot_motor=rotation_motor, + ) + ) @pytest.mark.parametrize( @@ -469,14 +477,14 @@ def msg_hook(msg: Msg): runs: RunEngineResult = RE( tomo_alignment_scan( [ktx1], - rotation_motor, - fe_shutter, - photon_shutter, exposure_time, num_projections=num_projections, init_angle=init_angle, stop_angle=stop_angle, base_x_offset=base_x_offset, + fe_shutter=fe_shutter, + photon_shutter=photon_shutter, + rot_motor=rotation_motor, sample_stage_x=None if not include_sample_stage_x else sample_stage_x, ), cache_docs, # type: ignore